first commit ish

This commit is contained in:
Nils Blomgren
2026-06-04 08:51:22 +02:00
parent e0f967482e
commit 658f15d086
320 changed files with 307404 additions and 1187 deletions

View File

@@ -1,9 +1,11 @@
@using EntKube.Web.Data
@using EntKube.Web.Services
@using Microsoft.EntityFrameworkCore
@using EntKube.Web.Components.Pages.Shared
@inject VaultService VaultService
@inject TenantService TenantService
@inject StorageService StorageService
@inject CnpgService CnpgService
<div class="d-flex align-items-center mb-2">
<i class="bi bi-shield-lock fs-4 me-2 text-primary"></i>
@@ -39,6 +41,12 @@ else
<button class="btn btn-sm @(scope == "storage" ? "btn-primary" : "btn-outline-primary")" @onclick='() => SwitchScope("storage")'>
<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
</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
</button>
</div>
</div>
@@ -125,6 +133,43 @@ else
}
}
else if (scope == "cnpg")
{
@if (cnpgClusterOptions is not null && cnpgClusterOptions.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="selectedCnpgClusterId" @bind:after="LoadSecrets">
<option value="@Guid.Empty">Select a CNPG cluster...</option>
@foreach ((string label, Guid id) in cnpgClusterOptions)
{
<option value="@id">@label</option>
}
</select>
</div>
</div>
</div>
}
else
{
<div class="text-center py-4">
<i class="bi bi-database text-muted" style="font-size: 2rem;"></i>
<p class="text-muted small mt-2 mb-0">No CNPG clusters found for this tenant.</p>
</div>
}
}
@* --- Docker Registries Panel --- *@
@if (scope == "docker")
{
<DockerRegistriesPanel
TenantId="TenantId"
AppId="Guid.Empty"
IsAdmin="true" />
}
@* --- Secrets Panel --- *@
@if (HasSelection())
{
@@ -132,6 +177,14 @@ else
<div class="card-header bg-white d-flex align-items-center py-2">
<i class="bi bi-lock me-2 text-primary"></i>
<strong>Secrets</strong>
@if (scope == "app" && secrets?.Any(s => s.SyncToKubernetes) == true)
{
<button class="btn btn-sm btn-outline-success ms-auto" @onclick="SyncAllAppSecrets" disabled="@syncingSecrets">
@if (syncingSecrets) { <span class="spinner-border spinner-border-sm me-1"></span> }
else { <i class="bi bi-cloud-arrow-up me-1"></i> }
Sync All to K8s
</button>
}
</div>
<div class="card-body">
@* Add secret form *@
@@ -193,6 +246,10 @@ else
<thead class="table-light">
<tr>
<th>Name</th>
@if (scope == "cnpg")
{
<th>Scope</th>
}
<th>K8s Sync</th>
<th>K8s Secret</th>
<th>Namespace</th>
@@ -205,6 +262,19 @@ else
{
<tr>
<td><code>@secret.Name</code></td>
@if (scope == "cnpg")
{
<td>
@if (secret.CnpgDatabase is not null)
{
<span class="badge bg-info-subtle text-info border border-info-subtle">@secret.CnpgDatabase.Name</span>
}
else
{
<span class="badge bg-secondary-subtle text-secondary border border-secondary-subtle">cluster</span>
}
</td>
}
<td>
@if (secret.SyncToKubernetes)
{
@@ -278,11 +348,23 @@ else
<tr>
<td colspan="6" class="bg-light">
<div class="row g-2 p-2 align-items-center">
<div class="col-md-4">
@if (scope == "app" && clusterOptions is not null)
{
<div class="col-md-3">
<select class="form-select form-select-sm" @bind="syncClusterId">
<option value="@Guid.Empty">Target cluster…</option>
@foreach ((string label, Guid id) in clusterOptions)
{
<option value="@id">@label</option>
}
</select>
</div>
}
<div class="col-md-3">
<input type="text" class="form-control form-control-sm"
placeholder="K8s Secret name (e.g. my-app-secrets)" @bind="syncSecretName" />
</div>
<div class="col-md-4">
<div class="col-md-3">
<input type="text" class="form-control form-control-sm"
placeholder="Namespace (e.g. default)" @bind="syncNamespace" />
</div>
@@ -303,6 +385,10 @@ else
</table>
</div>
}
@if (syncOutput is not null)
{
<div class="mt-3 p-2 bg-dark text-white rounded font-monospace small" style="white-space: pre-wrap; max-height: 200px; overflow-y: auto;">@syncOutput</div>
}
</div>
</div>
}
@@ -326,6 +412,13 @@ else
private List<(string Label, Guid Id)>? storageLinkOptions;
private Guid selectedStorageLinkId;
// CNPG cluster scope
private List<(string Label, Guid Id)>? cnpgClusterOptions;
private Guid selectedCnpgClusterId;
// Cluster options (used in app-scope sync config)
private List<(string Label, Guid Id)>? clusterOptions;
// Secrets
private List<VaultSecret>? secrets;
private string newSecretName = "";
@@ -335,6 +428,11 @@ else
private Guid? syncConfigSecretId;
private string syncSecretName = "";
private string syncNamespace = "";
private Guid syncClusterId;
// Sync-to-K8s state (app scope)
private bool syncingSecrets;
private string? syncOutput;
// Reveal/Edit state
private Guid? revealedSecretId;
@@ -376,6 +474,7 @@ else
}
List<KubernetesCluster> clusters = await TenantService.GetClustersAsync(TenantId);
clusterOptions = clusters.Select(c => (c.Name, c.Id)).ToList();
componentOptions = [];
foreach (KubernetesCluster cluster in clusters)
@@ -394,6 +493,13 @@ else
storageLinkOptions = links
.Select(l => ($"{l.Environment.Name} / {l.Name} ({l.Provider})", l.Id))
.ToList();
// Load CNPG clusters for the database secrets scope.
List<CnpgCluster> cnpgClusters = await CnpgService.GetClustersAsync(TenantId);
cnpgClusterOptions = cnpgClusters
.Select(c => ($"{c.KubernetesCluster.Name} / {c.Name}", c.Id))
.ToList();
}
private void SwitchScope(string newScope)
@@ -403,15 +509,20 @@ else
selectedAppId = Guid.Empty;
selectedComponentId = Guid.Empty;
selectedStorageLinkId = Guid.Empty;
selectedCnpgClusterId = Guid.Empty;
errorMessage = null;
successMessage = null;
syncClusterId = Guid.Empty;
syncOutput = null;
// Docker registries panel manages its own state; no reset needed here.
}
private bool HasSelection()
{
return (scope == "app" && selectedAppId != Guid.Empty)
|| (scope == "component" && selectedComponentId != Guid.Empty)
|| (scope == "storage" && selectedStorageLinkId != Guid.Empty);
|| (scope == "storage" && selectedStorageLinkId != Guid.Empty)
|| (scope == "cnpg" && selectedCnpgClusterId != Guid.Empty);
}
private async Task LoadSecrets()
@@ -431,6 +542,10 @@ else
{
secrets = await VaultService.GetStorageLinkSecretsAsync(TenantId, selectedStorageLinkId);
}
else if (scope == "cnpg" && selectedCnpgClusterId != Guid.Empty)
{
secrets = await VaultService.GetAllCnpgSecretsForClusterAsync(TenantId, selectedCnpgClusterId);
}
else
{
secrets = null;
@@ -456,6 +571,10 @@ else
{
await VaultService.SetStorageLinkSecretAsync(TenantId, selectedStorageLinkId, newSecretName.Trim(), newSecretValue.Trim());
}
else if (scope == "cnpg")
{
await VaultService.SetCnpgClusterSecretAsync(TenantId, selectedCnpgClusterId, newSecretName.Trim(), newSecretValue.Trim());
}
successMessage = $"Secret '{newSecretName.Trim()}' saved.";
newSecretName = "";
@@ -555,6 +674,7 @@ else
syncConfigSecretId = secret.Id;
syncSecretName = secret.KubernetesSecretName ?? "";
syncNamespace = secret.KubernetesNamespace ?? "";
syncClusterId = secret.KubernetesClusterId ?? Guid.Empty;
}
}
@@ -571,13 +691,34 @@ else
return;
}
Guid? clusterId = (scope == "app" && syncClusterId != Guid.Empty) ? syncClusterId : (Guid?)null;
await VaultService.ConfigureKubernetesSyncAsync(
syncConfigSecretId.Value,
syncEnabled: true,
secretName: syncSecretName.Trim(),
ns: syncNamespace.Trim());
ns: syncNamespace.Trim(),
clusterId: clusterId);
syncConfigSecretId = null;
await LoadSecrets();
}
private async Task SyncAllAppSecrets()
{
if (selectedAppId == Guid.Empty) return;
syncingSecrets = true;
syncOutput = null;
try
{
HelmExecutionResult result = await VaultService.SyncAppSecretsToKubernetesAsync(TenantId, selectedAppId);
syncOutput = result.Output;
}
finally
{
syncingSecrets = false;
}
}
}