first commit ish
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user