mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2026-05-16 04:48:12 +02:00
(refactor): reverting VersionInfo to standard behavior
This commit is contained in:
parent
d7721ff80e
commit
1c68cc63b8
24
CHANGELOG.md
24
CHANGELOG.md
@ -4,25 +4,35 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [3.3.22] - 2026-04-27
|
||||
|
||||
### Changed
|
||||
|
||||
- **Release tooling / frontend image versioning:** `DockerPush` now supports per-image `versionEnvFiles` and temporarily rewrites `VITE_APP_VERSION` in `src/MaksIT.WebUI/.env` and `src/MaksIT.WebUI/.env.prod` to the release semver from `<Version>` (for example `3.3.22`) during docker build/push, then restores original files so placeholders remain unchanged in git.
|
||||
- **HA / Terms of Service PDF cache:** Replaced pod-filesystem Terms of Service PDF caching with shared PostgreSQL cache table `terms_of_service_cache` (`url`, `url_hash_hex`, `etag`, `last_modified_utc`, `content_type`, `content_bytes`, `fetched_at_utc`, `expires_at_utc`). ToS retrieval now uses cache TTL/HTTP validators and no longer relies on local files.
|
||||
- **Terms of Service API:** Uses only session-based endpoint `GET /api/certs/{sessionId}/terms-of-service` for interactive ACME flows (stateless `isStaging` variant removed).
|
||||
|
||||
## [3.3.21] - 2026-04-26
|
||||
|
||||
### Changed
|
||||
|
||||
- **FluentMigrator:** Restored FluentMigrator defaults for the migration history table (**`VersionInfo`**, columns **`Version`**, **`AppliedOn`**, **`Description`**). Removed **`CertsFluentMigratorVersionTableMetaData`**, custom **`IVersionTableMetaDataAccessor`** registration, and post-migrate verification against **`version_info`**.
|
||||
|
||||
## [3.3.20] - 2026-04-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- **FluentMigrator DI:** **`AddFluentMigratorCore`** registers a default **`IVersionTableMetaDataAccessor`**; **`WithVersionTable`** on **`ConfigureRunner`** could still leave **`VersionInfo`** in use. Engine registration now removes any prior **`IVersionTableMetaDataAccessor`** descriptors and registers **`PassThroughVersionTableMetaDataAccessor`** for **`CertsFluentMigratorVersionTableMetaData`** so **`MigrateUp`** always targets **`version_info`**.
|
||||
- **FluentMigrator DI:** Attempted to force **`version_info`** by replacing **`IVersionTableMetaDataAccessor`** registrations. **Superseded in 3.3.21:** reverted to default **`VersionInfo`** table and standard column names (see **3.3.21**).
|
||||
|
||||
## [3.3.19] - 2026-04-26
|
||||
|
||||
### Changed
|
||||
|
||||
- **FluentMigrator / PostgreSQL:** Version metadata table is **`public.version_info`** with snake_case columns **`version`**, **`applied_on`**, **`description`** and unique index **`uc_version`**, configured via **`CertsFluentMigratorVersionTableMetaData`** and **`WithVersionTable`** on the runner (aligned with the rest of the schema naming).
|
||||
|
||||
### Removed
|
||||
|
||||
- **Startup migrations:** Removed legacy compatibility paths: **`VersionInfo` → `version_info` rename**, PascalCase → snake_case column repair on the version table, and the **EF-era baseline** that created or seeded **`version_info`** when **`users`** already existed (and **`RunMigrationsService.BaselineVersion`**).
|
||||
- **Startup migrations:** Removed legacy compatibility paths (EF-era baseline that seeded the version table when **`users`** already existed, **`VersionInfo` → `version_info` rename**, PascalCase → snake_case column repair, and **`RunMigrationsService.BaselineVersion`**).
|
||||
|
||||
### Upgrade notes
|
||||
|
||||
- **Breaking:** **Recreate the Certs engine database** (or use a new empty database). The app no longer upgrades in place from **`VersionInfo`**, mixed column casing, or pre–FluentMigrator-baseline layouts; **`MigrateUp`** expects a clean or fully FluentMigrator-managed schema.
|
||||
- **Breaking:** **Recreate the Certs engine database** (or use a new empty database) if you still relied on those removed startup paths; **`MigrateUp`** expects a schema managed only by FluentMigrator migrations in-process.
|
||||
|
||||
## [3.3.18] - 2026-04-26
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="147.5" height="20" role="img" aria-label="Branch Coverage: 7.8%">
|
||||
<title>Branch Coverage: 7.8%</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="147.5" height="20" role="img" aria-label="Branch Coverage: 7.7%">
|
||||
<title>Branch Coverage: 7.7%</title>
|
||||
<linearGradient id="s" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
@ -15,7 +15,7 @@
|
||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Branch Coverage</text>
|
||||
<text x="53.75" y="14" fill="#fff">Branch Coverage</text>
|
||||
<text aria-hidden="true" x="127.5" y="15" fill="#010101" fill-opacity=".3">7.8%</text>
|
||||
<text x="127.5" y="14" fill="#fff">7.8%</text>
|
||||
<text aria-hidden="true" x="127.5" y="15" fill="#010101" fill-opacity=".3">7.7%</text>
|
||||
<text x="127.5" y="14" fill="#fff">7.7%</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -1,5 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Method Coverage: 19.8%">
|
||||
<title>Method Coverage: 19.8%</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Method Coverage: 19.9%">
|
||||
<title>Method Coverage: 19.9%</title>
|
||||
<linearGradient id="s" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
@ -15,7 +15,7 @@
|
||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Method Coverage</text>
|
||||
<text x="53.75" y="14" fill="#fff">Method Coverage</text>
|
||||
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">19.8%</text>
|
||||
<text x="128.75" y="14" fill="#fff">19.8%</text>
|
||||
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">19.9%</text>
|
||||
<text x="128.75" y="14" fill="#fff">19.9%</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -73,6 +73,17 @@ public static class CertsLinq2DbMapping {
|
||||
.Property(x => x.TokenValue).HasColumnName("token_value")
|
||||
.Property(x => x.CreatedAtUtc).HasColumnName("created_at_utc");
|
||||
|
||||
builder.Entity<TermsOfServiceCacheDto>()
|
||||
.HasTableName(Table.TermsOfServiceCache.Name)
|
||||
.Property(x => x.Url).HasColumnName("url").IsPrimaryKey()
|
||||
.Property(x => x.UrlHashHex).HasColumnName("url_hash_hex")
|
||||
.Property(x => x.ETag).HasColumnName("etag")
|
||||
.Property(x => x.LastModifiedUtc).HasColumnName("last_modified_utc")
|
||||
.Property(x => x.ContentType).HasColumnName("content_type")
|
||||
.Property(x => x.ContentBytes).HasColumnName("content_bytes")
|
||||
.Property(x => x.FetchedAtUtc).HasColumnName("fetched_at_utc")
|
||||
.Property(x => x.ExpiresAtUtc).HasColumnName("expires_at_utc");
|
||||
|
||||
builder.Build();
|
||||
return schema;
|
||||
}
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
using MaksIT.CertsUI.Engine.Domain.Certs;
|
||||
using MaksIT.CertsUI.Engine.Dto.Certs;
|
||||
using MaksIT.CertsUI.Engine.Infrastructure;
|
||||
using MaksIT.CertsUI.Engine.Persistance.Services;
|
||||
using MaksIT.CertsUI.Engine.RuntimeCoordination;
|
||||
using MaksIT.CertsUI.Engine.Services;
|
||||
using MaksIT.Results;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace MaksIT.CertsUI.Engine.DomainServices;
|
||||
|
||||
@ -14,7 +19,7 @@ namespace MaksIT.CertsUI.Engine.DomainServices;
|
||||
public interface ICertsFlowDomainService {
|
||||
|
||||
#region Terms of service
|
||||
Result<string?> GetTermsOfService(Guid sessionId);
|
||||
Task<Result<string?>> GetTermsOfServiceAsync(Guid sessionId);
|
||||
#endregion
|
||||
|
||||
#region Session, orders, and certificates
|
||||
@ -54,6 +59,7 @@ public class CertsFlowDomainService : ICertsFlowDomainService {
|
||||
private readonly IRegistrationCachePersistanceService _registrationCache;
|
||||
private readonly IAgentDeploymentService _agentDeployment;
|
||||
private readonly ICertsFlowEngineConfiguration _config;
|
||||
private readonly ITermsOfServiceCachePersistenceService _termsOfServiceCache;
|
||||
private readonly IAcmeHttpChallengePersistenceService _httpChallenges;
|
||||
private readonly IRuntimeLeaseService _runtimeLease;
|
||||
private readonly IRuntimeInstanceId _runtimeInstance;
|
||||
@ -67,6 +73,7 @@ public class CertsFlowDomainService : ICertsFlowDomainService {
|
||||
IRegistrationCachePersistanceService registrationCache,
|
||||
IAgentDeploymentService agentDeployment,
|
||||
ICertsFlowEngineConfiguration config,
|
||||
ITermsOfServiceCachePersistenceService termsOfServiceCache,
|
||||
IAcmeHttpChallengePersistenceService httpChallenges,
|
||||
IRuntimeLeaseService runtimeLease,
|
||||
IRuntimeInstanceId runtimeInstance,
|
||||
@ -77,6 +84,7 @@ public class CertsFlowDomainService : ICertsFlowDomainService {
|
||||
_registrationCache = registrationCache;
|
||||
_agentDeployment = agentDeployment;
|
||||
_config = config;
|
||||
_termsOfServiceCache = termsOfServiceCache;
|
||||
_httpChallenges = httpChallenges;
|
||||
_runtimeLease = runtimeLease;
|
||||
_runtimeInstance = runtimeInstance;
|
||||
@ -86,44 +94,85 @@ public class CertsFlowDomainService : ICertsFlowDomainService {
|
||||
|
||||
#region Terms of service
|
||||
|
||||
public Result<string?> GetTermsOfService(Guid sessionId) {
|
||||
if (!_primaryReplica.IsPrimary)
|
||||
return Result<string?>.ServiceUnavailable(null, CertsFlowPrimaryReplica.ServiceUnavailableMessages);
|
||||
public async Task<Result<string?>> GetTermsOfServiceAsync(Guid sessionId) {
|
||||
var termsUriResult = _letsEncryptService.GetTermsOfServiceUri(sessionId);
|
||||
if (!termsUriResult.IsSuccess || termsUriResult.Value == null)
|
||||
return termsUriResult;
|
||||
|
||||
var result = _letsEncryptService.GetTermsOfServiceUri(sessionId);
|
||||
if (!result.IsSuccess || result.Value == null)
|
||||
return result;
|
||||
|
||||
var termsOfServiceUrl = result.Value;
|
||||
return await GetOrFetchTermsOfServicePdfBase64Async(termsUriResult.Value);
|
||||
}
|
||||
|
||||
private async Task<Result<string?>> GetOrFetchTermsOfServicePdfBase64Async(string termsOfServiceUrl) {
|
||||
try {
|
||||
var fileName = Path.GetFileName(new Uri(termsOfServiceUrl).LocalPath);
|
||||
var termsOfServicePdfPath = Path.Combine(_config.DataFolder, fileName);
|
||||
foreach (var file in Directory.GetFiles(_config.DataFolder, "*.pdf")) {
|
||||
if (!string.Equals(Path.GetFileName(file), fileName, StringComparison.OrdinalIgnoreCase)) {
|
||||
try {
|
||||
File.Delete(file);
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
}
|
||||
var cachedResult = await _termsOfServiceCache.GetByUrlAsync(termsOfServiceUrl);
|
||||
if (cachedResult.IsSuccess && cachedResult.Value != null && cachedResult.Value.ExpiresAtUtc > DateTimeOffset.UtcNow)
|
||||
return Result<string?>.Ok(Convert.ToBase64String(cachedResult.Value.ContentBytes));
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, termsOfServiceUrl);
|
||||
if (cachedResult.IsSuccess && cachedResult.Value != null) {
|
||||
if (!string.IsNullOrWhiteSpace(cachedResult.Value.ETag))
|
||||
request.Headers.TryAddWithoutValidation("If-None-Match", cachedResult.Value.ETag);
|
||||
if (cachedResult.Value.LastModifiedUtc.HasValue)
|
||||
request.Headers.IfModifiedSince = cachedResult.Value.LastModifiedUtc.Value;
|
||||
}
|
||||
byte[] pdfBytes;
|
||||
if (File.Exists(termsOfServicePdfPath)) {
|
||||
pdfBytes = File.ReadAllBytes(termsOfServicePdfPath);
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
if (response.StatusCode == HttpStatusCode.NotModified && cachedResult.IsSuccess && cachedResult.Value != null) {
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var notModifiedEntry = cachedResult.Value;
|
||||
notModifiedEntry.FetchedAtUtc = now;
|
||||
notModifiedEntry.ExpiresAtUtc = GetExpiry(now, response.Headers.CacheControl, response.Content.Headers.Expires);
|
||||
var refreshResult = await _termsOfServiceCache.UpsertAsync(notModifiedEntry);
|
||||
if (!refreshResult.IsSuccess)
|
||||
return refreshResult.ToResultOfType<string?>(null);
|
||||
return Result<string?>.Ok(Convert.ToBase64String(notModifiedEntry.ContentBytes));
|
||||
}
|
||||
else {
|
||||
pdfBytes = _httpClient.GetByteArrayAsync(termsOfServiceUrl).GetAwaiter().GetResult();
|
||||
File.WriteAllBytes(termsOfServicePdfPath, pdfBytes);
|
||||
}
|
||||
var base64 = Convert.ToBase64String(pdfBytes);
|
||||
return Result<string?>.Ok(base64);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return Result<string?>.InternalServerError(null, $"Failed to download Terms of Service PDF. Status: {(int)response.StatusCode} {response.ReasonPhrase}");
|
||||
|
||||
var bytes = await response.Content.ReadAsByteArrayAsync();
|
||||
if (bytes.Length == 0)
|
||||
return Result<string?>.InternalServerError(null, "Downloaded Terms of Service PDF is empty.");
|
||||
|
||||
var fetchedAt = DateTimeOffset.UtcNow;
|
||||
var cacheEntry = new TermsOfServiceCacheDto {
|
||||
Url = termsOfServiceUrl,
|
||||
UrlHashHex = ComputeSha256Hex(termsOfServiceUrl),
|
||||
ETag = response.Headers.ETag?.Tag,
|
||||
LastModifiedUtc = response.Content.Headers.LastModified,
|
||||
ContentType = response.Content.Headers.ContentType?.MediaType ?? "application/pdf",
|
||||
ContentBytes = bytes,
|
||||
FetchedAtUtc = fetchedAt,
|
||||
ExpiresAtUtc = GetExpiry(fetchedAt, response.Headers.CacheControl, response.Content.Headers.Expires)
|
||||
};
|
||||
|
||||
var upsertResult = await _termsOfServiceCache.UpsertAsync(cacheEntry);
|
||||
if (!upsertResult.IsSuccess)
|
||||
return upsertResult.ToResultOfType<string?>(null);
|
||||
|
||||
return Result<string?>.Ok(Convert.ToBase64String(bytes));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
_logger.LogError(ex, "Failed to download, cache, or convert Terms of Service PDF");
|
||||
return Result<string?>.InternalServerError(null, $"Failed to download, cache, or convert Terms of Service PDF: {ex.Message}");
|
||||
_logger.LogError(ex, "Failed to fetch or cache Terms of Service PDF");
|
||||
return Result<string?>.InternalServerError(null, $"Failed to fetch or cache Terms of Service PDF: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeSha256Hex(string text) {
|
||||
var bytes = Encoding.UTF8.GetBytes(text);
|
||||
var hash = SHA256.HashData(bytes);
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static DateTimeOffset GetExpiry(DateTimeOffset now, CacheControlHeaderValue? cacheControl, DateTimeOffset? expiresHeader) {
|
||||
if (cacheControl?.MaxAge is TimeSpan maxAge && maxAge > TimeSpan.Zero)
|
||||
return now.Add(maxAge);
|
||||
if (expiresHeader.HasValue && expiresHeader.Value > now)
|
||||
return expiresHeader.Value;
|
||||
return now.AddHours(24);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Session, orders, and certificates
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
namespace MaksIT.CertsUI.Engine.Dto.Certs;
|
||||
|
||||
/// <summary>PostgreSQL <c>terms_of_service_cache</c> row keyed by Terms of Service URL.</summary>
|
||||
public class TermsOfServiceCacheDto {
|
||||
public string Url { get; set; } = "";
|
||||
public string UrlHashHex { get; set; } = "";
|
||||
public string? ETag { get; set; }
|
||||
public DateTimeOffset? LastModifiedUtc { get; set; }
|
||||
public string ContentType { get; set; } = "application/pdf";
|
||||
public byte[] ContentBytes { get; set; } = [];
|
||||
public DateTimeOffset FetchedAtUtc { get; set; }
|
||||
public DateTimeOffset ExpiresAtUtc { get; set; }
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
using FluentMigrator.Runner;
|
||||
using FluentMigrator.Runner.Initialization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MaksIT.CertsUI.Engine.DomainServices;
|
||||
using MaksIT.CertsUI.Engine.FluentMigrations;
|
||||
@ -26,21 +25,14 @@ public static class ServiceCollectionExtensions {
|
||||
if (string.IsNullOrWhiteSpace(certsEngineConfiguration.ConnectionString))
|
||||
throw new ArgumentException("Certs engine connection string is required for FluentMigrator (empty string uses connectionless/preview mode and will not create tables).", nameof(certsEngineConfiguration));
|
||||
|
||||
// FluentMigrator: IRunMigrationsService invoked from Program.cs before RunAsync. Use .For.All() so migration discovery
|
||||
// matches in-process runner expectations (see FluentMigrator docs / #1062).
|
||||
// Version table: AddFluentMigratorCore registers a default IVersionTableMetaDataAccessor; replace it so public.version_info
|
||||
// is always used (ConfigureRunner's WithVersionTable alone can lose to that default depending on DI resolution order).
|
||||
// FluentMigrator: IRunMigrationsService invoked from Program.cs before RunAsync. Use .For.All() so version metadata
|
||||
// and migration discovery match in-process runner expectations (see FluentMigrator docs / #1062).
|
||||
services.AddFluentMigratorCore()
|
||||
.ConfigureRunner(rb => rb
|
||||
.AddPostgres()
|
||||
.WithGlobalConnectionString(certsEngineConfiguration.ConnectionString)
|
||||
.ScanIn(typeof(BaselineCertsSchema).Assembly).For.All())
|
||||
.AddLogging(lb => lb.AddFluentMigratorConsole());
|
||||
|
||||
foreach (var d in services.Where(x => x.ServiceType == typeof(IVersionTableMetaDataAccessor)).ToList())
|
||||
services.Remove(d);
|
||||
services.AddSingleton<IVersionTableMetaDataAccessor>(
|
||||
new PassThroughVersionTableMetaDataAccessor(new CertsFluentMigratorVersionTableMetaData()));
|
||||
services.AddScoped<IRunMigrationsService, RunMigrationsService>();
|
||||
services.AddScoped<ISchemaSyncService, SchemaSyncService>();
|
||||
|
||||
@ -61,6 +53,7 @@ public static class ServiceCollectionExtensions {
|
||||
#region Registration cache
|
||||
services.AddScoped<IRegistrationCachePersistanceService, RegistrationCachePersistanceServiceLinq2Db>();
|
||||
services.AddScoped<IAcmeHttpChallengePersistenceService, AcmeHttpChallengePersistenceServiceLinq2Db>();
|
||||
services.AddScoped<ITermsOfServiceCachePersistenceService, TermsOfServiceCachePersistenceServiceLinq2Db>();
|
||||
services.AddSingleton<IRuntimeLeaseService, RuntimeLeaseServiceNpgsql>();
|
||||
#endregion
|
||||
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
using FluentMigrator;
|
||||
|
||||
namespace MaksIT.CertsUI.Engine.FluentMigrations;
|
||||
|
||||
[Migration(20260427123000)]
|
||||
public class TermsOfServiceCache : Migration {
|
||||
public override void Up() {
|
||||
Create.Table("terms_of_service_cache")
|
||||
.WithColumn("url").AsCustom("text").NotNullable().PrimaryKey()
|
||||
.WithColumn("url_hash_hex").AsCustom("text").NotNullable()
|
||||
.WithColumn("etag").AsCustom("text").Nullable()
|
||||
.WithColumn("last_modified_utc").AsDateTimeOffset().Nullable()
|
||||
.WithColumn("content_type").AsCustom("text").NotNullable()
|
||||
.WithColumn("content_bytes").AsCustom("bytea").NotNullable()
|
||||
.WithColumn("fetched_at_utc").AsDateTimeOffset().NotNullable()
|
||||
.WithColumn("expires_at_utc").AsDateTimeOffset().NotNullable();
|
||||
|
||||
Create.Index("IX_terms_of_service_cache_url_hash_hex").OnTable("terms_of_service_cache").OnColumn("url_hash_hex");
|
||||
Create.Index("IX_terms_of_service_cache_expires_at_utc").OnTable("terms_of_service_cache").OnColumn("expires_at_utc");
|
||||
}
|
||||
|
||||
public override void Down() {
|
||||
Delete.Index("IX_terms_of_service_cache_url_hash_hex").OnTable("terms_of_service_cache");
|
||||
Delete.Index("IX_terms_of_service_cache_expires_at_utc").OnTable("terms_of_service_cache");
|
||||
Delete.Table("terms_of_service_cache");
|
||||
}
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
using FluentMigrator.Runner.VersionTableInfo;
|
||||
|
||||
namespace MaksIT.CertsUI.Engine.Infrastructure;
|
||||
|
||||
/// <summary>FluentMigrator version table: snake_case <c>public.version_info</c> (table and columns) for PostgreSQL consistency.</summary>
|
||||
public sealed class CertsFluentMigratorVersionTableMetaData : IVersionTableMetaData {
|
||||
|
||||
public const string Table = "version_info";
|
||||
|
||||
public const string VersionColumn = "version";
|
||||
|
||||
public const string AppliedOnColumn = "applied_on";
|
||||
|
||||
public const string DescriptionColumn = "description";
|
||||
|
||||
public const string UniqueIndex = "uc_version";
|
||||
|
||||
public bool OwnsSchema => true;
|
||||
|
||||
public string SchemaName => "public";
|
||||
|
||||
public string TableName => Table;
|
||||
|
||||
public string ColumnName => VersionColumn;
|
||||
|
||||
public string DescriptionColumnName => DescriptionColumn;
|
||||
|
||||
public string UniqueIndexName => UniqueIndex;
|
||||
|
||||
public string AppliedOnColumnName => AppliedOnColumn;
|
||||
|
||||
public bool CreateWithPrimaryKey => false;
|
||||
}
|
||||
@ -51,7 +51,7 @@ public sealed class RunMigrationsService(
|
||||
EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'app_runtime_leases')
|
||||
AND (
|
||||
EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'users')
|
||||
OR EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'version_info')
|
||||
OR EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'VersionInfo')
|
||||
);
|
||||
""",
|
||||
conn);
|
||||
@ -61,7 +61,7 @@ public sealed class RunMigrationsService(
|
||||
return;
|
||||
|
||||
throw new InvalidOperationException(
|
||||
"After migrations and coordination DDL, schema \"public\" is missing \"app_runtime_leases\" and/or core tables (\"users\" / \"version_info\"). " +
|
||||
"After migrations and coordination DDL, schema \"public\" is missing \"app_runtime_leases\" and/or core tables (\"users\" / \"VersionInfo\"). " +
|
||||
"Confirm Database= in the connection string, role CREATE privileges, and that FluentMigrator committed (non-empty connection string).");
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
using MaksIT.CertsUI.Engine.Dto.Certs;
|
||||
using MaksIT.Results;
|
||||
|
||||
namespace MaksIT.CertsUI.Engine.Persistance.Services;
|
||||
|
||||
public interface ITermsOfServiceCachePersistenceService {
|
||||
Task<Result<TermsOfServiceCacheDto?>> GetByUrlAsync(string url, CancellationToken cancellationToken = default);
|
||||
Task<Result> UpsertAsync(TermsOfServiceCacheDto cacheEntry, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
using LinqToDB;
|
||||
using MaksIT.Core.Extensions;
|
||||
using MaksIT.CertsUI.Engine.Dto.Certs;
|
||||
using MaksIT.CertsUI.Engine.Infrastructure;
|
||||
using MaksIT.Results;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MaksIT.CertsUI.Engine.Persistance.Services.Linq2Db;
|
||||
|
||||
public sealed class TermsOfServiceCachePersistenceServiceLinq2Db(
|
||||
ILogger<TermsOfServiceCachePersistenceServiceLinq2Db> logger,
|
||||
ICertsDataConnectionFactory connectionFactory
|
||||
) : ITermsOfServiceCachePersistenceService {
|
||||
|
||||
public Task<Result<TermsOfServiceCacheDto?>> GetByUrlAsync(string url, CancellationToken cancellationToken = default) {
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
return Task.FromResult(Result<TermsOfServiceCacheDto?>.BadRequest(null, "Terms of Service URL is required."));
|
||||
|
||||
try {
|
||||
using var db = connectionFactory.Create();
|
||||
var row = db.GetTable<TermsOfServiceCacheDto>().FirstOrDefault(x => x.Url == url);
|
||||
if (row == null)
|
||||
return Task.FromResult(Result<TermsOfServiceCacheDto?>.NotFound(null, $"Terms of Service cache not found for URL: {url}"));
|
||||
|
||||
return Task.FromResult(Result<TermsOfServiceCacheDto?>.Ok(row));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
logger.LogError(ex, "Failed to load Terms of Service cache for {Url}", url);
|
||||
return Task.FromResult(Result<TermsOfServiceCacheDto?>.InternalServerError(null, ["Failed to load Terms of Service cache.", .. ex.ExtractMessages()]));
|
||||
}
|
||||
}
|
||||
|
||||
public Task<Result> UpsertAsync(TermsOfServiceCacheDto cacheEntry, CancellationToken cancellationToken = default) {
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ArgumentNullException.ThrowIfNull(cacheEntry);
|
||||
if (string.IsNullOrWhiteSpace(cacheEntry.Url))
|
||||
return Task.FromResult(Result.BadRequest("Terms of Service URL is required."));
|
||||
|
||||
try {
|
||||
using var db = connectionFactory.Create();
|
||||
var existing = db.GetTable<TermsOfServiceCacheDto>().FirstOrDefault(x => x.Url == cacheEntry.Url);
|
||||
if (existing == null) {
|
||||
db.Insert(cacheEntry);
|
||||
}
|
||||
else {
|
||||
db.GetTable<TermsOfServiceCacheDto>()
|
||||
.Where(x => x.Url == cacheEntry.Url)
|
||||
.Set(x => x.UrlHashHex, cacheEntry.UrlHashHex)
|
||||
.Set(x => x.ETag, cacheEntry.ETag)
|
||||
.Set(x => x.LastModifiedUtc, cacheEntry.LastModifiedUtc)
|
||||
.Set(x => x.ContentType, cacheEntry.ContentType)
|
||||
.Set(x => x.ContentBytes, cacheEntry.ContentBytes)
|
||||
.Set(x => x.FetchedAtUtc, cacheEntry.FetchedAtUtc)
|
||||
.Set(x => x.ExpiresAtUtc, cacheEntry.ExpiresAtUtc)
|
||||
.Update();
|
||||
}
|
||||
|
||||
return Task.FromResult(Result.Ok());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
logger.LogError(ex, "Failed to upsert Terms of Service cache for {Url}", cacheEntry.Url);
|
||||
return Task.FromResult(Result.InternalServerError(["Failed to upsert Terms of Service cache.", .. ex.ExtractMessages()]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -16,5 +16,6 @@ public class Table(int id, string name) : Enumeration(id, name) {
|
||||
|
||||
#region Certs
|
||||
public static readonly Table RegistrationCaches = new(2, "registration_caches");
|
||||
public static readonly Table TermsOfServiceCache = new(5, "terms_of_service_cache");
|
||||
#endregion
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using System.Net;
|
||||
using MaksIT.CertsUI.Engine.Domain.Certs;
|
||||
using MaksIT.CertsUI.Engine.DomainServices;
|
||||
using MaksIT.CertsUI.Engine.Dto.Certs;
|
||||
using MaksIT.CertsUI.Engine.Infrastructure;
|
||||
using MaksIT.CertsUI.Engine.Persistance.Services;
|
||||
using MaksIT.CertsUI.Engine.RuntimeCoordination;
|
||||
@ -26,6 +27,7 @@ public sealed class CertsFlowServiceTests
|
||||
Mock<ILetsEncryptService> le,
|
||||
Mock<IRegistrationCachePersistanceService>? registrationCache = null,
|
||||
Mock<IAgentDeploymentService>? agent = null,
|
||||
Mock<ITermsOfServiceCachePersistenceService>? termsOfServiceCache = null,
|
||||
Mock<IAcmeHttpChallengePersistenceService>? httpChallenges = null,
|
||||
Mock<IRuntimeLeaseService>? runtimeLease = null,
|
||||
Mock<IRuntimeInstanceId>? runtimeInstance = null,
|
||||
@ -34,6 +36,14 @@ public sealed class CertsFlowServiceTests
|
||||
{
|
||||
registrationCache ??= new Mock<IRegistrationCachePersistanceService>();
|
||||
agent ??= new Mock<IAgentDeploymentService>();
|
||||
var tosCacheProvided = termsOfServiceCache is not null;
|
||||
termsOfServiceCache ??= new Mock<ITermsOfServiceCachePersistenceService>();
|
||||
if (!tosCacheProvided) {
|
||||
termsOfServiceCache.Setup(c => c.GetByUrlAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Result<TermsOfServiceCacheDto?>.NotFound(null, "missing"));
|
||||
termsOfServiceCache.Setup(c => c.UpsertAsync(It.IsAny<TermsOfServiceCacheDto>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Result.Ok());
|
||||
}
|
||||
var httpChallengesProvided = httpChallenges is not null;
|
||||
httpChallenges ??= new Mock<IAcmeHttpChallengePersistenceService>();
|
||||
if (!httpChallengesProvided) {
|
||||
@ -68,6 +78,7 @@ public sealed class CertsFlowServiceTests
|
||||
registrationCache.Object,
|
||||
agent.Object,
|
||||
new TestCertsFlowEngineConfiguration(fx),
|
||||
termsOfServiceCache.Object,
|
||||
httpChallenges.Object,
|
||||
runtimeLease.Object,
|
||||
runtimeInstance.Object,
|
||||
@ -308,40 +319,52 @@ public sealed class CertsFlowServiceTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTermsOfService_WhenLetsEncryptFails_Propagates()
|
||||
public async Task GetTermsOfServiceAsync_WhenLetsEncryptFails_Propagates()
|
||||
{
|
||||
using var fx = new WebApiTestFixture();
|
||||
var sessionId = Guid.NewGuid();
|
||||
var le = new Mock<ILetsEncryptService>();
|
||||
le.Setup(x => x.GetTermsOfServiceUri(It.IsAny<Guid>()))
|
||||
le.Setup(x => x.GetTermsOfServiceUri(sessionId))
|
||||
.Returns(Result<string?>.InternalServerError(null, "no uri"));
|
||||
|
||||
var sut = CreateSut(fx, le);
|
||||
|
||||
var result = sut.GetTermsOfService(Guid.NewGuid());
|
||||
var result = await sut.GetTermsOfServiceAsync(sessionId);
|
||||
|
||||
Assert.False(result.IsSuccess);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTermsOfService_WhenPdfAlreadyOnDisk_ReturnsBase64WithoutHttp()
|
||||
public async Task GetTermsOfServiceAsync_WhenCachedAndNotExpired_ReturnsBase64WithoutHttp()
|
||||
{
|
||||
using var fx = new WebApiTestFixture();
|
||||
var fileName = "cached-tos.pdf";
|
||||
var dataPath = Path.Combine(fx.AppOptions.Value.CertsUIEngineConfiguration.DataFolder, fileName);
|
||||
File.WriteAllBytes(dataPath, [7, 7, 7]);
|
||||
|
||||
var sessionId = Guid.NewGuid();
|
||||
var url = "https://acme.test/sub/cached-tos.pdf";
|
||||
var le = new Mock<ILetsEncryptService>();
|
||||
le.Setup(x => x.GetTermsOfServiceUri(It.IsAny<Guid>()))
|
||||
.Returns(Result<string?>.Ok($"https://acme.test/sub/{fileName}"));
|
||||
le.Setup(x => x.GetTermsOfServiceUri(sessionId))
|
||||
.Returns(Result<string?>.Ok(url));
|
||||
|
||||
var tosCache = new Mock<ITermsOfServiceCachePersistenceService>();
|
||||
tosCache.Setup(c => c.GetByUrlAsync(url, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Result<TermsOfServiceCacheDto?>.Ok(new TermsOfServiceCacheDto {
|
||||
Url = url,
|
||||
UrlHashHex = "abc",
|
||||
ContentType = "application/pdf",
|
||||
ContentBytes = [7, 7, 7],
|
||||
FetchedAtUtc = DateTimeOffset.UtcNow.AddMinutes(-1),
|
||||
ExpiresAtUtc = DateTimeOffset.UtcNow.AddHours(1)
|
||||
}));
|
||||
tosCache.Setup(c => c.UpsertAsync(It.IsAny<TermsOfServiceCacheDto>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Result.Ok());
|
||||
|
||||
var httpHit = false;
|
||||
var sut = CreateSut(fx, le, httpHandler: new StubHttpMessageHandler(_ =>
|
||||
var sut = CreateSut(fx, le, termsOfServiceCache: tosCache, httpHandler: new StubHttpMessageHandler(_ =>
|
||||
{
|
||||
httpHit = true;
|
||||
return new HttpResponseMessage(HttpStatusCode.OK);
|
||||
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent([1, 2, 3]) };
|
||||
}));
|
||||
|
||||
var result = sut.GetTermsOfService(Guid.NewGuid());
|
||||
var result = await sut.GetTermsOfServiceAsync(sessionId);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.Equal(Convert.ToBase64String([7, 7, 7]), result.Value);
|
||||
|
||||
@ -25,8 +25,8 @@ namespace MaksIT.CertsUI.Controllers {
|
||||
}
|
||||
|
||||
[HttpGet("{sessionId}/terms-of-service")]
|
||||
public IActionResult TermsOfService(Guid sessionId) {
|
||||
var result = _certsFlowService.GetTermsOfService(sessionId);
|
||||
public async Task<IActionResult> TermsOfService(Guid sessionId) {
|
||||
var result = await _certsFlowService.GetTermsOfServiceAsync(sessionId);
|
||||
return result.ToCertsFlowActionResult();
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>3.3.20</Version>
|
||||
<Version>3.3.22</Version>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
@ -4,7 +4,7 @@ using MaksIT.Results;
|
||||
namespace MaksIT.CertsUI.Services;
|
||||
|
||||
public interface ICertsFlowService {
|
||||
Result<string?> GetTermsOfService(Guid sessionId);
|
||||
Task<Result<string?>> GetTermsOfServiceAsync(Guid sessionId);
|
||||
Task<Result> CompleteChallengesAsync(Guid sessionId);
|
||||
Task<Result<Guid?>> ConfigureClientAsync(bool isStaging);
|
||||
Task<Result<Guid?>> InitAsync(Guid sessionId, Guid? accountId, string description, string[] contacts);
|
||||
@ -23,8 +23,8 @@ public sealed class CertsFlowService(
|
||||
ICertsFlowDomainService domain
|
||||
) : ICertsFlowService {
|
||||
|
||||
public Result<string?> GetTermsOfService(Guid sessionId) =>
|
||||
domain.GetTermsOfService(sessionId);
|
||||
public Task<Result<string?>> GetTermsOfServiceAsync(Guid sessionId) =>
|
||||
domain.GetTermsOfServiceAsync(sessionId);
|
||||
|
||||
public Task<Result> CompleteChallengesAsync(Guid sessionId) =>
|
||||
domain.CompleteChallengesAsync(sessionId);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
VITE_APP_TITLE=MaksIT.CertsUI
|
||||
VITE_APP_VERSION=0.0.0
|
||||
VITE_COMPANY=MaksIT
|
||||
VITE_COMPANY_URL=https://maks-it.com
|
||||
VITE_API_URL=http://localhost:8080/api
|
||||
@ -1,4 +1,5 @@
|
||||
VITE_APP_TITLE=MaksIT.CertsUI
|
||||
VITE_APP_VERSION=0.0.0
|
||||
VITE_COMPANY=MaksIT
|
||||
VITE_COMPANY_URL=https://maks-it.com
|
||||
VITE_API_URL=http://localhost:8080/api
|
||||
@ -54,8 +54,7 @@ const LayoutWrapper: FC<LayoutWrapperProps> = (props) => {
|
||||
}
|
||||
footer={
|
||||
{
|
||||
children: <p>
|
||||
© {new Date().getFullYear()} <a
|
||||
children: <p>{import.meta.env.VITE_APP_VERSION} - © {new Date().getFullYear()} <a
|
||||
href={import.meta.env.VITE_COMPANY_URL}
|
||||
target={'_blank'}
|
||||
rel={'noopener noreferrer'}>
|
||||
|
||||
@ -47,6 +47,27 @@ function Get-RegistryCredentialsFromEnv {
|
||||
return @{ User = $parts[0]; Password = $parts[1] }
|
||||
}
|
||||
|
||||
function Set-EnvVersionValue {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$FilePath,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Version
|
||||
)
|
||||
|
||||
$content = Get-Content -LiteralPath $FilePath -Raw
|
||||
if ($content -match '(?m)^\s*VITE_APP_VERSION\s*=') {
|
||||
$content = $content -replace '(?m)^\s*VITE_APP_VERSION\s*=.*$', "VITE_APP_VERSION=$Version"
|
||||
}
|
||||
else {
|
||||
$separator = if ($content -match "(\r?\n)$") { '' } else { [Environment]::NewLine }
|
||||
$content = "$content${separator}VITE_APP_VERSION=$Version"
|
||||
}
|
||||
|
||||
Set-Content -LiteralPath $FilePath -Value $content -NoNewline
|
||||
}
|
||||
|
||||
function Invoke-Plugin {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
@ -137,29 +158,69 @@ function Invoke-Plugin {
|
||||
$service = [string]$img.service
|
||||
$baseName = "$registryUrl/$($pluginSettings.projectName)/$service"
|
||||
|
||||
$primaryRef = "${baseName}:$($imageTags[0])"
|
||||
Write-Log -Level "STEP" -Message "Building $primaryRef ..."
|
||||
docker build -t $primaryRef -f $dockerfilePath $contextPath
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Docker build failed for $primaryRef"
|
||||
}
|
||||
$versionEnvFiles = @()
|
||||
if ($img.PSObject.Properties.Name -contains 'versionEnvFiles' -and $null -ne $img.versionEnvFiles) {
|
||||
foreach ($relativeEnvFile in @($img.versionEnvFiles)) {
|
||||
if ([string]::IsNullOrWhiteSpace([string]$relativeEnvFile)) {
|
||||
continue
|
||||
}
|
||||
|
||||
Write-Log -Level "STEP" -Message "Pushing $primaryRef ..."
|
||||
docker push $primaryRef
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Docker push failed for $primaryRef"
|
||||
}
|
||||
$envFilePath = [System.IO.Path]::GetFullPath((Join-Path $contextPath ([string]$relativeEnvFile)))
|
||||
if (-not (Test-Path -LiteralPath $envFilePath -PathType Leaf)) {
|
||||
throw "Configured versionEnvFiles entry not found: $envFilePath"
|
||||
}
|
||||
|
||||
for ($ti = 1; $ti -lt $imageTags.Count; $ti++) {
|
||||
$aliasRef = "${baseName}:$($imageTags[$ti])"
|
||||
Write-Log -Level "STEP" -Message "Tagging and pushing $aliasRef ..."
|
||||
docker tag $primaryRef $aliasRef
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Docker tag failed: $primaryRef -> $aliasRef"
|
||||
$backupPath = "$envFilePath.repoutils.bak"
|
||||
Copy-Item -LiteralPath $envFilePath -Destination $backupPath -Force
|
||||
$versionEnvFiles += [pscustomobject]@{
|
||||
FilePath = $envFilePath
|
||||
BackupPath = $backupPath
|
||||
}
|
||||
}
|
||||
docker push $aliasRef
|
||||
}
|
||||
|
||||
try {
|
||||
foreach ($envFile in $versionEnvFiles) {
|
||||
Write-Log -Level "INFO" -Message "Temporarily setting VITE_APP_VERSION=$bareVersion in $($envFile.FilePath)"
|
||||
Set-EnvVersionValue -FilePath $envFile.FilePath -Version $bareVersion
|
||||
}
|
||||
|
||||
$primaryRef = "${baseName}:$($imageTags[0])"
|
||||
Write-Log -Level "STEP" -Message "Building $primaryRef ..."
|
||||
docker build -t $primaryRef -f $dockerfilePath $contextPath
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Docker push failed for $aliasRef"
|
||||
throw "Docker build failed for $primaryRef"
|
||||
}
|
||||
|
||||
Write-Log -Level "STEP" -Message "Pushing $primaryRef ..."
|
||||
docker push $primaryRef
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Docker push failed for $primaryRef"
|
||||
}
|
||||
|
||||
for ($ti = 1; $ti -lt $imageTags.Count; $ti++) {
|
||||
$aliasRef = "${baseName}:$($imageTags[$ti])"
|
||||
Write-Log -Level "STEP" -Message "Tagging and pushing $aliasRef ..."
|
||||
docker tag $primaryRef $aliasRef
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Docker tag failed: $primaryRef -> $aliasRef"
|
||||
}
|
||||
docker push $aliasRef
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Docker push failed for $aliasRef"
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
foreach ($envFile in $versionEnvFiles) {
|
||||
if (Test-Path -LiteralPath $envFile.BackupPath -PathType Leaf) {
|
||||
Move-Item -LiteralPath $envFile.BackupPath -Destination $envFile.FilePath -Force
|
||||
}
|
||||
}
|
||||
foreach ($envFile in $versionEnvFiles) {
|
||||
if (Test-Path -LiteralPath $envFile.BackupPath -PathType Leaf) {
|
||||
Remove-Item -LiteralPath $envFile.BackupPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ Configure this plugin **immediately before** `DockerPush`, `HelmPush`, `GitHub`,
|
||||
|
||||
## Helm charts in git
|
||||
|
||||
Commit `Chart.yaml` with placeholder `version` and `appVersion` (for example `0.0.0`) so `helm lint` stays valid. `HelmPush` temporarily replaces both with the release semver (same as the git tag / `DotNetReleaseVersion`) before packaging, then restores the file. Image tags for `DockerPush` come from the engine context, not from the chart file in the repo.
|
||||
Commit `Chart.yaml` with placeholder `version` and `appVersion` (for example `0.0.0`) so `helm lint` stays valid. `HelmPush` temporarily replaces both with the release semver (same as the git tag / `DotNetReleaseVersion`) before packaging, then restores the file. `DockerPush` image tags come from the engine context (not from chart files), and you can optionally set per-image `versionEnvFiles` to temporarily write `VITE_APP_VERSION={shared.version}` into frontend `.env` files during build, with automatic restore after push.
|
||||
|
||||
This repository uses `src/helm/`. For a minimal scaffold chart, see **maksit-repoutils** `charts/my-service/`.
|
||||
|
||||
|
||||
@ -58,7 +58,11 @@
|
||||
},
|
||||
{
|
||||
"service": "client",
|
||||
"dockerfile": "MaksIT.WebUI/Dockerfile.prod"
|
||||
"dockerfile": "MaksIT.WebUI/Dockerfile.prod",
|
||||
"versionEnvFiles": [
|
||||
"MaksIT.WebUI/.env",
|
||||
"MaksIT.WebUI/.env.prod"
|
||||
]
|
||||
},
|
||||
{
|
||||
"service": "reverseproxy",
|
||||
@ -92,6 +96,7 @@
|
||||
"projectName": "DockerPush: image path segment: registryUrl/projectName/service:tag.",
|
||||
"contextPath": "DockerPush: directory containing MaksIT.CertsUI and MaksIT.WebUI (repo src/).",
|
||||
"images": "Dockerfile paths are relative to contextPath.",
|
||||
"versionEnvFiles": "DockerPush image option: array of files (relative to contextPath) where VITE_APP_VERSION is temporarily set to shared.version before docker build, then restored after build/push.",
|
||||
"chartPath": "HelmPush: directory containing Chart.yaml. Keep version/appVersion as placeholders in git (e.g. 0.0.0); HelmPush overwrites with bare semver from DotNetReleaseVersion (shared.version, e.g. 3.3.4, no v) before package/push; falls back to stripping v from shared.tag if version is missing.",
|
||||
"ociRepository": "HelmPush: OCI registry URL for helm push (e.g. oci://cr.maks-it.com/charts).",
|
||||
"pushLatest": "Docker: also push :latest (images also get bare :3.3.4 and :v3.3.4 from DotNetReleaseVersion). Helm: oras copy chart to :latest (requires oras on PATH).",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user