server management
Some checks failed
Build and Deploy / Build and push image (push) Failing after 5m50s
Build and Deploy / Deploy to server (push) Has been skipped

This commit is contained in:
Nils Blomgren
2026-06-11 15:09:31 +02:00
parent 343de77810
commit 5da40698ad
152 changed files with 121747 additions and 3884 deletions

View File

@@ -6,6 +6,8 @@
@inject TenantService TenantService
@inject StorageService StorageService
@inject CnpgService CnpgService
@inject MongoService MongoService
@inject RegisteredPostgresService RegisteredPostgresService
<div class="d-flex align-items-center mb-2">
<i class="bi bi-shield-lock fs-4 me-2 text-primary"></i>
@@ -42,7 +44,13 @@ else
<i class="bi bi-hdd me-1"></i>Storage Secrets
</button>
<button class="btn btn-sm @(scope == "cnpg" ? "btn-primary" : "btn-outline-primary")" @onclick='() => SwitchScope("cnpg")'>
<i class="bi bi-database me-1"></i>Database Secrets
<i class="bi bi-database me-1"></i>PostgreSQL
</button>
<button class="btn btn-sm @(scope == "mongodb" ? "btn-primary" : "btn-outline-primary")" @onclick='() => SwitchScope("mongodb")'>
<i class="bi bi-database me-1"></i>MongoDB
</button>
<button class="btn btn-sm @(scope == "regpostgres" ? "btn-primary" : "btn-outline-primary")" @onclick='() => SwitchScope("regpostgres")'>
<i class="bi bi-database me-1"></i>Registered DB
</button>
<button class="btn btn-sm @(scope == "docker" ? "btn-primary" : "btn-outline-primary")" @onclick='() => SwitchScope("docker")'>
<i class="bi bi-box-seam me-1"></i>Docker Registries
@@ -153,6 +161,58 @@ else
}
}
else if (scope == "mongodb")
{
@if (mongoClusterOptions is not null && mongoClusterOptions.Count > 0)
{
<div class="card border-0 shadow-sm mb-3">
<div class="card-body py-2">
<div class="input-group">
<span class="input-group-text bg-transparent"><i class="bi bi-database"></i></span>
<select class="form-select" @bind="selectedMongoClusterId" @bind:after="LoadSecrets">
<option value="@Guid.Empty">Select a MongoDB cluster...</option>
@foreach ((string label, Guid id) in mongoClusterOptions)
{
<option value="@id">@label</option>
}
</select>
</div>
</div>
</div>
}
else
{
<EmptyState Icon="bi-database"
Message="No MongoDB clusters found for this tenant." />
}
}
else if (scope == "regpostgres")
{
@if (regPostgresOptions is not null && regPostgresOptions.Count > 0)
{
<div class="card border-0 shadow-sm mb-3">
<div class="card-body py-2">
<div class="input-group">
<span class="input-group-text bg-transparent"><i class="bi bi-database"></i></span>
<select class="form-select" @bind="selectedRegPostgresInstanceId" @bind:after="LoadSecrets">
<option value="@Guid.Empty">Select a registered Postgres instance...</option>
@foreach ((string label, Guid id) in regPostgresOptions)
{
<option value="@id">@label</option>
}
</select>
</div>
</div>
</div>
}
else
{
<EmptyState Icon="bi-database"
Message="No registered Postgres instances found for this tenant." />
}
}
@* --- Docker Registries Panel --- *@
@if (scope == "docker")
{
@@ -179,7 +239,9 @@ else
}
</div>
<div class="card-body">
@* Add secret form *@
@* Add secret form — hidden for read-only DB scopes *@
@if (scope is not "mongodb" and not "regpostgres")
{
<div class="row g-2 mb-3">
<div class="col-md-4">
<div class="input-group input-group-sm">
@@ -202,6 +264,7 @@ else
</button>
</div>
</div>
}
@if (!string.IsNullOrEmpty(errorMessage))
{
@@ -231,9 +294,9 @@ else
<thead class="table-light">
<tr>
<th>Name</th>
@if (scope == "cnpg")
@if (scope is "cnpg" or "mongodb" or "regpostgres")
{
<th>Scope</th>
<th>Database</th>
}
<th>K8s Sync</th>
<th>K8s Secret</th>
@@ -247,13 +310,21 @@ else
{
<tr>
<td><code>@secret.Name</code></td>
@if (scope == "cnpg")
@if (scope is "cnpg" or "mongodb" or "regpostgres")
{
<td>
@if (secret.CnpgDatabase is not null)
{
<span class="badge bg-info-subtle text-info border border-info-subtle">@secret.CnpgDatabase.Name</span>
}
else if (secret.MongoDatabase is not null)
{
<span class="badge bg-success-subtle text-success border border-success-subtle">@secret.MongoDatabase.Name</span>
}
else if (secret.RegisteredPostgresDatabase is not null)
{
<span class="badge bg-primary-subtle text-primary border border-primary-subtle">@secret.RegisteredPostgresDatabase.Name</span>
}
else
{
<span class="badge bg-secondary-subtle text-secondary border border-secondary-subtle">cluster</span>
@@ -281,6 +352,9 @@ else
<button class="btn btn-sm btn-outline-warning me-1" @onclick="() => StartEdit(secret)" title="Edit value">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-secondary me-1" @onclick="() => ToggleHistory(secret.Id)" title="Version history">
<i class="bi bi-clock-history"></i>
</button>
<button class="btn btn-sm btn-outline-info me-1" @onclick="() => ToggleSync(secret)"
title="@(secret.SyncToKubernetes ? "Disable K8s sync" : "Enable K8s sync")">
<i class="bi @(secret.SyncToKubernetes ? "bi-cloud-slash" : "bi-cloud-arrow-up")"></i>
@@ -328,6 +402,81 @@ else
</tr>
}
@* Version history row *@
@if (historySecretId == secret.Id)
{
<tr>
<td colspan="7" class="bg-light p-0">
<div class="p-3">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-clock-history me-2 text-secondary"></i>
<strong class="small">Version History</strong>
<span class="badge bg-secondary ms-2">@(secretVersions?.Count ?? 0)</span>
</div>
@if (secretVersions is null)
{
<div class="spinner-border spinner-border-sm text-secondary" role="status"></div>
}
else if (secretVersions.Count == 0)
{
<p class="text-muted small mb-0">No previous versions recorded.</p>
}
else
{
<table class="table table-sm table-bordered mb-0">
<thead class="table-secondary">
<tr>
<th style="width: 60px;">#</th>
<th>Set by</th>
<th>Date</th>
<th class="text-end" style="width: 160px;">Actions</th>
</tr>
</thead>
<tbody>
@foreach (VaultSecretVersion ver in secretVersions)
{
<tr>
<td><span class="badge bg-secondary">v@ver.VersionNumber</span></td>
<td><small class="text-muted">@(ver.CreatedBy ?? "—")</small></td>
<td><small class="text-muted">@ver.CreatedAt.ToString("MMM d, HH:mm")</small></td>
<td class="text-end">
<button class="btn btn-xs btn-outline-secondary me-1"
@onclick="() => RevealVersion(secret.Id, ver)"
title="@(revealedVersionId == ver.Id ? "Hide" : "Reveal value")">
<i class="bi @(revealedVersionId == ver.Id ? "bi-eye-slash" : "bi-eye")"></i>
</button>
<button class="btn btn-xs btn-outline-warning"
@onclick="() => RollbackToVersion(secret.Id, ver.Id)"
title="Restore this value">
<i class="bi bi-arrow-counterclockwise me-1"></i>Restore
</button>
</td>
</tr>
@if (revealedVersionId == ver.Id)
{
<tr class="table-light">
<td colspan="4" class="px-2 py-1">
<span class="text-muted small me-2">Value:</span>
@if (versionRevealLoading)
{
<span class="spinner-border spinner-border-sm"></span>
}
else
{
<code class="user-select-all">@revealedVersionValue</code>
}
</td>
</tr>
}
}
</tbody>
</table>
}
</div>
</td>
</tr>
}
@if (syncConfigSecretId == secret.Id)
{
<tr>
@@ -402,6 +551,14 @@ else
private List<(string Label, Guid Id)>? cnpgClusterOptions;
private Guid selectedCnpgClusterId;
// MongoDB cluster scope
private List<(string Label, Guid Id)>? mongoClusterOptions;
private Guid selectedMongoClusterId;
// Registered Postgres scope
private List<(string Label, Guid Id)>? regPostgresOptions;
private Guid selectedRegPostgresInstanceId;
// Cluster options (used in app-scope sync config)
private List<(string Label, Guid Id)>? clusterOptions;
@@ -426,6 +583,13 @@ else
private Guid? editSecretId;
private string editSecretValue = "";
// Version history state
private Guid? historySecretId;
private List<VaultSecretVersion>? secretVersions;
private Guid? revealedVersionId;
private string? revealedVersionValue;
private bool versionRevealLoading;
protected override async Task OnInitializedAsync()
{
SecretVault? vault = await VaultService.GetVaultAsync(TenantId);
@@ -486,6 +650,18 @@ else
cnpgClusterOptions = cnpgClusters
.Select(c => ($"{c.KubernetesCluster.Name} / {c.Name}", c.Id))
.ToList();
// MongoDB clusters
List<MongoCluster> mongoClusters = await MongoService.GetClustersAsync(TenantId);
mongoClusterOptions = mongoClusters
.Select(c => ($"{c.KubernetesCluster.Name} / {c.Name}", c.Id))
.ToList();
// Registered Postgres instances
List<RegisteredPostgresInstance> regInstances = await RegisteredPostgresService.GetInstancesAsync(TenantId);
regPostgresOptions = regInstances
.Select(i => (i.Name, i.Id))
.ToList();
}
private void SwitchScope(string newScope)
@@ -496,6 +672,8 @@ else
selectedComponentId = Guid.Empty;
selectedStorageLinkId = Guid.Empty;
selectedCnpgClusterId = Guid.Empty;
selectedMongoClusterId = Guid.Empty;
selectedRegPostgresInstanceId = Guid.Empty;
errorMessage = null;
successMessage = null;
syncClusterId = Guid.Empty;
@@ -508,7 +686,9 @@ else
return (scope == "app" && selectedAppId != Guid.Empty)
|| (scope == "component" && selectedComponentId != Guid.Empty)
|| (scope == "storage" && selectedStorageLinkId != Guid.Empty)
|| (scope == "cnpg" && selectedCnpgClusterId != Guid.Empty);
|| (scope == "cnpg" && selectedCnpgClusterId != Guid.Empty)
|| (scope == "mongodb" && selectedMongoClusterId != Guid.Empty)
|| (scope == "regpostgres" && selectedRegPostgresInstanceId != Guid.Empty);
}
private async Task LoadSecrets()
@@ -532,6 +712,14 @@ else
{
secrets = await VaultService.GetAllCnpgSecretsForClusterAsync(TenantId, selectedCnpgClusterId);
}
else if (scope == "mongodb" && selectedMongoClusterId != Guid.Empty)
{
secrets = await VaultService.GetAllMongoSecretsForClusterAsync(TenantId, selectedMongoClusterId);
}
else if (scope == "regpostgres" && selectedRegPostgresInstanceId != Guid.Empty)
{
secrets = await VaultService.GetAllRegisteredPostgresSecretsForInstanceAsync(TenantId, selectedRegPostgresInstanceId);
}
else
{
secrets = null;
@@ -707,4 +895,58 @@ else
syncingSecrets = false;
}
}
private async Task ToggleHistory(Guid secretId)
{
if (historySecretId == secretId)
{
historySecretId = null;
secretVersions = null;
revealedVersionId = null;
revealedVersionValue = null;
return;
}
historySecretId = secretId;
revealedVersionId = null;
revealedVersionValue = null;
secretVersions = null;
secretVersions = await VaultService.GetSecretVersionsAsync(secretId);
}
private async Task RevealVersion(Guid secretId, VaultSecretVersion version)
{
if (revealedVersionId == version.Id)
{
revealedVersionId = null;
revealedVersionValue = null;
return;
}
versionRevealLoading = true;
revealedVersionId = version.Id;
revealedVersionValue = null;
revealedVersionValue = await VaultService.GetSecretVersionValueAsync(secretId, version.Id);
versionRevealLoading = false;
}
private async Task RollbackToVersion(Guid secretId, Guid versionId)
{
errorMessage = null;
bool ok = await VaultService.RollbackToVersionAsync(secretId, versionId);
if (ok)
{
successMessage = "Secret restored to selected version.";
historySecretId = null;
secretVersions = null;
revealedVersionId = null;
revealedVersionValue = null;
await LoadSecrets();
}
else
{
errorMessage = "Failed to restore version.";
}
}
}