mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2025-12-31 04:00:03 +01:00
(feature): migrate to helm and k8s, cleanup
This commit is contained in:
parent
35eec5d864
commit
30f5ededa3
@ -8,8 +8,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.6" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -20,14 +20,14 @@ enum ApiRoutes {
|
||||
// CERTS_FLOW_HOSRS_WITH_UPCOMING_SSL_EXPIRY = `api/CertsFlow/HostsWithUpcomingSslExpiry/{sessionId}`
|
||||
}
|
||||
|
||||
const apiBase = process.env.NEXT_PUBLIC_API_BASE_URL ?? ''
|
||||
|
||||
const GetApiRoute = (route: ApiRoutes, ...args: string[]): string => {
|
||||
let result: string = route
|
||||
args.forEach((arg) => {
|
||||
result = result.replace(/{.*?}/, arg)
|
||||
})
|
||||
// TODO: need env var
|
||||
return `http://localhost:8080/${result}`
|
||||
//return `http://websrv0001.corp.maks-it.com:8080/${result}`
|
||||
return `${apiBase.replace(/\/+$/, '')}/${result.replace(/^\/+/, '')}`
|
||||
}
|
||||
|
||||
export { GetApiRoute, ApiRoutes }
|
||||
|
||||
@ -14,7 +14,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.9" />
|
||||
<PackageReference Include="System.Threading.RateLimiting" Version="9.0.9" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -1,105 +1,93 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.RateLimiting;
|
||||
|
||||
public class LockManager : IDisposable {
|
||||
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
|
||||
private readonly ConcurrentDictionary<int, int> _reentrantCounts = new ConcurrentDictionary<int, int>();
|
||||
private readonly TokenBucketRateLimiter _rateLimiter = new TokenBucketRateLimiter(new TokenBucketRateLimiterOptions {
|
||||
TokenLimit = 5, // max 5 requests per second (adjust as needed)
|
||||
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
|
||||
QueueLimit = 100,
|
||||
ReplenishmentPeriod = TimeSpan.FromSeconds(1),
|
||||
TokensPerPeriod = 5,
|
||||
AutoReplenishment = true
|
||||
});
|
||||
|
||||
public async Task<T> ExecuteWithLockAsync<T>(Func<Task<T>> action) {
|
||||
var lease = await _rateLimiter.AcquireAsync(1);
|
||||
if (!lease.IsAcquired) throw new InvalidOperationException("Rate limit exceeded");
|
||||
|
||||
var threadId = Thread.CurrentThread.ManagedThreadId;
|
||||
|
||||
if (!_reentrantCounts.ContainsKey(threadId)) {
|
||||
_reentrantCounts[threadId] = 0;
|
||||
}
|
||||
|
||||
if (_reentrantCounts[threadId] == 0) {
|
||||
await _semaphore.WaitAsync();
|
||||
}
|
||||
|
||||
if (!_reentrantCounts.ContainsKey(threadId)) _reentrantCounts[threadId] = 0;
|
||||
if (_reentrantCounts[threadId] == 0) await _semaphore.WaitAsync();
|
||||
_reentrantCounts[threadId]++;
|
||||
try {
|
||||
return await action();
|
||||
}
|
||||
finally {
|
||||
_reentrantCounts[threadId]--;
|
||||
if (_reentrantCounts[threadId] == 0) {
|
||||
_semaphore.Release();
|
||||
}
|
||||
if (_reentrantCounts[threadId] == 0) _semaphore.Release();
|
||||
lease.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ExecuteWithLockAsync(Func<Task> action) {
|
||||
var lease = await _rateLimiter.AcquireAsync(1);
|
||||
if (!lease.IsAcquired) throw new InvalidOperationException("Rate limit exceeded");
|
||||
|
||||
var threadId = Thread.CurrentThread.ManagedThreadId;
|
||||
|
||||
if (!_reentrantCounts.ContainsKey(threadId)) {
|
||||
_reentrantCounts[threadId] = 0;
|
||||
}
|
||||
|
||||
if (_reentrantCounts[threadId] == 0) {
|
||||
await _semaphore.WaitAsync();
|
||||
}
|
||||
|
||||
if (!_reentrantCounts.ContainsKey(threadId)) _reentrantCounts[threadId] = 0;
|
||||
if (_reentrantCounts[threadId] == 0) await _semaphore.WaitAsync();
|
||||
_reentrantCounts[threadId]++;
|
||||
try {
|
||||
await action();
|
||||
}
|
||||
finally {
|
||||
_reentrantCounts[threadId]--;
|
||||
if (_reentrantCounts[threadId] == 0) {
|
||||
_semaphore.Release();
|
||||
}
|
||||
if (_reentrantCounts[threadId] == 0) _semaphore.Release();
|
||||
lease.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<T> ExecuteWithLockAsync<T>(Func<T> action) {
|
||||
var lease = await _rateLimiter.AcquireAsync(1);
|
||||
if (!lease.IsAcquired) throw new InvalidOperationException("Rate limit exceeded");
|
||||
|
||||
var threadId = Thread.CurrentThread.ManagedThreadId;
|
||||
|
||||
if (!_reentrantCounts.ContainsKey(threadId)) {
|
||||
_reentrantCounts[threadId] = 0;
|
||||
}
|
||||
|
||||
if (_reentrantCounts[threadId] == 0) {
|
||||
await _semaphore.WaitAsync();
|
||||
}
|
||||
|
||||
if (!_reentrantCounts.ContainsKey(threadId)) _reentrantCounts[threadId] = 0;
|
||||
if (_reentrantCounts[threadId] == 0) await _semaphore.WaitAsync();
|
||||
_reentrantCounts[threadId]++;
|
||||
try {
|
||||
return await Task.Run(action);
|
||||
}
|
||||
finally {
|
||||
_reentrantCounts[threadId]--;
|
||||
if (_reentrantCounts[threadId] == 0) {
|
||||
_semaphore.Release();
|
||||
}
|
||||
if (_reentrantCounts[threadId] == 0) _semaphore.Release();
|
||||
lease.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ExecuteWithLockAsync(Action action) {
|
||||
var lease = await _rateLimiter.AcquireAsync(1);
|
||||
if (!lease.IsAcquired) throw new InvalidOperationException("Rate limit exceeded");
|
||||
|
||||
var threadId = Thread.CurrentThread.ManagedThreadId;
|
||||
|
||||
if (!_reentrantCounts.ContainsKey(threadId)) {
|
||||
_reentrantCounts[threadId] = 0;
|
||||
}
|
||||
|
||||
if (_reentrantCounts[threadId] == 0) {
|
||||
await _semaphore.WaitAsync();
|
||||
}
|
||||
|
||||
if (!_reentrantCounts.ContainsKey(threadId)) _reentrantCounts[threadId] = 0;
|
||||
if (_reentrantCounts[threadId] == 0) await _semaphore.WaitAsync();
|
||||
_reentrantCounts[threadId]++;
|
||||
try {
|
||||
await Task.Run(action);
|
||||
}
|
||||
finally {
|
||||
_reentrantCounts[threadId]--;
|
||||
if (_reentrantCounts[threadId] == 0) {
|
||||
_semaphore.Release();
|
||||
}
|
||||
if (_reentrantCounts[threadId] == 0) _semaphore.Release();
|
||||
lease.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_semaphore.Dispose();
|
||||
_rateLimiter.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
7
src/Deploy-Helm.bat
Normal file
7
src/Deploy-Helm.bat
Normal file
@ -0,0 +1,7 @@
|
||||
@echo off
|
||||
|
||||
REM Change directory to the location of the script
|
||||
cd /d %~dp0
|
||||
|
||||
REM Invoke the PowerShell script (Release-NuGetPackage.ps1) in the same directory
|
||||
powershell -ExecutionPolicy Bypass -File "%~dp0Deploy-Helm.ps1"
|
||||
65
src/Deploy-Helm.ps1
Normal file
65
src/Deploy-Helm.ps1
Normal file
@ -0,0 +1,65 @@
|
||||
# Set variables
|
||||
$projectName = "certs-ui"
|
||||
$namespace = "certs-ui"
|
||||
$chartPath = "./helm"
|
||||
$harborUrl = "cr.maks-it.com"
|
||||
$loadBalancerIP = "172.16.0.5"
|
||||
|
||||
# Retrieve and decode username:password from environment variable (Base64)
|
||||
try {
|
||||
$decoded = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Env:CR_MAKS_IT))
|
||||
} catch {
|
||||
Write-Error "Failed to decode CR_MAKS_IT as Base64. Expected base64('username:password')."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Split decoded credentials
|
||||
$creds = $decoded -split ':', 2
|
||||
$harborUsername = $creds[0]
|
||||
$harborPassword = $creds[1]
|
||||
|
||||
# Verify environment variable
|
||||
if (-not $harborUsername -or -not $harborPassword) {
|
||||
Write-Error "Decoded CR_MAKS_IT must be in the format 'username:password'."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Ensure namespace exists
|
||||
if (-not (kubectl get ns $namespace -o name 2>$null)) {
|
||||
Write-Output "Creating namespace '$namespace'..."
|
||||
kubectl create namespace $namespace | Out-Null
|
||||
}
|
||||
else {
|
||||
Write-Output "Namespace '$namespace' already exists."
|
||||
}
|
||||
|
||||
# Create or update Docker registry pull secret
|
||||
Write-Output "Creating or updating image pull secret..."
|
||||
kubectl -n $namespace create secret docker-registry cr-maksit-pull `
|
||||
--docker-server=$harborUrl `
|
||||
--docker-username=$harborUsername `
|
||||
--docker-password=$harborPassword `
|
||||
--docker-email="devnull@maks-it.com" `
|
||||
--dry-run=client -o yaml | kubectl apply -f - | Out-Null
|
||||
|
||||
# Lint Helm chart
|
||||
Write-Output "Linting Helm chart..."
|
||||
helm lint $chartPath
|
||||
|
||||
# Render Helm chart to verify output (optional)
|
||||
Write-Output "Rendering Helm chart for validation..."
|
||||
helm template $projectName $chartPath -n $namespace | Out-Null
|
||||
|
||||
# Deploy Helm release
|
||||
Write-Output "Deploying Helm release '$projectName'..."
|
||||
helm upgrade --install $projectName $chartPath -n $namespace `
|
||||
--set imagePullSecret.create=false `
|
||||
--set imagePullSecrets[0].name=cr-maksit-pull `
|
||||
|
||||
# Check deployment status
|
||||
Write-Output "Waiting for deployment rollout..."
|
||||
kubectl -n $namespace rollout status deployment/$projectName-reverseproxy
|
||||
|
||||
# Display service details
|
||||
Write-Output "Service information:"
|
||||
kubectl -n $namespace get svc $projectName-reverseproxy
|
||||
@ -5,16 +5,10 @@ VisualStudioVersion = 17.6.33815.320
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncrypt", "LetsEncrypt\LetsEncrypt.csproj", "{7DE431E5-889C-434E-AD02-9F89D7A0ED27}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncryptConsole", "LetsEncryptConsole\LetsEncryptConsole.csproj", "{2E4BE41E-E442-4CB8-824E-9888FFAA1BEF}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core", "Core\Core.csproj", "{27A58A5F-B52A-44F2-9639-84C6F02EA75D}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SSHProvider", "SSHProvider\SSHProvider.csproj", "{B6556305-D728-4368-A22C-93079C236808}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3374FDB1-C95E-4103-8E14-5BBF0BDC4E9D}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SSHProviderTests", "Tests\SSHSerivceTests\SSHProviderTests.csproj", "{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncryptServer", "LetsEncryptServer\LetsEncryptServer.csproj", "{B5F39E04-C2E3-49BF-82C2-9DEBAA949E3D}"
|
||||
EndProject
|
||||
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{0233E43F-435D-4309-B20C-ECD4BFBD2E63}"
|
||||
@ -35,22 +29,10 @@ Global
|
||||
{7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7DE431E5-889C-434E-AD02-9F89D7A0ED27}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2E4BE41E-E442-4CB8-824E-9888FFAA1BEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2E4BE41E-E442-4CB8-824E-9888FFAA1BEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2E4BE41E-E442-4CB8-824E-9888FFAA1BEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2E4BE41E-E442-4CB8-824E-9888FFAA1BEF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{27A58A5F-B52A-44F2-9639-84C6F02EA75D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{27A58A5F-B52A-44F2-9639-84C6F02EA75D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{27A58A5F-B52A-44F2-9639-84C6F02EA75D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{27A58A5F-B52A-44F2-9639-84C6F02EA75D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B6556305-D728-4368-A22C-93079C236808}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B6556305-D728-4368-A22C-93079C236808}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B6556305-D728-4368-A22C-93079C236808}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B6556305-D728-4368-A22C-93079C236808}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B5F39E04-C2E3-49BF-82C2-9DEBAA949E3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B5F39E04-C2E3-49BF-82C2-9DEBAA949E3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B5F39E04-C2E3-49BF-82C2-9DEBAA949E3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@ -75,9 +57,6 @@ Global
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{3937760A-FFB3-4A8C-ABD1-CDDCE1D977C4} = {3374FDB1-C95E-4103-8E14-5BBF0BDC4E9D}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {B78BD325-B2C1-456C-8EA8-42F9B89E0351}
|
||||
EndGlobalSection
|
||||
|
||||
27
src/LetsEncrypt/Entities/ContentType.cs
Normal file
27
src/LetsEncrypt/Entities/ContentType.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace MaksIT.LetsEncrypt.Entities
|
||||
{
|
||||
public enum ContentType
|
||||
{
|
||||
[Display(Name = "application/jose+json")]
|
||||
JoseJson,
|
||||
[Display(Name = "application/problem+json")]
|
||||
ProblemJson,
|
||||
[Display(Name = "application/pem-certificate-chain")]
|
||||
PemCertificateChain,
|
||||
[Display(Name = "application/json")]
|
||||
Json
|
||||
}
|
||||
|
||||
public static class ContentTypeExtensions
|
||||
{
|
||||
public static string GetDisplayName(this ContentType contentType)
|
||||
{
|
||||
var type = typeof(ContentType);
|
||||
var memInfo = type.GetMember(contentType.ToString());
|
||||
var attributes = memInfo[0].GetCustomAttributes(typeof(DisplayAttribute), false);
|
||||
return attributes.Length > 0 ? ((DisplayAttribute)attributes[0]).Name : contentType.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/LetsEncrypt/Entities/OrderStatus.cs
Normal file
27
src/LetsEncrypt/Entities/OrderStatus.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace MaksIT.LetsEncrypt.Entities
|
||||
{
|
||||
public enum OrderStatus
|
||||
{
|
||||
[Display(Name = "pending")]
|
||||
Pending,
|
||||
[Display(Name = "valid")]
|
||||
Valid,
|
||||
[Display(Name = "ready")]
|
||||
Ready,
|
||||
[Display(Name = "processing")]
|
||||
Processing
|
||||
}
|
||||
|
||||
public static class OrderStatusExtensions
|
||||
{
|
||||
public static string GetDisplayName(this OrderStatus status)
|
||||
{
|
||||
var type = typeof(OrderStatus);
|
||||
var memInfo = type.GetMember(status.ToString());
|
||||
var attributes = memInfo[0].GetCustomAttributes(typeof(DisplayAttribute), false);
|
||||
return attributes.Length > 0 ? ((DisplayAttribute)attributes[0]).Name : status.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8,12 +8,12 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DomainResult.Common" Version="3.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageReference Include="DomainResult.Common" Version="3.3.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.9" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -22,7 +22,6 @@ using MaksIT.LetsEncrypt.Models.Interfaces;
|
||||
using MaksIT.LetsEncrypt.Models.Requests;
|
||||
using MaksIT.LetsEncrypt.Entities.Jws;
|
||||
using MaksIT.LetsEncrypt.Entities.LetsEncrypt;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MaksIT.LetsEncrypt.Services;
|
||||
|
||||
@ -39,6 +38,10 @@ public interface ILetsEncryptService {
|
||||
}
|
||||
|
||||
public class LetsEncryptService : ILetsEncryptService {
|
||||
private const string DnsType = "dns";
|
||||
private const string DirectoryEndpoint = "directory";
|
||||
private const string ReplayNonceHeader = "Replay-Nonce";
|
||||
|
||||
private readonly ILogger<LetsEncryptService> _logger;
|
||||
private readonly LetsEncryptConfiguration _appSettings;
|
||||
private readonly HttpClient _httpClient;
|
||||
@ -57,40 +60,61 @@ public class LetsEncryptService : ILetsEncryptService {
|
||||
}
|
||||
|
||||
private State GetOrCreateState(Guid sessionId) {
|
||||
if (!_memoryCache.TryGetValue(sessionId, out State state)) {
|
||||
if (!_memoryCache.TryGetValue(sessionId, out State? state) || state == null) {
|
||||
state = new State();
|
||||
_memoryCache.Set(sessionId, state, TimeSpan.FromHours(1));
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
// Helper: Send ACME request and process response
|
||||
private async Task<SendResult<T>> SendAcmeRequest<T>(HttpRequestMessage request, State state, HttpMethod method) {
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
UpdateStateNonceIfNeeded(response, state, method);
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
HandleProblemResponseAsync(response, responseText);
|
||||
return ProcessResponseContent<T>(response, responseText);
|
||||
}
|
||||
|
||||
// Helper: Poll challenge status until valid or timeout
|
||||
private async Task<IDomainResult> PollChallengeStatus(Guid sessionId, AuthorizationChallengeChallenge challenge, State state) {
|
||||
if (challenge?.Url == null) return IDomainResult.Failed("Challenge URL is null");
|
||||
var start = DateTime.UtcNow;
|
||||
while (true) {
|
||||
var pollRequest = new HttpRequestMessage(HttpMethod.Post, challenge.Url);
|
||||
await HandleNonceAsync(sessionId, challenge.Url, state);
|
||||
var pollJson = EncodeMessage(true, null, state, new JwsHeader {
|
||||
Url = challenge.Url,
|
||||
Nonce = state.Nonce
|
||||
});
|
||||
PrepareRequestContent(pollRequest, pollJson, HttpMethod.Post);
|
||||
var pollResponse = await _httpClient.SendAsync(pollRequest);
|
||||
UpdateStateNonceIfNeeded(pollResponse, state, HttpMethod.Post);
|
||||
var pollResponseText = await pollResponse.Content.ReadAsStringAsync();
|
||||
HandleProblemResponseAsync(pollResponse, pollResponseText);
|
||||
var authChallenge = ProcessResponseContent<AuthorizationChallengeResponse>(pollResponse, pollResponseText);
|
||||
if (authChallenge.Result?.Status != "pending")
|
||||
return authChallenge.Result?.Status == "valid" ? IDomainResult.Success() : IDomainResult.Failed();
|
||||
if ((DateTime.UtcNow - start).Seconds > 120)
|
||||
return IDomainResult.Failed("Timeout");
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
}
|
||||
|
||||
#region ConfigureClient
|
||||
public async Task<IDomainResult> ConfigureClient(Guid sessionId, bool isStaging) {
|
||||
try {
|
||||
var state = GetOrCreateState(sessionId);
|
||||
|
||||
state.IsStaging = isStaging;
|
||||
// TODO: need to propagate from Configuration
|
||||
_httpClient.BaseAddress ??= new Uri(isStaging ? _appSettings.Staging : _appSettings.Production);
|
||||
|
||||
if (state.Directory == null) {
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, new Uri("directory", UriKind.Relative));
|
||||
await HandleNonceAsync(sessionId, new Uri("directory", UriKind.Relative), state);
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Get);
|
||||
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
HandleProblemResponseAsync(response, responseText);
|
||||
|
||||
var directory = ProcessResponseContent<AcmeDirectory>(response, responseText);
|
||||
|
||||
state.Directory = directory.Result;
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, new Uri(DirectoryEndpoint, UriKind.Relative));
|
||||
await HandleNonceAsync(sessionId, new Uri(DirectoryEndpoint, UriKind.Relative), state);
|
||||
var directory = await SendAcmeRequest<AcmeDirectory>(request, state, HttpMethod.Get);
|
||||
state.Directory = directory.Result ?? throw new InvalidOperationException("Directory response is null");
|
||||
}
|
||||
|
||||
return IDomainResult.Success();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
} catch (Exception ex) {
|
||||
const string message = "Let's Encrypt client unhandled exception";
|
||||
_logger.LogError(ex, message);
|
||||
return IDomainResult.CriticalDependencyError(message);
|
||||
@ -104,64 +128,42 @@ public class LetsEncryptService : ILetsEncryptService {
|
||||
_logger.LogError("Invalid sessionId");
|
||||
return IDomainResult.Failed();
|
||||
}
|
||||
|
||||
if (contacts == null || contacts.Length == 0) {
|
||||
_logger.LogError("Contacts are null or empty");
|
||||
return IDomainResult.Failed();
|
||||
}
|
||||
|
||||
var state = GetOrCreateState(sessionId);
|
||||
|
||||
if (state.Directory == null) {
|
||||
_logger.LogError("State directory is null");
|
||||
return IDomainResult.Failed();
|
||||
}
|
||||
|
||||
_logger.LogInformation($"Executing {nameof(Init)}...");
|
||||
|
||||
try {
|
||||
var accountKey = new RSACryptoServiceProvider(4096);
|
||||
|
||||
if (cache != null && cache.AccountKey != null) {
|
||||
state.Cache = cache;
|
||||
accountKey.ImportCspBlob(cache.AccountKey);
|
||||
state.JwsService = new JwsService(accountKey);
|
||||
state.JwsService.SetKeyId(cache.Location.ToString());
|
||||
}
|
||||
else {
|
||||
state.JwsService.SetKeyId(cache.Location?.ToString() ?? string.Empty);
|
||||
} else {
|
||||
state.JwsService = new JwsService(accountKey);
|
||||
|
||||
var letsEncryptOrder = new Account {
|
||||
TermsOfServiceAgreed = true,
|
||||
Contacts = contacts.Select(contact => $"mailto:{contact}").ToArray()
|
||||
};
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, state.Directory.NewAccount);
|
||||
|
||||
|
||||
await HandleNonceAsync(sessionId, state.Directory.NewAccount, state);
|
||||
|
||||
var json = EncodeMessage(false, letsEncryptOrder, state, new JwsHeader {
|
||||
Url = state.Directory.NewAccount,
|
||||
Nonce = state.Nonce
|
||||
});
|
||||
PrepareRequestContent(request, json, HttpMethod.Post);
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Post);
|
||||
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
HandleProblemResponseAsync(response, responseText);
|
||||
|
||||
var result = ProcessResponseContent<Account>(response, responseText);
|
||||
|
||||
state.JwsService.SetKeyId(result.Result.Location.ToString());
|
||||
|
||||
if (result.Result.Status != "valid") {
|
||||
_logger.LogError($"Account status is not valid, was: {result.Result.Status} \r\n {result.ResponseText}");
|
||||
var result = await SendAcmeRequest<Account>(request, state, HttpMethod.Post);
|
||||
state.JwsService.SetKeyId(result.Result?.Location?.ToString() ?? string.Empty);
|
||||
if (result.Result?.Status != "valid") {
|
||||
_logger.LogError($"Account status is not valid, was: {result.Result?.Status} \r\n {result.ResponseText}");
|
||||
return IDomainResult.Failed();
|
||||
}
|
||||
|
||||
state.Cache = new RegistrationCache {
|
||||
AccountId = accountId,
|
||||
Description = description,
|
||||
@ -169,14 +171,12 @@ public class LetsEncryptService : ILetsEncryptService {
|
||||
IsStaging = state.IsStaging,
|
||||
Location = result.Result.Location,
|
||||
AccountKey = accountKey.ExportCspBlob(true),
|
||||
Id = result.Result.Id,
|
||||
Id = result.Result.Id ?? string.Empty,
|
||||
Key = result.Result.Key
|
||||
};
|
||||
}
|
||||
|
||||
return IDomainResult.Success();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
} catch (Exception ex) {
|
||||
const string message = "Let's Encrypt client unhandled exception";
|
||||
_logger.LogError(ex, message);
|
||||
return IDomainResult.CriticalDependencyError(message);
|
||||
@ -186,10 +186,8 @@ public class LetsEncryptService : ILetsEncryptService {
|
||||
|
||||
public (RegistrationCache?, IDomainResult) GetRegistrationCache(Guid sessionId) {
|
||||
var state = GetOrCreateState(sessionId);
|
||||
|
||||
if(state?.Cache == null)
|
||||
return IDomainResult.Failed<RegistrationCache?>();
|
||||
|
||||
return IDomainResult.Success(state.Cache);
|
||||
}
|
||||
|
||||
@ -197,18 +195,13 @@ public class LetsEncryptService : ILetsEncryptService {
|
||||
public (string?, IDomainResult) GetTermsOfServiceUri(Guid sessionId) {
|
||||
try {
|
||||
var state = GetOrCreateState(sessionId);
|
||||
|
||||
_logger.LogInformation($"Executing {nameof(GetTermsOfServiceUri)}...");
|
||||
|
||||
if (state.Directory == null) {
|
||||
if (state.Directory?.Meta?.TermsOfService == null) {
|
||||
return IDomainResult.Failed<string?>();
|
||||
}
|
||||
|
||||
return IDomainResult.Success(state.Directory.Meta.TermsOfService);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
} catch (Exception ex) {
|
||||
var message = "Let's Encrypt client unhandled exception";
|
||||
|
||||
_logger.LogError(ex, message);
|
||||
return IDomainResult.CriticalDependencyError<string?>(message);
|
||||
}
|
||||
@ -219,107 +212,76 @@ public class LetsEncryptService : ILetsEncryptService {
|
||||
public async Task<(Dictionary<string, string>?, IDomainResult)> NewOrder(Guid sessionId, string[] hostnames, string challengeType) {
|
||||
try {
|
||||
var state = GetOrCreateState(sessionId);
|
||||
|
||||
_logger.LogInformation($"Executing {nameof(NewOrder)}...");
|
||||
|
||||
state.Challenges.Clear();
|
||||
|
||||
var letsEncryptOrder = new Order {
|
||||
Expires = DateTime.UtcNow.AddDays(2),
|
||||
Identifiers = hostnames.Select(hostname => new OrderIdentifier {
|
||||
Type = "dns",
|
||||
Value = hostname
|
||||
}).ToArray()
|
||||
Identifiers = hostnames?.Where(h => h != null).Select(hostname => new OrderIdentifier {
|
||||
Type = DnsType,
|
||||
Value = hostname ?? string.Empty
|
||||
}).ToArray() ?? Array.Empty<OrderIdentifier>()
|
||||
};
|
||||
|
||||
if (state.Directory == null || state.Directory.NewOrder == null)
|
||||
return (null, IDomainResult.Failed());
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, state.Directory.NewOrder);
|
||||
await HandleNonceAsync(sessionId, state.Directory.NewOrder, state);
|
||||
|
||||
var json = EncodeMessage(false, letsEncryptOrder, state, new JwsHeader {
|
||||
Url = state.Directory.NewOrder,
|
||||
Nonce = state.Nonce
|
||||
});
|
||||
PrepareRequestContent(request, json, HttpMethod.Post);
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Post);
|
||||
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
HandleProblemResponseAsync(response, responseText);
|
||||
|
||||
var order = ProcessResponseContent<Order>(response, responseText);
|
||||
|
||||
if (order.Result.Status == "ready")
|
||||
return IDomainResult.Success(new Dictionary<string, string>());
|
||||
|
||||
if (order.Result.Status != "pending") {
|
||||
_logger.LogError($"Created new order and expected status 'pending', but got: {order.Result.Status} \r\n {order.Result}");
|
||||
return IDomainResult.Failed<Dictionary<string, string>?>();
|
||||
var order = await SendAcmeRequest<Order>(request, state, HttpMethod.Post);
|
||||
if (StatusEquals(order.Result?.Status, OrderStatus.Ready))
|
||||
return (new Dictionary<string, string>(), IDomainResult.Success());
|
||||
if (!StatusEquals(order.Result?.Status, OrderStatus.Pending)) {
|
||||
_logger.LogError($"Created new order and expected status '{OrderStatus.Pending.GetDisplayName()}', but got: {order.Result?.Status} \r\n {order.Result}");
|
||||
return (null, IDomainResult.Failed());
|
||||
}
|
||||
|
||||
state.CurrentOrder = order.Result;
|
||||
|
||||
var results = new Dictionary<string, string>();
|
||||
foreach (var item in state.CurrentOrder.Authorizations) {
|
||||
|
||||
foreach (var item in state.CurrentOrder?.Authorizations ?? Array.Empty<Uri>()) {
|
||||
if (item == null) continue;
|
||||
request = new HttpRequestMessage(HttpMethod.Post, item);
|
||||
await HandleNonceAsync(sessionId, item, state);
|
||||
|
||||
json = EncodeMessage(true, null, state, new JwsHeader {
|
||||
Url = item,
|
||||
Nonce = state.Nonce
|
||||
});
|
||||
PrepareRequestContent(request, json, HttpMethod.Post);
|
||||
|
||||
|
||||
|
||||
response = await _httpClient.SendAsync(request);
|
||||
UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Post);
|
||||
|
||||
responseText = await response.Content.ReadAsStringAsync();
|
||||
HandleProblemResponseAsync(response, responseText);
|
||||
|
||||
var challengeResponse = ProcessResponseContent<AuthorizationChallengeResponse>(response, responseText);
|
||||
|
||||
|
||||
if (challengeResponse.Result.Status == "valid")
|
||||
var challengeResponse = await SendAcmeRequest<AuthorizationChallengeResponse>(request, state, HttpMethod.Post);
|
||||
if (StatusEquals(challengeResponse.Result?.Status, OrderStatus.Valid))
|
||||
continue;
|
||||
|
||||
if (challengeResponse.Result.Status != "pending") {
|
||||
_logger.LogError($"Expected authorization status 'pending', but got: {state.CurrentOrder.Status} \r\n {challengeResponse.ResponseText}");
|
||||
return IDomainResult.Failed<Dictionary<string, string>?>();
|
||||
if (!StatusEquals(challengeResponse.Result?.Status, OrderStatus.Pending)) {
|
||||
_logger.LogError($"Expected authorization status '{OrderStatus.Pending.GetDisplayName()}', but got: {state.CurrentOrder?.Status} \r\n {challengeResponse.ResponseText}");
|
||||
return (null, IDomainResult.Failed());
|
||||
}
|
||||
var challenge = challengeResponse.Result?.Challenges?.FirstOrDefault(x => x?.Type == challengeType);
|
||||
if (challenge == null || challenge.Token == null) {
|
||||
_logger.LogError("Challenge or token is null");
|
||||
return (null, IDomainResult.Failed());
|
||||
}
|
||||
|
||||
var challenge = challengeResponse.Result.Challenges.First(x => x.Type == challengeType);
|
||||
state.Challenges.Add(challenge);
|
||||
state.Cache.ChallengeType = challengeType;
|
||||
|
||||
var keyToken = state.JwsService.GetKeyAuthorization(challenge.Token);
|
||||
|
||||
if (state.Cache != null) state.Cache.ChallengeType = challengeType;
|
||||
var keyToken = state.JwsService != null ? state.JwsService.GetKeyAuthorization(challenge.Token) : string.Empty;
|
||||
switch (challengeType) {
|
||||
case "dns-01":
|
||||
using (var sha256 = SHA256.Create()) {
|
||||
var dnsToken = state.JwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken)));
|
||||
results[challengeResponse.Result.Identifier.Value] = dnsToken;
|
||||
var dnsToken = state.JwsService != null ? state.JwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken ?? string.Empty))) : string.Empty;
|
||||
results[challengeResponse.Result?.Identifier?.Value ?? string.Empty] = dnsToken;
|
||||
}
|
||||
break;
|
||||
|
||||
case "http-01":
|
||||
results[challengeResponse.Result.Identifier.Value] = keyToken;
|
||||
results[challengeResponse.Result?.Identifier?.Value ?? string.Empty] = keyToken ?? string.Empty;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
return IDomainResult.Success(results);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return (results, IDomainResult.Success());
|
||||
} catch (Exception ex) {
|
||||
var message = "Let's Encrypt client unhandled exception";
|
||||
|
||||
_logger.LogError(ex, message);
|
||||
return IDomainResult.CriticalDependencyError<Dictionary<string, string>?>(message);
|
||||
return (null, IDomainResult.CriticalDependencyError(message));
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
@ -328,68 +290,31 @@ public class LetsEncryptService : ILetsEncryptService {
|
||||
public async Task<IDomainResult> CompleteChallenges(Guid sessionId) {
|
||||
try {
|
||||
var state = GetOrCreateState(sessionId);
|
||||
|
||||
_logger.LogInformation($"Executing {nameof(CompleteChallenges)}...");
|
||||
|
||||
if (state.CurrentOrder?.Identifiers == null) {
|
||||
return IDomainResult.Failed("Current order identifiers are null");
|
||||
}
|
||||
|
||||
for (var index = 0; index < state.Challenges.Count; index++) {
|
||||
var challenge = state.Challenges[index];
|
||||
var start = DateTime.UtcNow;
|
||||
|
||||
while (true) {
|
||||
var authorizeChallenge = new AuthorizeChallenge();
|
||||
|
||||
switch (challenge.Type) {
|
||||
case "dns-01":
|
||||
authorizeChallenge.KeyAuthorization = state.JwsService.GetKeyAuthorization(challenge.Token);
|
||||
break;
|
||||
|
||||
case "http-01":
|
||||
break;
|
||||
if (challenge?.Url == null) {
|
||||
_logger.LogError("Challenge URL is null");
|
||||
return IDomainResult.Failed();
|
||||
}
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, challenge.Url);
|
||||
await HandleNonceAsync(sessionId, challenge.Url, state);
|
||||
|
||||
var json = EncodeMessage(false, "{}", state, new JwsHeader {
|
||||
Url = challenge.Url,
|
||||
Nonce = state.Nonce
|
||||
});
|
||||
PrepareRequestContent(request, json, HttpMethod.Post);
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Post);
|
||||
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
HandleProblemResponseAsync(response, responseText);
|
||||
|
||||
var authChallenge = ProcessResponseContent<AuthorizationChallengeResponse>(response, responseText);
|
||||
//return IDomainResult.Success(result);
|
||||
|
||||
|
||||
if (authChallenge.Result.Status == "valid")
|
||||
break;
|
||||
|
||||
if (authChallenge.Result.Status != "pending") {
|
||||
_logger.LogError($"Challenge failed with status {authChallenge.Result.Status} \r\n {authChallenge.ResponseText}");
|
||||
return IDomainResult.Failed();
|
||||
var authChallenge = await SendAcmeRequest<AuthorizationChallengeResponse>(request, state, HttpMethod.Post);
|
||||
var result = await PollChallengeStatus(sessionId, challenge, state);
|
||||
if (!result.IsSuccess)
|
||||
return result;
|
||||
}
|
||||
|
||||
await Task.Delay(1000);
|
||||
|
||||
if ((DateTime.UtcNow - start).Seconds > 120)
|
||||
return IDomainResult.Failed("Timeout");
|
||||
}
|
||||
}
|
||||
|
||||
return IDomainResult.Success();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
} catch (Exception ex) {
|
||||
var message = "Let's Encrypt client unhandled exception";
|
||||
|
||||
_logger.LogError(ex, message);
|
||||
return IDomainResult.CriticalDependencyError(message);
|
||||
}
|
||||
@ -400,40 +325,26 @@ public class LetsEncryptService : ILetsEncryptService {
|
||||
public async Task<IDomainResult> GetOrder(Guid sessionId, string[] hostnames) {
|
||||
try {
|
||||
_logger.LogInformation($"Executing {nameof(GetOrder)}");
|
||||
|
||||
var state = GetOrCreateState(sessionId);
|
||||
|
||||
var letsEncryptOrder = new Order {
|
||||
Expires = DateTime.UtcNow.AddDays(2),
|
||||
Identifiers = hostnames.Select(hostname => new OrderIdentifier {
|
||||
Identifiers = hostnames?.Where(h => h != null).Select(hostname => new OrderIdentifier {
|
||||
Type = "dns",
|
||||
Value = hostname
|
||||
}).ToArray()
|
||||
Value = hostname!
|
||||
}).ToArray() ?? Array.Empty<OrderIdentifier>()
|
||||
};
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, state.Directory.NewOrder);
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, state.Directory!.NewOrder);
|
||||
await HandleNonceAsync(sessionId, state.Directory.NewOrder, state);
|
||||
|
||||
var json = EncodeMessage(false, letsEncryptOrder, state, new JwsHeader {
|
||||
Url = state.Directory.NewOrder,
|
||||
Nonce = state.Nonce
|
||||
});
|
||||
PrepareRequestContent(request, json, HttpMethod.Post);
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Post);
|
||||
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
HandleProblemResponseAsync(response, responseText);
|
||||
|
||||
var order = ProcessResponseContent<Order>(response, responseText);
|
||||
var order = await SendAcmeRequest<Order>(request, state, HttpMethod.Post);
|
||||
state.CurrentOrder = order.Result;
|
||||
|
||||
return IDomainResult.Success();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
} catch (Exception ex) {
|
||||
var message = "Let's Encrypt client unhandled exception";
|
||||
|
||||
_logger.LogError(ex, message);
|
||||
return IDomainResult.CriticalDependencyError(message);
|
||||
}
|
||||
@ -444,136 +355,89 @@ public class LetsEncryptService : ILetsEncryptService {
|
||||
public async Task<IDomainResult> GetCertificate(Guid sessionId, string subject) {
|
||||
try {
|
||||
var state = GetOrCreateState(sessionId);
|
||||
|
||||
_logger.LogInformation($"Executing {nameof(GetCertificate)}...");
|
||||
|
||||
if (state.CurrentOrder == null) {
|
||||
if (state.CurrentOrder?.Identifiers == null) {
|
||||
return IDomainResult.Failed();
|
||||
}
|
||||
|
||||
var key = new RSACryptoServiceProvider(4096);
|
||||
var csr = new CertificateRequest("CN=" + subject,
|
||||
key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
|
||||
var san = new SubjectAlternativeNameBuilder();
|
||||
foreach (var host in state.CurrentOrder.Identifiers)
|
||||
foreach (var host in state.CurrentOrder.Identifiers) {
|
||||
if (host?.Value != null)
|
||||
san.AddDnsName(host.Value);
|
||||
|
||||
}
|
||||
csr.CertificateExtensions.Add(san.Build());
|
||||
|
||||
var letsEncryptOrder = new FinalizeRequest {
|
||||
Csr = state.JwsService.Base64UrlEncoded(csr.CreateSigningRequest())
|
||||
Csr = state.JwsService!.Base64UrlEncoded(csr.CreateSigningRequest())
|
||||
};
|
||||
|
||||
Uri? certificateUrl = default;
|
||||
|
||||
var start = DateTime.UtcNow;
|
||||
|
||||
while (certificateUrl == null) {
|
||||
// https://community.letsencrypt.org/t/breaking-changes-in-asynchronous-order-finalization-api/195882
|
||||
await GetOrder(sessionId, state.CurrentOrder.Identifiers.Select(x => x.Value).ToArray());
|
||||
|
||||
if (state.CurrentOrder.Status == "ready") {
|
||||
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, state.CurrentOrder.Finalize);
|
||||
await HandleNonceAsync(sessionId, state.CurrentOrder.Finalize, state);
|
||||
|
||||
var hostnames = state.CurrentOrder.Identifiers?.Select(x => x?.Value).Where(x => x != null).Cast<string>().ToArray() ?? Array.Empty<string>();
|
||||
await GetOrder(sessionId, hostnames);
|
||||
var status = state.CurrentOrder?.Status;
|
||||
if (StatusEquals(status, OrderStatus.Ready)) {
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, state.CurrentOrder.Finalize!);
|
||||
await HandleNonceAsync(sessionId, state.CurrentOrder.Finalize!, state);
|
||||
var json = EncodeMessage(false, letsEncryptOrder, state, new JwsHeader {
|
||||
Url = state.CurrentOrder.Finalize,
|
||||
Nonce = state.Nonce
|
||||
});
|
||||
PrepareRequestContent(request, json, HttpMethod.Post);
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Post);
|
||||
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
HandleProblemResponseAsync(response, responseText);
|
||||
|
||||
var order = ProcessResponseContent<Order>(response, responseText);
|
||||
|
||||
|
||||
|
||||
if (order.Result.Status == "processing") {
|
||||
|
||||
request = new HttpRequestMessage(HttpMethod.Post, state.CurrentOrder.Location);
|
||||
await HandleNonceAsync(sessionId, state.CurrentOrder.Location, state);
|
||||
|
||||
var order = await SendAcmeRequest<Order>(request, state, HttpMethod.Post);
|
||||
if (StatusEquals(order.Result?.Status, OrderStatus.Processing)) {
|
||||
request = new HttpRequestMessage(HttpMethod.Post, state.CurrentOrder.Location!);
|
||||
await HandleNonceAsync(sessionId, state.CurrentOrder.Location!, state);
|
||||
json = EncodeMessage(true, null, state, new JwsHeader {
|
||||
Url = state.CurrentOrder.Location,
|
||||
Nonce = state.Nonce
|
||||
});
|
||||
PrepareRequestContent(request, json, HttpMethod.Post);
|
||||
|
||||
response = await _httpClient.SendAsync(request);
|
||||
UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Post);
|
||||
|
||||
responseText = await response.Content.ReadAsStringAsync();
|
||||
HandleProblemResponseAsync(response, responseText);
|
||||
|
||||
order = ProcessResponseContent<Order>(response, responseText);
|
||||
order = await SendAcmeRequest<Order>(request, state, HttpMethod.Post);
|
||||
}
|
||||
|
||||
if (order.Result.Status == "valid") {
|
||||
if (StatusEquals(order.Result?.Status, OrderStatus.Valid)) {
|
||||
certificateUrl = order.Result.Certificate;
|
||||
}
|
||||
} else if (StatusEquals(status, OrderStatus.Valid)) {
|
||||
certificateUrl = state.CurrentOrder.Certificate;
|
||||
break;
|
||||
}
|
||||
|
||||
if ((DateTime.UtcNow - start).Seconds > 120)
|
||||
throw new TimeoutException();
|
||||
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
|
||||
|
||||
|
||||
var finalRequest = new HttpRequestMessage(HttpMethod.Post, certificateUrl);
|
||||
await HandleNonceAsync(sessionId, certificateUrl, state);
|
||||
|
||||
var finalRequest = new HttpRequestMessage(HttpMethod.Post, certificateUrl!);
|
||||
await HandleNonceAsync(sessionId, certificateUrl!, state);
|
||||
var finalJson = EncodeMessage(true, null, state, new JwsHeader {
|
||||
Url = certificateUrl,
|
||||
Nonce = state.Nonce
|
||||
});
|
||||
PrepareRequestContent(finalRequest, finalJson, HttpMethod.Post);
|
||||
|
||||
var finalResponse = await _httpClient.SendAsync(finalRequest);
|
||||
UpdateStateNonceIfNeededAsync(finalResponse, state, HttpMethod.Post);
|
||||
|
||||
var finalResponseText = await finalResponse.Content.ReadAsStringAsync();
|
||||
HandleProblemResponseAsync(finalResponse, finalResponseText);
|
||||
|
||||
var pem = ProcessResponseContent<string>(finalResponse, finalResponseText);
|
||||
|
||||
|
||||
var pem = await SendAcmeRequest<string>(finalRequest, state, HttpMethod.Post);
|
||||
if (state.Cache == null) {
|
||||
_logger.LogError($"{nameof(state.Cache)} is null");
|
||||
return IDomainResult.Failed();
|
||||
}
|
||||
|
||||
state.Cache.CachedCerts ??= new Dictionary<string, CertificateCache>();
|
||||
state.Cache.CachedCerts[subject] = new CertificateCache {
|
||||
Cert = pem.Result,
|
||||
Cert = pem.Result ?? string.Empty,
|
||||
Private = key.ExportCspBlob(true),
|
||||
PrivatePem = key.ExportRSAPrivateKeyPem()
|
||||
};
|
||||
|
||||
var cert = new X509Certificate2(Encoding.UTF8.GetBytes(pem.Result));
|
||||
|
||||
|
||||
|
||||
return IDomainResult.Success();
|
||||
var certPem = pem.Result ?? string.Empty;
|
||||
if (!string.IsNullOrEmpty(certPem)) {
|
||||
var cert = new X509Certificate2(Encoding.UTF8.GetBytes(certPem));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return IDomainResult.Success();
|
||||
} catch (Exception ex) {
|
||||
var message = "Let's Encrypt client unhandled exception";
|
||||
|
||||
_logger.LogError(ex, message);
|
||||
return IDomainResult.CriticalDependencyError(message);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
public Task<IDomainResult> KeyChange(Guid sessionId) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
@ -581,58 +445,44 @@ public class LetsEncryptService : ILetsEncryptService {
|
||||
public async Task<IDomainResult> RevokeCertificate(Guid sessionId, string subject, RevokeReason reason) {
|
||||
try {
|
||||
var state = GetOrCreateState(sessionId);
|
||||
|
||||
_logger.LogInformation($"Executing {nameof(RevokeCertificate)}...");
|
||||
|
||||
if (state.Cache == null || state.Cache.CachedCerts == null || !state.Cache.CachedCerts.TryGetValue(subject, out var certificateCache)) {
|
||||
if (state.Cache?.CachedCerts == null || !state.Cache.CachedCerts.TryGetValue(subject, out var certificateCache) || certificateCache == null) {
|
||||
_logger.LogError("Certificate not found in cache");
|
||||
return IDomainResult.Failed("Certificate not found");
|
||||
}
|
||||
|
||||
// Load the certificate from PEM format and convert it to DER format
|
||||
var certificate = new X509Certificate2(Encoding.UTF8.GetBytes(certificateCache.Cert));
|
||||
var certPem = certificateCache.Cert ?? string.Empty;
|
||||
if (string.IsNullOrEmpty(certPem)) {
|
||||
_logger.LogError("Certificate PEM is null or empty");
|
||||
return IDomainResult.Failed("Certificate PEM is null or empty");
|
||||
}
|
||||
var certificate = new X509Certificate2(Encoding.UTF8.GetBytes(certPem));
|
||||
var derEncodedCert = certificate.Export(X509ContentType.Cert);
|
||||
var base64UrlEncodedCert = state.JwsService.Base64UrlEncoded(derEncodedCert);
|
||||
|
||||
|
||||
var base64UrlEncodedCert = state.JwsService!.Base64UrlEncoded(derEncodedCert);
|
||||
var revokeRequest = new RevokeRequest {
|
||||
Certificate = base64UrlEncodedCert,
|
||||
Reason = (int)reason
|
||||
};
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, state.Directory.RevokeCert);
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, state.Directory!.RevokeCert);
|
||||
await HandleNonceAsync(sessionId, state.Directory.RevokeCert, state);
|
||||
|
||||
var jwsHeader = new JwsHeader {
|
||||
Url = state.Directory.RevokeCert,
|
||||
Nonce = state.Nonce
|
||||
};
|
||||
|
||||
var json = state.JwsService.Encode(revokeRequest, jwsHeader).ToJson();
|
||||
|
||||
request.Content = new StringContent(json);
|
||||
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/jose+json");
|
||||
|
||||
request.Content.Headers.ContentType = new MediaTypeHeaderValue(GetContentType(ContentType.JoseJson));
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
UpdateStateNonceIfNeededAsync(response, state, HttpMethod.Post);
|
||||
|
||||
UpdateStateNonceIfNeeded(response, state, HttpMethod.Post);
|
||||
var responseText = await response.Content.ReadAsStringAsync();
|
||||
if (response.Content.Headers.ContentType?.MediaType == "application/problem+json") {
|
||||
if (response.Content.Headers.ContentType?.MediaType == GetContentType(ContentType.ProblemJson)) {
|
||||
var erroObj = responseText.ToObject<Problem>();
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
IDomainResult.CriticalDependencyError(responseText);
|
||||
|
||||
|
||||
// Remove the certificate from the cache after successful revocation
|
||||
state.Cache.CachedCerts.Remove(subject);
|
||||
|
||||
_logger.LogInformation("Certificate revoked successfully");
|
||||
|
||||
return IDomainResult.Success();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
} catch (Exception ex) {
|
||||
var message = "Let's Encrypt client unhandled exception";
|
||||
_logger.LogError(ex, message);
|
||||
return IDomainResult.CriticalDependencyError($"{message}: {ex.Message}");
|
||||
@ -641,6 +491,7 @@ public class LetsEncryptService : ILetsEncryptService {
|
||||
|
||||
#region SendAsync
|
||||
private async Task HandleNonceAsync(Guid sessionId, Uri uri, State state) {
|
||||
if (uri == null) throw new ArgumentNullException(nameof(uri));
|
||||
if (uri.OriginalString != "directory") {
|
||||
var (nonce, newNonceResult) = await NewNonce(sessionId);
|
||||
if (!newNonceResult.IsSuccess || nonce == null) {
|
||||
@ -656,63 +507,63 @@ public class LetsEncryptService : ILetsEncryptService {
|
||||
private async Task<(string?, IDomainResult)> NewNonce(Guid sessionId) {
|
||||
try {
|
||||
var state = GetOrCreateState(sessionId);
|
||||
|
||||
_logger.LogInformation($"Executing {nameof(NewNonce)}...");
|
||||
|
||||
if (state.Directory == null)
|
||||
IDomainResult.Failed();
|
||||
|
||||
if (state.Directory?.NewNonce == null)
|
||||
return (null, IDomainResult.Failed());
|
||||
var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, state.Directory.NewNonce));
|
||||
return IDomainResult.Success(result.Headers.GetValues("Replay-Nonce").First());
|
||||
var nonce = result.Headers.GetValues("Replay-Nonce").FirstOrDefault();
|
||||
return (nonce, IDomainResult.Success());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
var message = "Let's Encrypt client unhandled exception";
|
||||
|
||||
_logger.LogError(ex, message);
|
||||
return IDomainResult.CriticalDependencyError<string?>(message);
|
||||
return (null, IDomainResult.CriticalDependencyError(message));
|
||||
}
|
||||
}
|
||||
|
||||
private string EncodeMessage(bool isPostAsGet, object? requestModel, State state, JwsHeader jwsHeader) {
|
||||
return isPostAsGet
|
||||
? state.JwsService.Encode(jwsHeader).ToJson()
|
||||
: state.JwsService.Encode(requestModel, jwsHeader).ToJson();
|
||||
? state.JwsService!.Encode(jwsHeader).ToJson()
|
||||
: state.JwsService!.Encode(requestModel, jwsHeader).ToJson();
|
||||
}
|
||||
|
||||
private static string GetContentType(ContentType type) => type.GetDisplayName();
|
||||
|
||||
private void PrepareRequestContent(HttpRequestMessage request, string json, HttpMethod method) {
|
||||
request.Content = new StringContent(json);
|
||||
var contentType = method == HttpMethod.Post ? "application/jose+json" : "application/json";
|
||||
request.Content = new StringContent(json ?? string.Empty);
|
||||
var contentType = method == HttpMethod.Post ? GetContentType(ContentType.JoseJson) : GetContentType(ContentType.Json);
|
||||
request.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
|
||||
}
|
||||
|
||||
private void UpdateStateNonceIfNeededAsync(HttpResponseMessage response, State state, HttpMethod method) {
|
||||
if (method == HttpMethod.Post && response.Headers.Contains("Replay-Nonce")) {
|
||||
state.Nonce = response.Headers.GetValues("Replay-Nonce").First();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleProblemResponseAsync(HttpResponseMessage response, string responseText) {
|
||||
if (response.Content.Headers.ContentType?.MediaType == "application/problem+json") {
|
||||
if (response.Content.Headers.ContentType?.MediaType == GetContentType(ContentType.ProblemJson)) {
|
||||
throw new LetsEncrytException(responseText.ToObject<Problem>(), response);
|
||||
}
|
||||
}
|
||||
|
||||
private SendResult<TResult> ProcessResponseContent<TResult>(HttpResponseMessage response, string responseText) {
|
||||
if (response.Content.Headers.ContentType?.MediaType == "application/pem-certificate-chain" && typeof(TResult) == typeof(string)) {
|
||||
if (response.Content.Headers.ContentType?.MediaType == GetContentType(ContentType.PemCertificateChain) && typeof(TResult) == typeof(string)) {
|
||||
return new SendResult<TResult> {
|
||||
Result = (TResult)(object)responseText
|
||||
};
|
||||
}
|
||||
|
||||
var responseContent = responseText.ToObject<TResult>();
|
||||
if (responseContent is IHasLocation ihl && response.Headers.Location != null) {
|
||||
ihl.Location = response.Headers.Location;
|
||||
}
|
||||
|
||||
return new SendResult<TResult> {
|
||||
Result = responseContent,
|
||||
ResponseText = responseText
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
|
||||
private void UpdateStateNonceIfNeeded(HttpResponseMessage response, State state, HttpMethod method) {
|
||||
if (method == HttpMethod.Post && response.Headers.Contains(ReplayNonceHeader)) {
|
||||
state.Nonce = response.Headers.GetValues(ReplayNonceHeader).FirstOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for status comparison
|
||||
private static bool StatusEquals(string? status, OrderStatus expected) => status == expected.GetDisplayName();
|
||||
}
|
||||
|
||||
@ -9,10 +9,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DomainResult" Version="3.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
<PackageReference Include="DomainResult" Version="3.3.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
7
src/Release-DockerImage.bat
Normal file
7
src/Release-DockerImage.bat
Normal file
@ -0,0 +1,7 @@
|
||||
@echo off
|
||||
|
||||
REM Change directory to the location of the script
|
||||
cd /d %~dp0
|
||||
|
||||
REM Invoke the PowerShell script (Release-NuGetPackage.ps1) in the same directory
|
||||
powershell -ExecutionPolicy Bypass -File "%~dp0Release-DockerImage.ps1"
|
||||
59
src/Release-DockerImage.ps1
Normal file
59
src/Release-DockerImage.ps1
Normal file
@ -0,0 +1,59 @@
|
||||
# Set variables
|
||||
$projectName = "certs-ui"
|
||||
$harborUrl = "cr.maks-it.com" # e.g., "harbor.yourdomain.com"
|
||||
$tag = "latest" # Customize the tag as needed
|
||||
|
||||
# Retrieve and decode username:password from environment variable (Base64 encoded)
|
||||
try {
|
||||
$decoded = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Env:CR_MAKS_IT))
|
||||
} catch {
|
||||
throw "Failed to decode CR_MAKS_IT as Base64. Ensure it's base64('username:password'). Error: $_"
|
||||
}
|
||||
|
||||
# Split decoded credentials
|
||||
$creds = $decoded -split ':', 2
|
||||
if ($creds.Count -ne 2) {
|
||||
throw "Invalid decoded CR_MAKS_IT format. Expected 'username:password'."
|
||||
}
|
||||
|
||||
$harborUsername = $creds[0]
|
||||
$harborPassword = $creds[1]
|
||||
|
||||
# Authenticate with Harbor
|
||||
Write-Output "Logging into $harborUrl as $harborUsername..."
|
||||
$loginResult = $harborPassword | docker login $harborUrl -u $harborUsername --password-stdin 2>&1
|
||||
if ($LASTEXITCODE -ne 0 -or ($loginResult -notmatch "Login Succeeded")) {
|
||||
throw "Docker login failed for $harborUrl.`n$loginResult"
|
||||
}
|
||||
|
||||
# List of services to build and push with the current context
|
||||
$services = @{
|
||||
"reverseproxy" = "ReverseProxy/Dockerfile"
|
||||
"server" = "LetsEncryptServer/Dockerfile"
|
||||
"client" = "ClientApp/Dockerfile"
|
||||
}
|
||||
|
||||
$contextPath = "."
|
||||
|
||||
foreach ($service in $services.Keys) {
|
||||
$dockerfilePath = $services[$service]
|
||||
$imageName = "$harborUrl/$projectName/${service}:${tag}"
|
||||
|
||||
# Build the Docker image
|
||||
Write-Output "Building image $imageName from $dockerfilePath..."
|
||||
docker build -t $imageName -f $dockerfilePath $contextPath
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Docker build failed for $imageName"
|
||||
}
|
||||
|
||||
# Push the Docker image
|
||||
Write-Output "Pushing image $imageName..."
|
||||
docker push $imageName
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Docker push failed for $imageName"
|
||||
}
|
||||
}
|
||||
|
||||
# Logout after pushing images
|
||||
docker logout $harborUrl | Out-Null
|
||||
Write-Output "Completed successfully."
|
||||
14
src/ReleaseAndDeploy.bat
Normal file
14
src/ReleaseAndDeploy.bat
Normal file
@ -0,0 +1,14 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
REM Get the directory of the current script
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
|
||||
REM Run the first batch file
|
||||
call "%SCRIPT_DIR%Release-DockerImage.bat"
|
||||
|
||||
REM Run the second batch file
|
||||
call "%SCRIPT_DIR%Deploy-Helm.bat"
|
||||
|
||||
echo All scripts completed.
|
||||
pause
|
||||
@ -8,9 +8,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Extensions" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.6" />
|
||||
<PackageReference Include="Yarp.ReverseProxy" Version="2.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Extensions" Version="9.0.9" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -2,43 +2,31 @@
|
||||
"ReverseProxy": {
|
||||
"Routes": {
|
||||
"well-known-acme-challenge-route": {
|
||||
"Match": {
|
||||
"Path": "/.well-known/acme-challenge/{**catch-all}"
|
||||
},
|
||||
"Match": { "Path": "/.well-known/acme-challenge/{**catch-all}" },
|
||||
"ClusterId": "letsencrypt-server"
|
||||
},
|
||||
"swagger-route": {
|
||||
"Match": {
|
||||
"Path": "/swagger/{**catch-all}"
|
||||
},
|
||||
"Match": { "Path": "/swagger/{**catch-all}" },
|
||||
"ClusterId": "letsencrypt-server"
|
||||
},
|
||||
"api-route": {
|
||||
"Match": {
|
||||
"Path": "/api/{**catch-all}"
|
||||
},
|
||||
"Match": { "Path": "/api/{**catch-all}" },
|
||||
"ClusterId": "letsencrypt-server"
|
||||
},
|
||||
"default-route": {
|
||||
"Match": {
|
||||
"Path": "{**catch-all}"
|
||||
},
|
||||
"Match": { "Path": "{**catch-all}" },
|
||||
"ClusterId": "letsencrypt-app"
|
||||
}
|
||||
},
|
||||
"Clusters": {
|
||||
"letsencrypt-server": {
|
||||
"Destinations": {
|
||||
"destination1": {
|
||||
"Address": "http://letsencrypt-server:5000/"
|
||||
}
|
||||
"d1": { "Address": "http://certs-ui-server:5000/" }
|
||||
}
|
||||
},
|
||||
"letsencrypt-app": {
|
||||
"Destinations": {
|
||||
"destination1": {
|
||||
"Address": "http://letsencrypt-app:3000/"
|
||||
}
|
||||
"d1": { "Address": "http://certs-ui-client:3000/" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
|
||||
namespace MaksIT.SSHProvider;
|
||||
|
||||
public class Configuration {
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DomainResult.Common" Version="3.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="SSH.NET" Version="2024.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -1,168 +0,0 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using DomainResults.Common;
|
||||
|
||||
using Renci.SshNet;
|
||||
using Renci.SshNet.Common;
|
||||
|
||||
namespace MaksIT.SSHProvider {
|
||||
|
||||
public interface ISSHService : IDisposable {
|
||||
IDomainResult Upload(string workingdirectory, string fileName, byte[] bytes);
|
||||
|
||||
IDomainResult ListDir(string workingdirectory);
|
||||
|
||||
IDomainResult Download();
|
||||
}
|
||||
|
||||
public class SSHService : ISSHService {
|
||||
|
||||
public readonly ILogger _logger;
|
||||
|
||||
public readonly SshClient _sshClient;
|
||||
public readonly SftpClient _sftpClient;
|
||||
|
||||
public SSHService(
|
||||
ILogger logger,
|
||||
string host,
|
||||
int port,
|
||||
string username,
|
||||
string password
|
||||
) {
|
||||
|
||||
if(string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
throw new ArgumentNullException($"{nameof(username)} or {nameof(password)} is null, empty or white space");
|
||||
|
||||
_logger = logger;
|
||||
_sshClient = new SshClient(host, port, username, password);
|
||||
_sftpClient = new SftpClient(host, port, username, password);
|
||||
}
|
||||
|
||||
|
||||
public SSHService(
|
||||
ILogger logger,
|
||||
string host,
|
||||
int port,
|
||||
string username,
|
||||
string [] privateKeys
|
||||
) {
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username) || privateKeys.Any(x => string.IsNullOrWhiteSpace(x)))
|
||||
throw new ArgumentNullException($"{nameof(username)} or {nameof(privateKeys)} contains key which is null, empty or white space");
|
||||
|
||||
_logger = logger;
|
||||
|
||||
var privateKeyFiles = new List<PrivateKeyFile>();
|
||||
foreach (var privateKey in privateKeys) {
|
||||
using (var ms = new MemoryStream(Encoding.ASCII.GetBytes(privateKey))) {
|
||||
privateKeyFiles.Add(new PrivateKeyFile(ms));
|
||||
}
|
||||
}
|
||||
|
||||
_sshClient = new SshClient(host, port, username, privateKeyFiles.ToArray());
|
||||
_sftpClient = new SftpClient(host, port, username, privateKeyFiles.ToArray());
|
||||
}
|
||||
|
||||
public IDomainResult Connect() {
|
||||
try {
|
||||
_sshClient.Connect();
|
||||
_sftpClient.Connect();
|
||||
|
||||
return IDomainResult.Success();
|
||||
}
|
||||
catch (Exception ex){
|
||||
_logger.LogError(ex, "SSH Service unhandled exeption");
|
||||
return IDomainResult.CriticalDependencyError();
|
||||
}
|
||||
}
|
||||
|
||||
public IDomainResult Upload(string workingdirectory, string fileName, byte[] bytes) {
|
||||
|
||||
try {
|
||||
_sftpClient.ChangeDirectory(workingdirectory);
|
||||
_logger.LogInformation($"Changed directory to {workingdirectory}");
|
||||
|
||||
using var memoryStream = new MemoryStream(bytes);
|
||||
|
||||
_logger.LogInformation($"Uploading {fileName} ({memoryStream.Length:N0} bytes)");
|
||||
|
||||
_sftpClient.BufferSize = 4 * 1024; // bypass Payload error large files
|
||||
_sftpClient.UploadFile(memoryStream, fileName);
|
||||
|
||||
return IDomainResult.Success();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
_logger.LogError(ex, "SSH Service unhandled exeption");
|
||||
return IDomainResult.CriticalDependencyError();
|
||||
}
|
||||
}
|
||||
|
||||
public IDomainResult ListDir(string workingdirectory) {
|
||||
try {
|
||||
|
||||
var listDirectory = _sftpClient.ListDirectory(workingdirectory);
|
||||
|
||||
_logger.LogInformation($"Listing directory:");
|
||||
|
||||
foreach (var file in listDirectory) {
|
||||
_logger.LogInformation($" - " + file.Name);
|
||||
}
|
||||
|
||||
return IDomainResult.Success();
|
||||
}
|
||||
|
||||
catch (Exception ex) {
|
||||
_logger.LogError(ex, "SSH Service unhandled exeption");
|
||||
return IDomainResult.CriticalDependencyError();
|
||||
}
|
||||
}
|
||||
|
||||
public IDomainResult Download() {
|
||||
return IDomainResult.Failed();
|
||||
}
|
||||
|
||||
public IDomainResult RunSudoCommand(string password, string command) {
|
||||
try {
|
||||
command = $"sudo {command}";
|
||||
|
||||
using (var shellStream = _sshClient.CreateShellStream("xterm", 80, 24, 800, 600, 1024, new Dictionary<TerminalModes, uint> {
|
||||
{ TerminalModes.ECHO, 53 }
|
||||
})) {
|
||||
// Get logged in
|
||||
string rep = shellStream.Expect(new Regex(@"[$>]"), TimeSpan.FromSeconds(10)); // expect user prompt with timeout
|
||||
_logger.LogInformation("Initial prompt: {Prompt}", rep);
|
||||
|
||||
// Send command
|
||||
shellStream.WriteLine(command);
|
||||
rep = shellStream.Expect(new Regex(@"([$#>:])"), TimeSpan.FromSeconds(10)); // expect password or user prompt with timeout
|
||||
_logger.LogInformation("After command prompt: {Prompt}", rep);
|
||||
|
||||
// Check to send password
|
||||
if (rep.Contains(":")) {
|
||||
// Send password
|
||||
shellStream.WriteLine(password);
|
||||
rep = shellStream.Expect(new Regex(@"[$#>]"), TimeSpan.FromSeconds(10)); // expect user or root prompt with timeout
|
||||
_logger.LogInformation("After password prompt: {Prompt}", rep);
|
||||
}
|
||||
|
||||
return IDomainResult.Success();
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
_logger.LogError(ex, "SSH Service unhandled exception");
|
||||
return IDomainResult.CriticalDependencyError();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_sshClient.Disconnect();
|
||||
_sshClient.Dispose();
|
||||
|
||||
_sftpClient.Disconnect();
|
||||
_sftpClient.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using Xunit;
|
||||
|
||||
//using PecMgr.VaultProvider.Extensions;
|
||||
//using PecMgr.VaultProvider;
|
||||
//using PecMgr.Core.Abstractions;
|
||||
|
||||
namespace MaksIT.Tests.SSHProviderTests.Abstractions;
|
||||
//[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
|
||||
public abstract class ConfigurationBase {
|
||||
|
||||
protected IConfiguration Configuration;
|
||||
|
||||
protected ServiceCollection ServiceCollection = new ServiceCollection();
|
||||
|
||||
protected ServiceProvider ServiceProvider { get => ServiceCollection.BuildServiceProvider(); }
|
||||
|
||||
public ConfigurationBase() {
|
||||
Configuration = InitConfig();
|
||||
ConfigureServices(ServiceCollection);
|
||||
}
|
||||
|
||||
protected abstract void ConfigureServices(IServiceCollection services);
|
||||
|
||||
private IConfiguration InitConfig() {
|
||||
var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
|
||||
var currentDirectory = Directory.GetCurrentDirectory();
|
||||
|
||||
var configurationBuilder = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddEnvironmentVariables();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(aspNetCoreEnvironment) && new FileInfo(Path.Combine(currentDirectory, $"appsettings.{aspNetCoreEnvironment}.json")).Exists)
|
||||
configurationBuilder.AddJsonFile($"appsettings.{aspNetCoreEnvironment}.json", true);
|
||||
else if (new FileInfo(Path.Combine(currentDirectory, "appsettings.json")).Exists)
|
||||
configurationBuilder.AddJsonFile("appsettings.json", true, true);
|
||||
else
|
||||
throw new FileNotFoundException($"Unable to find appsetting.json in {currentDirectory}");
|
||||
|
||||
//var builtConfig = configurationBuilder.Build();
|
||||
//var vaultOptions = builtConfig.GetSection("Vault");
|
||||
|
||||
//configurationBuilder.AddVault(options => {
|
||||
// options.Address = vaultOptions["Address"];
|
||||
|
||||
// options.UnsealKeys = vaultOptions.GetSection("UnsealKeys").Get<List<string>>();
|
||||
|
||||
// options.AuthMethod = EnumerationStringId.FromValue<AuthenticationMethod>(vaultOptions["AuthMethod"]);
|
||||
// options.AppRoleAuthMethod = vaultOptions.GetSection("AppRoleAuthMethod").Get<AppRoleAuthMethod>();
|
||||
// options.TokenAuthMethod = vaultOptions.GetSection("TokenAuthMethod").Get<TokenAuthMethod>();
|
||||
|
||||
// options.MountPath = vaultOptions["MountPath"];
|
||||
// options.SecretType = vaultOptions["SecretType"];
|
||||
|
||||
// options.ConfigurationMappings = vaultOptions.GetSection("ConfigurationMappings").Get<Dictionary<string, string>>();
|
||||
//});
|
||||
|
||||
return configurationBuilder.Build();
|
||||
}
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using Serilog;
|
||||
|
||||
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using MaksIT.SSHProvider;
|
||||
|
||||
namespace MaksIT.Tests.SSHProviderTests.Abstractions;
|
||||
public abstract class ServicesBase : ConfigurationBase {
|
||||
|
||||
public ServicesBase() : base() { }
|
||||
|
||||
protected override void ConfigureServices(IServiceCollection services) {
|
||||
// configure strongly typed settings objects
|
||||
var appSettingsSection = Configuration.GetSection("Configuration");
|
||||
services.Configure<Configuration>(appSettingsSection);
|
||||
var appSettings = appSettingsSection.Get<Configuration>();
|
||||
|
||||
#region configurazione logging
|
||||
services.AddLogging(configure => {
|
||||
configure.AddSerilog(new LoggerConfiguration()
|
||||
//.ReadFrom.Configuration(_configuration)
|
||||
.CreateLogger());
|
||||
});
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||
<PackageReference Include="xunit" Version="2.8.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
||||
|
||||
<PackageReference Include="Serilog.Enrichers.Span" Version="3.1.0" />
|
||||
<PackageReference Include="Serilog.Expressions" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\SSHProvider\SSHProvider.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -1,57 +0,0 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using MaksIT.SSHProvider;
|
||||
|
||||
using MaksIT.Tests.SSHProviderTests.Abstractions;
|
||||
|
||||
namespace MaksIT.SSHSerivceTests;
|
||||
public class UnitTest1 : ServicesBase {
|
||||
|
||||
public readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory;
|
||||
|
||||
[Fact]
|
||||
public void UploadFile() {
|
||||
|
||||
var username = "";
|
||||
var password = "";
|
||||
var filePath = Path.Combine(_appPath, "randomfile.txt");
|
||||
CreateRandomFile(filePath, 1);
|
||||
|
||||
var logger = ServiceProvider.GetService<ILogger<SSHService>>();
|
||||
|
||||
using var sshService = new SSHService(logger, "192.168.0.10", 22, username, password);
|
||||
sshService.Connect();
|
||||
|
||||
var bytes = File.ReadAllBytes(filePath);
|
||||
|
||||
logger.LogInformation($"Uploading {filePath} ({bytes.Length:N0} bytes)");
|
||||
|
||||
sshService.RunSudoCommand(password, "chown nginx:nginx /var/www/ssl -R");
|
||||
sshService.RunSudoCommand(password, "chmod 777 /var/www/ssl -R");
|
||||
|
||||
sshService.Upload("/var/www/ssl", Path.GetFileName(filePath), bytes);
|
||||
|
||||
sshService.RunSudoCommand(password, "chown nginx:nginx /var/www/ssl -R");
|
||||
sshService.RunSudoCommand(password, "chmod 775 /var/www/ssl -R");
|
||||
}
|
||||
|
||||
private void CreateRandomFile(string filePath, int sizeInMb) {
|
||||
// Note: block size must be a factor of 1MB to avoid rounding errors
|
||||
const int blockSize = 1024 * 8;
|
||||
const int blocksPerMb = (1024 * 1024) / blockSize;
|
||||
|
||||
byte[] data = new byte[blockSize];
|
||||
|
||||
using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider()) {
|
||||
using (FileStream stream = File.OpenWrite(filePath)) {
|
||||
for (int i = 0; i < sizeInMb * blocksPerMb; i++) {
|
||||
crypto.GetBytes(data);
|
||||
stream.Write(data, 0, data.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
global using Xunit;
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"Serilog": {
|
||||
"Using": [ "Serilog.Settings.Configuration", "Serilog.Expressions", "Serilog.Sinks.Console" ],
|
||||
"MinimumLevel": "Verbose",
|
||||
"Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ],
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "Console",
|
||||
"Args": {
|
||||
"restrictedToMinimumLevel": "Verbose",
|
||||
//"formatter": "Serilog.Formatting.Compact.RenderedCompactJsonFormatter, Serilog.Formatting.Compact"
|
||||
|
||||
"theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console",
|
||||
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} <s:{SourceContext}>{NewLine}{Exception}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"Configuration": {
|
||||
|
||||
}
|
||||
}
|
||||
@ -14,8 +14,6 @@ services:
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
- LETSENCRYPT_SERVER=http://localhost:8080
|
||||
# ports:
|
||||
# - "3000:3000"
|
||||
networks:
|
||||
- maks-it
|
||||
|
||||
@ -31,10 +29,8 @@ services:
|
||||
- MAKS-IT_AGENT_KEY=UGnCaElLLJClHgUeet/yr7vNvPf13b1WkDJQMfsiP6I=
|
||||
- MAKS-IT_AGENT_SERVICE=haproxy
|
||||
volumes:
|
||||
- ./docker-compose/LetsEncryptServer/acme:/app/bin/Debug/net8.0/acme
|
||||
- ./docker-compose/LetsEncryptServer/cache:/app/bin/Debug/net8.0/cache
|
||||
ports:
|
||||
- "5000:5000"
|
||||
- D:\Compose\MaksIT.CertsUI\acme:/app/bin/Debug/net8.0/acme
|
||||
- D:\Compose\MaksIT.CertsUI\cache:/app/bin/Debug/net8.0/cache
|
||||
networks:
|
||||
- maks-it
|
||||
|
||||
|
||||
6
src/helm/Chart.yaml
Normal file
6
src/helm/Chart.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: certs-ui
|
||||
description: MaksIT CertsUI
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "latest"
|
||||
35
src/helm/templates/_helpers.tpl
Normal file
35
src/helm/templates/_helpers.tpl
Normal file
@ -0,0 +1,35 @@
|
||||
{{- define "certs-ui.name" -}}
|
||||
{{- .Chart.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "certs-ui.fullname" -}}
|
||||
{{- $name := .Chart.Name -}}
|
||||
{{- $rel := .Release.Name -}}
|
||||
{{- if or (hasPrefix (printf "%s-" $name) $rel) (eq $rel $name) -}}
|
||||
{{- $rel | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" $rel $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "certs-ui.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" -}}
|
||||
{{- end }}
|
||||
|
||||
{{- define "certs-ui.labels" -}}
|
||||
app.kubernetes.io/name: {{ include "certs-ui.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
helm.sh/chart: {{ include "certs-ui.chart" . }}
|
||||
{{- end }}
|
||||
|
||||
{{- /* Image pull secrets (global) -> list of names) */ -}}
|
||||
{{- define "certs-ui.imagePullSecrets" -}}
|
||||
{{- $ips := default (list) .Values.global.imagePullSecrets -}}
|
||||
{{- if $ips }}
|
||||
imagePullSecrets:
|
||||
{{- range $ips }}
|
||||
- name: {{ .name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
83
src/helm/templates/deployments.yaml
Normal file
83
src/helm/templates/deployments.yaml
Normal file
@ -0,0 +1,83 @@
|
||||
{{- $root := . -}}
|
||||
{{- range $compName, $comp := .Values.components }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "certs-ui.fullname" $root }}-{{ $compName }}
|
||||
labels:
|
||||
{{- include "certs-ui.labels" $root | nindent 4 }}
|
||||
app.kubernetes.io/component: {{ $compName }}
|
||||
spec:
|
||||
replicas: {{ default 1 $comp.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: {{ $root.Release.Name }}
|
||||
app.kubernetes.io/name: {{ include "certs-ui.name" $root }}
|
||||
app.kubernetes.io/component: {{ $compName }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "certs-ui.labels" $root | nindent 8 }}
|
||||
app.kubernetes.io/component: {{ $compName }}
|
||||
{{- if and $comp.secretsFile $comp.secretsFile.forceUpdate }}
|
||||
annotations:
|
||||
"checksum/secrets-file": {{ (default "" $comp.secretsFile.content) | toString | sha256sum | quote }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- include "certs-ui.imagePullSecrets" $root | nindent 6 }}
|
||||
containers:
|
||||
- name: {{ $compName }}
|
||||
image: "{{ $comp.image.registry }}/{{ $comp.image.repository }}:{{ $comp.image.tag }}"
|
||||
imagePullPolicy: {{ default "IfNotPresent" $comp.image.pullPolicy }}
|
||||
{{ $svc := default dict $comp.service }}
|
||||
{{ $tgt := default 8080 $svc.targetPort }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ $tgt }}
|
||||
{{- if $comp.env }}
|
||||
env:
|
||||
{{- range $comp.env }}
|
||||
- name: {{ .name }}
|
||||
value: {{ .value | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- $p := default dict $comp.persistence -}}
|
||||
{{- $vols := default (list) $p.volumes -}}
|
||||
{{- $hasVols := gt (len $vols) 0 -}}
|
||||
{{- $hasSecret := (hasKey $comp "secretsFile") -}}
|
||||
{{- if or $hasVols $hasSecret }}
|
||||
volumeMounts:
|
||||
{{- range $vol := $vols }}
|
||||
- name: {{ $compName }}-{{ $vol.name }}
|
||||
mountPath: {{ $vol.mountPath }}
|
||||
{{- end }}
|
||||
{{- if $comp.secretsFile }}
|
||||
- name: {{ $compName }}-secrets
|
||||
mountPath: {{ $comp.secretsFile.mountPath }}
|
||||
subPath: {{ base $comp.secretsFile.mountPath }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if or $hasVols $hasSecret }}
|
||||
volumes:
|
||||
{{- range $vol := $vols }}
|
||||
- name: {{ $compName }}-{{ $vol.name }}
|
||||
{{- if eq $vol.type "pvc" }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "certs-ui.fullname" $root }}-{{ $compName }}-{{ $vol.name }}
|
||||
{{- else if eq $vol.type "emptyDir" }}
|
||||
emptyDir: {{ toYaml (default dict $vol.emptyDir) | nindent 12 }}
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if $comp.secretsFile }}
|
||||
- name: {{ $compName }}-secrets
|
||||
secret:
|
||||
secretName: {{ include "certs-ui.fullname" $root }}-{{ $compName }}-secrets
|
||||
items:
|
||||
- key: {{ $comp.secretsFile.key }}
|
||||
path: {{ base $comp.secretsFile.mountPath }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
23
src/helm/templates/pvc.yaml
Normal file
23
src/helm/templates/pvc.yaml
Normal file
@ -0,0 +1,23 @@
|
||||
{{- $root := . -}}
|
||||
{{- range $compName, $comp := .Values.components }}
|
||||
{{- $p := default dict $comp.persistence }}
|
||||
{{- $vols := default (list) $p.volumes }}
|
||||
{{- range $vol := $vols }}
|
||||
{{- if and (eq $vol.type "pvc") $vol.pvc.create }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "certs-ui.fullname" $root }}-{{ $compName }}-{{ $vol.name }}
|
||||
labels:
|
||||
{{- include "certs-ui.labels" $root | nindent 4 }}
|
||||
app.kubernetes.io/component: {{ $compName }}
|
||||
spec:
|
||||
accessModes: {{ toYaml (default (list "ReadWriteOnce") $vol.pvc.accessModes) | nindent 2 }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ default "1Gi" $vol.pvc.size }}
|
||||
storageClassName: {{ default "local-path" $vol.pvc.storageClass }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
28
src/helm/templates/secret-appsecrets.yaml
Normal file
28
src/helm/templates/secret-appsecrets.yaml
Normal file
@ -0,0 +1,28 @@
|
||||
{{- $root := . -}}
|
||||
{{- range $compName, $comp := .Values.components }}
|
||||
{{- if $comp.secretsFile }}
|
||||
{{- $sf := $comp.secretsFile -}}
|
||||
{{- $secretName := printf "%s-%s-secrets" (include "certs-ui.fullname" $root) $compName -}}
|
||||
{{- $existing := lookup "v1" "Secret" $root.Release.Namespace $secretName -}}
|
||||
{{- if and $sf.keep $existing }}
|
||||
{{/* keep=true and Secret exists -> render nothing */}}
|
||||
{{- else }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ $secretName }}
|
||||
labels:
|
||||
{{- include "certs-ui.labels" $root | nindent 4 }}
|
||||
app.kubernetes.io/component: {{ $compName }}
|
||||
{{- if $sf.keep }}
|
||||
annotations:
|
||||
"helm.sh/resource-policy": keep
|
||||
{{- end }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
{{ $sf.key }}: |
|
||||
{{ $sf.content | indent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
73
src/helm/templates/services.yaml
Normal file
73
src/helm/templates/services.yaml
Normal file
@ -0,0 +1,73 @@
|
||||
{{- $root := . -}}
|
||||
{{- range $compName, $comp := .Values.components }}
|
||||
{{- $svc := default dict $comp.service }}
|
||||
{{- if and $svc $svc.enabled }}
|
||||
{{- $stype := default "ClusterIP" $svc.type }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "certs-ui.fullname" $root }}-{{ $compName }}
|
||||
labels:
|
||||
{{- include "certs-ui.labels" $root | nindent 4 }}
|
||||
app.kubernetes.io/component: {{ $compName }}
|
||||
{{- if $svc.labels }}
|
||||
{{ toYaml $svc.labels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if $svc.annotations }}
|
||||
annotations:
|
||||
{{ toYaml $svc.annotations | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ $stype }}
|
||||
{{- if $svc.clusterIP }}
|
||||
clusterIP: {{ $svc.clusterIP }}
|
||||
{{- end }}
|
||||
{{- if $svc.loadBalancerClass }}
|
||||
loadBalancerClass: {{ $svc.loadBalancerClass }}
|
||||
{{- end }}
|
||||
{{- if and (or (eq $stype "LoadBalancer") (eq $stype "NodePort")) ($svc.allocateLoadBalancerNodePorts | default nil) }}
|
||||
allocateLoadBalancerNodePorts: {{ $svc.allocateLoadBalancerNodePorts }}
|
||||
{{- end }}
|
||||
{{- if and (eq $stype "LoadBalancer") $svc.loadBalancerSourceRanges }}
|
||||
loadBalancerSourceRanges:
|
||||
{{ toYaml $svc.loadBalancerSourceRanges | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if and (eq $stype "LoadBalancer") $svc.ipFamilies }}
|
||||
ipFamilies:
|
||||
{{ toYaml $svc.ipFamilies | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if and (eq $stype "LoadBalancer") $svc.ipFamilyPolicy }}
|
||||
ipFamilyPolicy: {{ $svc.ipFamilyPolicy }}
|
||||
{{- end }}
|
||||
{{- if and (eq $stype "LoadBalancer") $svc.loadBalancerIP }}
|
||||
loadBalancerIP: {{ $svc.loadBalancerIP }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ default 80 $svc.port }}
|
||||
targetPort: {{ default 80 $svc.targetPort }}
|
||||
{{- if eq $stype "NodePort" }}
|
||||
{{- if $svc.nodePort }}
|
||||
nodePort: {{ $svc.nodePort }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
selector:
|
||||
app.kubernetes.io/instance: {{ $root.Release.Name }}
|
||||
app.kubernetes.io/name: {{ include "certs-ui.name" $root }}
|
||||
app.kubernetes.io/component: {{ $compName }}
|
||||
{{- if and (ne $stype "ClusterIP") $svc.externalTrafficPolicy }}
|
||||
externalTrafficPolicy: {{ $svc.externalTrafficPolicy }}
|
||||
{{- end }}
|
||||
{{- if and (eq $stype "LoadBalancer") $svc.healthCheckNodePort }}
|
||||
healthCheckNodePort: {{ $svc.healthCheckNodePort }}
|
||||
{{- end }}
|
||||
{{- if and (typeIs "string" $svc.sessionAffinity) $svc.sessionAffinity }}
|
||||
sessionAffinity: {{ $svc.sessionAffinity }}
|
||||
{{- if and (eq $svc.sessionAffinity "ClientIP") (typeIs "map" $svc.sessionAffinityConfig) }}
|
||||
sessionAffinityConfig:
|
||||
{{ toYaml $svc.sessionAffinityConfig | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
96
src/helm/values.yaml
Normal file
96
src/helm/values.yaml
Normal file
@ -0,0 +1,96 @@
|
||||
global:
|
||||
imagePullSecrets:
|
||||
- name: cr-maksit-pull
|
||||
|
||||
components:
|
||||
server:
|
||||
replicas: 1
|
||||
image:
|
||||
registry: cr.maks-it.com
|
||||
repository: certs-ui/server
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
env:
|
||||
- name: ASPNETCORE_ENVIRONMENT
|
||||
value: Development
|
||||
- name: ASPNETCORE_HTTP_PORTS
|
||||
value: "5000"
|
||||
service:
|
||||
enabled: true
|
||||
type: ClusterIP
|
||||
port: 5000
|
||||
targetPort: 5000
|
||||
persistence:
|
||||
volumes:
|
||||
- name: acme
|
||||
mountPath: /acme
|
||||
type: pvc
|
||||
pvc:
|
||||
create: true
|
||||
storageClass: local-path
|
||||
size: 50Mi
|
||||
accessModes: [ReadWriteOnce]
|
||||
- name: cache
|
||||
mountPath: /cache
|
||||
type: pvc
|
||||
pvc:
|
||||
create: true
|
||||
storageClass: local-path
|
||||
size: 50Mi
|
||||
accessModes: [ReadWriteOnce]
|
||||
secretsFile:
|
||||
key: appsecrets.json
|
||||
mountPath: /secrets/appsecrets.json
|
||||
content: |
|
||||
{
|
||||
"Agent": {
|
||||
}
|
||||
}
|
||||
keep: true
|
||||
forceUpdate: false
|
||||
|
||||
client:
|
||||
replicas: 1
|
||||
image:
|
||||
registry: cr.maks-it.com
|
||||
repository: certs-ui/client
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
env:
|
||||
- name: ASPNETCORE_ENVIRONMENT
|
||||
value: Development
|
||||
- name: NEXT_PUBLIC_API_BASE_URL
|
||||
value: http://certs-ui-server:5000
|
||||
service:
|
||||
enabled: true
|
||||
type: ClusterIP
|
||||
port: 3000
|
||||
targetPort: 3000
|
||||
|
||||
reverseproxy:
|
||||
replicas: 1
|
||||
image:
|
||||
registry: cr.maks-it.com
|
||||
repository: certs-ui/reverseproxy
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
env:
|
||||
- name: ASPNETCORE_ENVIRONMENT
|
||||
value: Development
|
||||
- name: ASPNETCORE_HTTP_PORTS
|
||||
value: "8080"
|
||||
service:
|
||||
enabled: true
|
||||
type: LoadBalancer
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
loadBalancerIP: "172.16.0.5"
|
||||
annotations:
|
||||
lbipam.cilium.io/ips: "172.16.0.5"
|
||||
labels:
|
||||
export: "bgp"
|
||||
externalTrafficPolicy: Local
|
||||
sessionAffinity: ClientIP
|
||||
sessionAffinityConfig:
|
||||
clientIP:
|
||||
timeoutSeconds: 10800
|
||||
Loading…
Reference in New Issue
Block a user