gateway api fixes
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@inject TenantService TenantService
|
||||
@inject DeploymentService DeploymentService
|
||||
@inject CnpgService CnpgService
|
||||
|
||||
@* ═══════════════════════════════════════════════════════════════════
|
||||
App Detail — a full-page view for a single application.
|
||||
@@ -226,6 +227,16 @@
|
||||
<i class="bi bi-diagram-3 me-1"></i>Resources
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link @(deploymentSection == "databases" ? "active" : "")"
|
||||
@onclick="OpenDatabasesTab">
|
||||
<i class="bi bi-database me-1"></i>Databases
|
||||
@if (databaseBindings is not null && databaseBindings.Count > 0)
|
||||
{
|
||||
<span class="badge bg-primary ms-1">@databaseBindings.Count</span>
|
||||
}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@* ── Manifests sub-tab ── *@
|
||||
@@ -393,6 +404,123 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ── Databases sub-tab ── *@
|
||||
@if (deploymentSection == "databases")
|
||||
{
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white py-2 d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<i class="bi bi-database me-2 text-primary"></i>
|
||||
<strong>Database Bindings</strong>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-primary" @onclick="() => showAddBinding = !showAddBinding">
|
||||
<i class="bi bi-plus me-1"></i>Attach Database
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (!string.IsNullOrEmpty(bindingError))
|
||||
{
|
||||
<div class="alert alert-danger py-1 small mb-2">@bindingError</div>
|
||||
}
|
||||
|
||||
@* --- Add binding form --- *@
|
||||
@if (showAddBinding)
|
||||
{
|
||||
<div class="card border-primary mb-3">
|
||||
<div class="card-body">
|
||||
<h6 class="mb-3"><i class="bi bi-plus-circle me-1"></i>Attach a PostgreSQL Database</h6>
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small">Database</label>
|
||||
<select class="form-select form-select-sm" @bind="newBindingDatabaseId">
|
||||
<option value="">Select database...</option>
|
||||
@if (availableCnpgDatabases is not null)
|
||||
{
|
||||
@foreach ((CnpgCluster cluster, CnpgDatabase database) in availableCnpgDatabases)
|
||||
{
|
||||
<option value="@database.Id">@cluster.Name / @database.Name</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small">Secret name in app namespace</label>
|
||||
<input class="form-control form-control-sm" @bind="newBindingSecretName"
|
||||
placeholder="e.g. my-app-db" />
|
||||
<div class="form-text">The K8s Secret created in <code>@selectedDeployment?.Namespace</code>.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-1">
|
||||
<button class="btn btn-sm btn-primary" @onclick="AddDatabaseBinding"
|
||||
disabled="@(newBindingDatabaseId == Guid.Empty || string.IsNullOrWhiteSpace(newBindingSecretName))">
|
||||
Attach & Sync
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" @onclick="() => showAddBinding = false">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* --- Binding list --- *@
|
||||
@if (databaseBindings is null)
|
||||
{
|
||||
<div class="text-center py-3">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
|
||||
</div>
|
||||
}
|
||||
else if (databaseBindings.Count == 0)
|
||||
{
|
||||
<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 databases attached. Attach a database to have its credentials automatically synced to this deployment's namespace.</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="small">Database</th>
|
||||
<th class="small">Secret name</th>
|
||||
<th class="small">Last synced</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (DatabaseBinding binding in databaseBindings)
|
||||
{
|
||||
<tr>
|
||||
<td class="small">
|
||||
@if (binding.CnpgDatabase is not null)
|
||||
{
|
||||
<i class="bi bi-database me-1 text-primary"></i>
|
||||
@binding.CnpgDatabase.CnpgCluster.Name<span class="text-muted"> / </span>@binding.CnpgDatabase.Name
|
||||
}
|
||||
else if (binding.MongoDatabase is not null)
|
||||
{
|
||||
<i class="bi bi-database me-1 text-success"></i>
|
||||
@binding.MongoDatabase.MongoCluster.Name<span class="text-muted"> / </span>@binding.MongoDatabase.Name
|
||||
}
|
||||
</td>
|
||||
<td><code class="small">@binding.KubernetesSecretName</code></td>
|
||||
<td class="small text-muted">
|
||||
@(binding.LastSyncedAt.HasValue ? binding.LastSyncedAt.Value.ToString("yyyy-MM-dd HH:mm") : "Never")
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-link btn-sm text-danger p-0" title="Detach"
|
||||
@onclick="() => RemoveDatabaseBinding(binding.Id)">
|
||||
<i class="bi bi-x-circle" style="font-size:0.85rem;"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -602,6 +730,14 @@
|
||||
// Resource tree
|
||||
private List<DeploymentResource>? resourceTree;
|
||||
|
||||
// Database bindings
|
||||
private List<DatabaseBinding>? databaseBindings;
|
||||
private List<(CnpgCluster Cluster, CnpgDatabase Database)>? availableCnpgDatabases;
|
||||
private bool showAddBinding;
|
||||
private Guid newBindingDatabaseId;
|
||||
private string newBindingSecretName = "";
|
||||
private string? bindingError;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadEnvironments();
|
||||
@@ -790,6 +926,80 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ──────── Database bindings ────────
|
||||
|
||||
private async Task OpenDatabasesTab()
|
||||
{
|
||||
deploymentSection = "databases";
|
||||
|
||||
if (selectedDeployment is not null)
|
||||
{
|
||||
databaseBindings = await CnpgService.GetDatabaseBindingsAsync(selectedDeployment.Id);
|
||||
|
||||
if (availableCnpgDatabases is null)
|
||||
{
|
||||
List<CnpgCluster> clusters = await CnpgService.GetClustersAsync(TenantId);
|
||||
availableCnpgDatabases = clusters
|
||||
.SelectMany(c => c.Databases.Select(d => (c, d)))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddDatabaseBinding()
|
||||
{
|
||||
if (selectedDeployment is null || newBindingDatabaseId == Guid.Empty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bindingError = null;
|
||||
|
||||
try
|
||||
{
|
||||
DatabaseBinding binding = await CnpgService.AddCnpgDatabaseBindingAsync(
|
||||
selectedDeployment.Id, newBindingDatabaseId, newBindingSecretName);
|
||||
|
||||
// Find the cluster that owns this database so we can call sync.
|
||||
(CnpgCluster cluster, CnpgDatabase _)? match = availableCnpgDatabases?
|
||||
.FirstOrDefault(x => x.Database.Id == newBindingDatabaseId);
|
||||
|
||||
if (match.HasValue)
|
||||
{
|
||||
await CnpgService.SyncDatabaseCredentialsToK8sAsync(
|
||||
TenantId, match.Value.cluster.Id, newBindingDatabaseId);
|
||||
}
|
||||
|
||||
showAddBinding = false;
|
||||
newBindingDatabaseId = Guid.Empty;
|
||||
newBindingSecretName = "";
|
||||
databaseBindings = await CnpgService.GetDatabaseBindingsAsync(selectedDeployment.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
bindingError = $"Failed to attach database: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveDatabaseBinding(Guid bindingId)
|
||||
{
|
||||
bindingError = null;
|
||||
|
||||
try
|
||||
{
|
||||
await CnpgService.RemoveDatabaseBindingAsync(bindingId);
|
||||
|
||||
if (selectedDeployment is not null)
|
||||
{
|
||||
databaseBindings = await CnpgService.GetDatabaseBindingsAsync(selectedDeployment.Id);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
bindingError = $"Failed to detach database: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
// ──────── Rendering helpers ────────
|
||||
|
||||
private RenderFragment GetTypeBadge(DeploymentType type) => type switch
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1703
src/EntKube.Web/Components/Pages/Tenants/IdentityTab.razor
Normal file
1703
src/EntKube.Web/Components/Pages/Tenants/IdentityTab.razor
Normal file
File diff suppressed because it is too large
Load Diff
@@ -172,6 +172,19 @@ else
|
||||
title="Manage bucket (CORS, Policy)">
|
||||
<i class="bi bi-gear"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-info border-0 py-0"
|
||||
@onclick="() => BrowseBuckets(link)"
|
||||
disabled="@(browsingLinkId.HasValue)"
|
||||
title="Browse all buckets accessible with these credentials">
|
||||
@if (browsingLinkId == link.Id)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="bi bi-list-ul"></i>
|
||||
}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger border-0 py-0"
|
||||
@onclick="() => DeleteCleuraS3Bucket(link.Id)"
|
||||
disabled="@deletingLinkId.HasValue"
|
||||
@@ -327,6 +340,82 @@ else
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ── Browse Buckets Panel ── *@
|
||||
@if (browsedLink is not null)
|
||||
{
|
||||
<div class="card shadow-sm mb-4 border-secondary">
|
||||
<div class="card-header bg-secondary bg-opacity-10 d-flex align-items-center justify-content-between">
|
||||
<h6 class="mb-0">
|
||||
<i class="bi bi-list-ul me-2"></i>Buckets accessible via <strong>@browsedLink.Name</strong>
|
||||
</h6>
|
||||
<button class="btn btn-sm btn-outline-secondary border-0" @onclick="CloseBrowseBuckets">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (!string.IsNullOrEmpty(browseError))
|
||||
{
|
||||
<div class="alert alert-danger small py-2">
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>@browseError
|
||||
</div>
|
||||
}
|
||||
else if (browsedBuckets is null)
|
||||
{
|
||||
<div class="text-center py-3">
|
||||
<div class="spinner-border spinner-border-sm text-secondary"></div>
|
||||
<span class="ms-2 text-muted small">Loading buckets...</span>
|
||||
</div>
|
||||
}
|
||||
else if (browsedBuckets.Count == 0)
|
||||
{
|
||||
<p class="text-muted small mb-0">No buckets found under this project.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="small">Bucket Name</th>
|
||||
<th class="small">Created</th>
|
||||
<th class="small">Registered in EntKube</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (S3BucketInfo bucket in browsedBuckets)
|
||||
{
|
||||
bool registered = storageLinks.Any(l =>
|
||||
l.Provider == StorageProvider.CleuraS3 &&
|
||||
l.BucketName == bucket.Name);
|
||||
<tr>
|
||||
<td class="small font-monospace">@bucket.Name</td>
|
||||
<td class="small text-muted">
|
||||
@(bucket.CreatedAt == default ? "—" : bucket.CreatedAt.ToString("yyyy-MM-dd"))
|
||||
</td>
|
||||
<td class="small">
|
||||
@if (registered)
|
||||
{
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle">Yes</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary-subtle text-secondary border">No</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="text-muted small mt-2 mb-0">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
@browsedBuckets.Count bucket(s) accessible. Buckets not registered in EntKube were created outside the platform.
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ── Add Storage Link Form ── *@
|
||||
@if (showForm)
|
||||
{
|
||||
@@ -652,6 +741,12 @@ else
|
||||
private int corsMaxAge = 3600;
|
||||
private string bucketPolicyJson = "";
|
||||
|
||||
// Browse buckets state
|
||||
private StorageLink? browsedLink;
|
||||
private Guid? browsingLinkId;
|
||||
private List<S3BucketInfo>? browsedBuckets;
|
||||
private string? browseError;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadData();
|
||||
@@ -846,6 +941,35 @@ else
|
||||
manageSuccess = null;
|
||||
}
|
||||
|
||||
private async Task BrowseBuckets(StorageLink link)
|
||||
{
|
||||
browsingLinkId = link.Id;
|
||||
browsedLink = link;
|
||||
browsedBuckets = null;
|
||||
browseError = null;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
browsedBuckets = await StorageService.ListCleuraS3BucketsAsync(TenantId, link.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
browseError = $"Failed to list buckets: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
browsingLinkId = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseBrowseBuckets()
|
||||
{
|
||||
browsedLink = null;
|
||||
browsedBuckets = null;
|
||||
browseError = null;
|
||||
}
|
||||
|
||||
private async Task LoadCors()
|
||||
{
|
||||
if (managedLink is null)
|
||||
|
||||
@@ -63,6 +63,11 @@ else
|
||||
<i class="bi bi-bucket me-1"></i>Storage
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link @(activeTab == "identity" ? "active" : "")" @onclick='() => activeTab = "identity"'>
|
||||
<i class="bi bi-person-badge me-1"></i>Identity
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
@@ -86,6 +91,9 @@ else
|
||||
case "storage":
|
||||
<StorageTab TenantId="tenant.Id" />
|
||||
break;
|
||||
case "identity":
|
||||
<IdentityTab TenantId="tenant.Id" />
|
||||
break;
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -86,4 +86,5 @@ public class AppDeployment
|
||||
public ICollection<DeploymentManifest> Manifests { get; set; } = [];
|
||||
public ICollection<DeploymentResource> Resources { get; set; } = [];
|
||||
public ICollection<StorageBinding> StorageBindings { get; set; } = [];
|
||||
public ICollection<DatabaseBinding> DatabaseBindings { get; set; } = [];
|
||||
}
|
||||
|
||||
@@ -36,6 +36,13 @@ public class ApplicationDbContext(DbContextOptions options) : IdentityDbContext<
|
||||
public DbSet<CnpgCluster> CnpgClusters => Set<CnpgCluster>();
|
||||
public DbSet<CnpgDatabase> CnpgDatabases => Set<CnpgDatabase>();
|
||||
public DbSet<CnpgBackup> CnpgBackups => Set<CnpgBackup>();
|
||||
public DbSet<MongoCluster> MongoClusters => Set<MongoCluster>();
|
||||
public DbSet<MongoDatabase> MongoDatabases => Set<MongoDatabase>();
|
||||
public DbSet<DatabaseBinding> DatabaseBindings => Set<DatabaseBinding>();
|
||||
public DbSet<MongoBackup> MongoBackups => Set<MongoBackup>();
|
||||
public DbSet<KeycloakComponentConfig> KeycloakComponentConfigs => Set<KeycloakComponentConfig>();
|
||||
public DbSet<KeycloakRealm> KeycloakRealms => Set<KeycloakRealm>();
|
||||
public DbSet<KeycloakBackup> KeycloakBackups => Set<KeycloakBackup>();
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
@@ -265,6 +272,16 @@ public class ApplicationDbContext(DbContextOptions options) : IdentityDbContext<
|
||||
.WithMany()
|
||||
.HasForeignKey(s => s.CnpgDatabaseId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasOne(s => s.MongoDatabase)
|
||||
.WithMany()
|
||||
.HasForeignKey(s => s.MongoDatabaseId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasOne(s => s.MongoCluster)
|
||||
.WithMany()
|
||||
.HasForeignKey(s => s.MongoClusterId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
// ClusterComponent — a deployable unit (Helm chart, operator, etc.) on a cluster.
|
||||
@@ -476,6 +493,151 @@ public class ApplicationDbContext(DbContextOptions options) : IdentityDbContext<
|
||||
.HasForeignKey(b => b.CnpgClusterId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
builder.Entity<MongoCluster>(entity =>
|
||||
{
|
||||
entity.HasKey(c => c.Id);
|
||||
entity.Property(c => c.Name).HasMaxLength(63).IsRequired();
|
||||
entity.Property(c => c.Namespace).HasMaxLength(63).IsRequired();
|
||||
entity.Property(c => c.MongoVersion).HasMaxLength(20).IsRequired();
|
||||
entity.Property(c => c.StorageSize).HasMaxLength(20).IsRequired();
|
||||
entity.Property(c => c.BackupSchedule).HasMaxLength(100);
|
||||
|
||||
entity.HasIndex(c => new { c.KubernetesClusterId, c.Name, c.Namespace }).IsUnique();
|
||||
|
||||
entity.HasOne(c => c.Tenant)
|
||||
.WithMany()
|
||||
.HasForeignKey(c => c.TenantId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasOne(c => c.KubernetesCluster)
|
||||
.WithMany()
|
||||
.HasForeignKey(c => c.KubernetesClusterId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasOne(c => c.StorageLink)
|
||||
.WithMany()
|
||||
.HasForeignKey(c => c.StorageLinkId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
builder.Entity<MongoDatabase>(entity =>
|
||||
{
|
||||
entity.HasKey(d => d.Id);
|
||||
entity.Property(d => d.Name).HasMaxLength(63).IsRequired();
|
||||
|
||||
entity.HasIndex(d => new { d.MongoClusterId, d.Name }).IsUnique();
|
||||
|
||||
entity.HasOne(d => d.MongoCluster)
|
||||
.WithMany(c => c.Databases)
|
||||
.HasForeignKey(d => d.MongoClusterId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
builder.Entity<MongoBackup>(entity =>
|
||||
{
|
||||
entity.HasKey(b => b.Id);
|
||||
entity.Property(b => b.Name).HasMaxLength(253).IsRequired();
|
||||
|
||||
entity.HasIndex(b => new { b.MongoClusterId, b.Name }).IsUnique();
|
||||
|
||||
entity.HasOne(b => b.MongoCluster)
|
||||
.WithMany(c => c.Backups)
|
||||
.HasForeignKey(b => b.MongoClusterId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
// KeycloakComponentConfig — links a detected Keycloak ClusterComponent to its
|
||||
// backing CNPG database. DB credentials and the admin password are stored as
|
||||
// component vault secrets (ComponentId = ClusterComponentId) and synced to K8s.
|
||||
|
||||
builder.Entity<KeycloakComponentConfig>(entity =>
|
||||
{
|
||||
entity.HasKey(c => c.Id);
|
||||
entity.Property(c => c.AdminUsername).HasMaxLength(100).IsRequired();
|
||||
entity.Property(c => c.AdminUrl).HasMaxLength(500);
|
||||
|
||||
entity.HasOne(c => c.Tenant)
|
||||
.WithMany()
|
||||
.HasForeignKey(c => c.TenantId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasOne(c => c.ClusterComponent)
|
||||
.WithMany()
|
||||
.HasForeignKey(c => c.ClusterComponentId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasOne(c => c.CnpgDatabase)
|
||||
.WithMany()
|
||||
.HasForeignKey(c => c.CnpgDatabaseId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
// KeycloakRealm — a realm managed via a component config. RealmName must be
|
||||
// unique within a config (Keycloak enforces this globally per server).
|
||||
|
||||
builder.Entity<KeycloakRealm>(entity =>
|
||||
{
|
||||
entity.HasKey(r => r.Id);
|
||||
entity.Property(r => r.RealmName).HasMaxLength(100).IsRequired();
|
||||
entity.Property(r => r.DisplayName).HasMaxLength(200).IsRequired();
|
||||
|
||||
entity.HasIndex(r => new { r.KeycloakComponentConfigId, r.RealmName }).IsUnique();
|
||||
|
||||
entity.HasOne(r => r.ComponentConfig)
|
||||
.WithMany(c => c.Realms)
|
||||
.HasForeignKey(r => r.KeycloakComponentConfigId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasOne(r => r.LinkedApp)
|
||||
.WithMany()
|
||||
.HasForeignKey(r => r.LinkedAppId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
});
|
||||
|
||||
// KeycloakBackup — realm JSON snapshots stored in S3.
|
||||
|
||||
builder.Entity<KeycloakBackup>(entity =>
|
||||
{
|
||||
entity.HasKey(b => b.Id);
|
||||
entity.Property(b => b.ObjectKey).HasMaxLength(1024).IsRequired();
|
||||
entity.Property(b => b.RealmName).HasMaxLength(100).IsRequired();
|
||||
|
||||
entity.HasOne(b => b.Realm)
|
||||
.WithMany(r => r.Backups)
|
||||
.HasForeignKey(b => b.KeycloakRealmId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasOne(b => b.StorageLink)
|
||||
.WithMany()
|
||||
.HasForeignKey(b => b.StorageLinkId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
// DatabaseBinding — connects a managed database (CNPG or MongoDB) to an
|
||||
// AppDeployment. The platform syncs credentials into the app's namespace
|
||||
// automatically, including after password rotations.
|
||||
|
||||
builder.Entity<DatabaseBinding>(entity =>
|
||||
{
|
||||
entity.HasKey(b => b.Id);
|
||||
entity.Property(b => b.KubernetesSecretName).HasMaxLength(253).IsRequired();
|
||||
|
||||
entity.HasOne(b => b.CnpgDatabase)
|
||||
.WithMany(d => d.DatabaseBindings)
|
||||
.HasForeignKey(b => b.CnpgDatabaseId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasOne(b => b.MongoDatabase)
|
||||
.WithMany(d => d.DatabaseBindings)
|
||||
.HasForeignKey(b => b.MongoDatabaseId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasOne(b => b.AppDeployment)
|
||||
.WithMany(d => d.DatabaseBindings)
|
||||
.HasForeignKey(b => b.AppDeploymentId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace EntKube.Web.Data;
|
||||
|
||||
/// <summary>
|
||||
@@ -69,6 +71,14 @@ public class CnpgBackup
|
||||
/// </summary>
|
||||
public string? LastError { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The barman-cloud backup identifier (e.g. "20260519T130201").
|
||||
/// Populated from the Backup CR status.backupId at display time.
|
||||
/// Not persisted — only available when the backup list comes from live K8s.
|
||||
/// </summary>
|
||||
[NotMapped]
|
||||
public string? BarmanId { get; set; }
|
||||
|
||||
// Navigation
|
||||
public CnpgCluster CnpgCluster { get; set; } = null!;
|
||||
}
|
||||
|
||||
@@ -77,6 +77,12 @@ public class CnpgCluster
|
||||
/// </summary>
|
||||
public string? BackupSchedule { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of days to retain backups. The Barman Cloud Plugin uses this as the
|
||||
/// recovery window (e.g. "30d"). Expired backups are cleaned up automatically.
|
||||
/// </summary>
|
||||
public int RetentionDays { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Current lifecycle status of the cluster.
|
||||
/// </summary>
|
||||
|
||||
@@ -41,6 +41,12 @@ public class CnpgClusterDetail
|
||||
/// The current write LAG in bytes across replicas (0 = fully caught up).
|
||||
/// </summary>
|
||||
public long ReplicationLagBytes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Live-synced backup list reconciled from K8s Backup CRs plus the DB records.
|
||||
/// Populated by GetClusterDetailAsync; falls back to Cluster.Backups if sync is skipped.
|
||||
/// </summary>
|
||||
public List<CnpgBackup> Backups { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -51,4 +51,5 @@ public class CnpgDatabase
|
||||
|
||||
// Navigation
|
||||
public CnpgCluster CnpgCluster { get; set; } = null!;
|
||||
public ICollection<DatabaseBinding> DatabaseBindings { get; set; } = [];
|
||||
}
|
||||
|
||||
60
src/EntKube.Web/Data/DatabaseBinding.cs
Normal file
60
src/EntKube.Web/Data/DatabaseBinding.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
namespace EntKube.Web.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Connects a managed database (CNPG or MongoDB) to an app deployment so the
|
||||
/// platform can sync credentials into the app's namespace automatically.
|
||||
///
|
||||
/// When a binding exists and SyncEnabled is true, any credential sync (including
|
||||
/// after password rotation) will also push a Kubernetes Secret named
|
||||
/// KubernetesSecretName into the AppDeployment's namespace on its cluster.
|
||||
/// The app can then mount the secret as env vars without knowing which database
|
||||
/// provider backs it.
|
||||
/// </summary>
|
||||
public class DatabaseBinding
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The CNPG database whose credentials should be synced to the app.
|
||||
/// Mutually exclusive with MongoDatabaseId.
|
||||
/// </summary>
|
||||
public Guid? CnpgDatabaseId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The MongoDB database whose credentials should be synced to the app.
|
||||
/// Mutually exclusive with CnpgDatabaseId.
|
||||
/// </summary>
|
||||
public Guid? MongoDatabaseId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The deployment that consumes the database credentials.
|
||||
/// The secret is written into this deployment's namespace on its cluster.
|
||||
/// </summary>
|
||||
public Guid AppDeploymentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Kubernetes Secret name to create in the app's namespace.
|
||||
/// Choose something meaningful to the app (e.g. "billing-db", "app-postgres").
|
||||
/// </summary>
|
||||
public required string KubernetesSecretName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When true, credential syncs (including password rotations) automatically
|
||||
/// propagate to this app's namespace. Disable to pause propagation without
|
||||
/// removing the binding record.
|
||||
/// </summary>
|
||||
public bool SyncEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Last time credentials were successfully written to the app's namespace.
|
||||
/// Null means the binding exists but has not been synced yet.
|
||||
/// </summary>
|
||||
public DateTime? LastSyncedAt { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation
|
||||
public CnpgDatabase? CnpgDatabase { get; set; }
|
||||
public MongoDatabase? MongoDatabase { get; set; }
|
||||
public AppDeployment AppDeployment { get; set; } = null!;
|
||||
}
|
||||
49
src/EntKube.Web/Data/KeycloakBackup.cs
Normal file
49
src/EntKube.Web/Data/KeycloakBackup.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
namespace EntKube.Web.Data;
|
||||
|
||||
public enum KeycloakBackupStatus
|
||||
{
|
||||
Creating,
|
||||
Ready,
|
||||
Failed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A backup of a Keycloak realm — the full realm JSON exported via the Keycloak
|
||||
/// Admin REST API and stored in an S3 bucket. Used for disaster recovery and
|
||||
/// migrating realms between environments.
|
||||
/// </summary>
|
||||
public class KeycloakBackup
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid KeycloakRealmId { get; set; }
|
||||
|
||||
public Guid TenantId { get; set; }
|
||||
|
||||
public Guid StorageLinkId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// S3 object key (e.g. "keycloak-backups/my-realm/2026-05-19T14-32-00.json").
|
||||
/// </summary>
|
||||
public required string ObjectKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of the realm name at backup time, for display after the realm
|
||||
/// may have been renamed or deleted.
|
||||
/// </summary>
|
||||
public required string RealmName { get; set; }
|
||||
|
||||
public long SizeBytes { get; set; }
|
||||
|
||||
public KeycloakBackupStatus Status { get; set; } = KeycloakBackupStatus.Creating;
|
||||
|
||||
public string? LastError { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime? CompletedAt { get; set; }
|
||||
|
||||
// Navigation
|
||||
public KeycloakRealm Realm { get; set; } = null!;
|
||||
public StorageLink StorageLink { get; set; } = null!;
|
||||
}
|
||||
52
src/EntKube.Web/Data/KeycloakComponentConfig.cs
Normal file
52
src/EntKube.Web/Data/KeycloakComponentConfig.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
namespace EntKube.Web.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Keycloak-specific configuration attached to an installed Keycloak ClusterComponent.
|
||||
/// Stores which CNPG database backs this Keycloak instance and the admin credentials
|
||||
/// needed for the Admin REST API.
|
||||
///
|
||||
/// DB credentials (KC_DB_URL, KC_DB_USERNAME, KC_DB_PASSWORD) are stored as
|
||||
/// component vault secrets and synced to a Kubernetes Secret so the Helm chart
|
||||
/// can consume them without any out-of-band secret management.
|
||||
///
|
||||
/// The admin password is stored as a component vault secret named
|
||||
/// "KEYCLOAK_ADMIN_PASSWORD" and synced to the same Kubernetes Secret.
|
||||
/// </summary>
|
||||
public class KeycloakComponentConfig
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid TenantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The installed Keycloak ClusterComponent this config belongs to.
|
||||
/// One config per Keycloak component instance.
|
||||
/// </summary>
|
||||
public Guid ClusterComponentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The managed CNPG database backing this Keycloak instance.
|
||||
/// Null until the operator selects a database.
|
||||
/// </summary>
|
||||
public Guid? CnpgDatabaseId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Keycloak admin username (default "admin").
|
||||
/// </summary>
|
||||
public string AdminUsername { get; set; } = "admin";
|
||||
|
||||
/// <summary>
|
||||
/// URL used by the platform to call the Keycloak Admin REST API.
|
||||
/// Typically the external hostname (e.g. "https://auth.example.com").
|
||||
/// Can be auto-suggested from the component's ExternalRoutes.
|
||||
/// </summary>
|
||||
public string? AdminUrl { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation
|
||||
public Tenant Tenant { get; set; } = null!;
|
||||
public ClusterComponent ClusterComponent { get; set; } = null!;
|
||||
public CnpgDatabase? CnpgDatabase { get; set; }
|
||||
public ICollection<KeycloakRealm> Realms { get; set; } = [];
|
||||
}
|
||||
51
src/EntKube.Web/Data/KeycloakRealm.cs
Normal file
51
src/EntKube.Web/Data/KeycloakRealm.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
namespace EntKube.Web.Data;
|
||||
|
||||
/// <summary>
|
||||
/// A Keycloak realm managed by EntKube. The realm exists in Keycloak;
|
||||
/// this entity tracks it so EntKube can surface it in the UI, manage backups,
|
||||
/// and optionally link it to a customer app for self-service portal access.
|
||||
/// </summary>
|
||||
public class KeycloakRealm
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid KeycloakComponentConfigId { get; set; }
|
||||
|
||||
public Guid TenantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Keycloak realm ID (e.g. "acme-prod"). Used in all API calls.
|
||||
/// </summary>
|
||||
public required string RealmName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable display name shown inside the Keycloak login UI.
|
||||
/// </summary>
|
||||
public required string DisplayName { get; set; }
|
||||
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Login page theme assigned to this realm (e.g. "keycloak", "custom-brand").
|
||||
/// Null means the Keycloak default.
|
||||
/// </summary>
|
||||
public string? LoginTheme { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Account console theme for self-service password management.
|
||||
/// </summary>
|
||||
public string? AccountTheme { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When set, this realm is linked to a customer app and the customer can
|
||||
/// manage their realm (users, groups, IdPs) through the portal.
|
||||
/// </summary>
|
||||
public Guid? LinkedAppId { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation
|
||||
public KeycloakComponentConfig ComponentConfig { get; set; } = null!;
|
||||
public App? LinkedApp { get; set; }
|
||||
public ICollection<KeycloakBackup> Backups { get; set; } = [];
|
||||
}
|
||||
1723
src/EntKube.Web/Data/Migrations/Postgres/20260518072619_AddCnpgRetentionDays.Designer.cs
generated
Normal file
1723
src/EntKube.Web/Data/Migrations/Postgres/20260518072619_AddCnpgRetentionDays.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EntKube.Web.Data.Migrations.Postgres
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddCnpgRetentionDays : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "RetentionDays",
|
||||
table: "CnpgClusters",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RetentionDays",
|
||||
table: "CnpgClusters");
|
||||
}
|
||||
}
|
||||
}
|
||||
1925
src/EntKube.Web/Data/Migrations/Postgres/20260518082344_AddMongoEntities.Designer.cs
generated
Normal file
1925
src/EntKube.Web/Data/Migrations/Postgres/20260518082344_AddMongoEntities.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EntKube.Web.Data.Migrations.Postgres
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddMongoEntities : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "MongoDatabaseId",
|
||||
table: "VaultSecrets",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MongoClusters",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
KubernetesClusterId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(63)", maxLength: 63, nullable: false),
|
||||
Namespace = table.Column<string>(type: "character varying(63)", maxLength: 63, nullable: false),
|
||||
MongoVersion = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
Members = table.Column<int>(type: "integer", nullable: false),
|
||||
StorageSize = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
|
||||
StorageLinkId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
BackupSchedule = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||
RetentionDays = table.Column<int>(type: "integer", nullable: false),
|
||||
Status = table.Column<int>(type: "integer", nullable: false),
|
||||
LastError = table.Column<string>(type: "text", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MongoClusters", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_MongoClusters_KubernetesClusters_KubernetesClusterId",
|
||||
column: x => x.KubernetesClusterId,
|
||||
principalTable: "KubernetesClusters",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_MongoClusters_StorageLinks_StorageLinkId",
|
||||
column: x => x.StorageLinkId,
|
||||
principalTable: "StorageLinks",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_MongoClusters_Tenants_TenantId",
|
||||
column: x => x.TenantId,
|
||||
principalTable: "Tenants",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MongoBackups",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
MongoClusterId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(253)", maxLength: 253, nullable: false),
|
||||
Type = table.Column<int>(type: "integer", nullable: false),
|
||||
Status = table.Column<int>(type: "integer", nullable: false),
|
||||
StartedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
CompletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
SizeBytes = table.Column<long>(type: "bigint", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MongoBackups", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_MongoBackups_MongoClusters_MongoClusterId",
|
||||
column: x => x.MongoClusterId,
|
||||
principalTable: "MongoClusters",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MongoDatabases",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
MongoClusterId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(63)", maxLength: 63, nullable: false),
|
||||
Owner = table.Column<string>(type: "text", nullable: false),
|
||||
Status = table.Column<int>(type: "integer", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MongoDatabases", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_MongoDatabases_MongoClusters_MongoClusterId",
|
||||
column: x => x.MongoClusterId,
|
||||
principalTable: "MongoClusters",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VaultSecrets_MongoDatabaseId",
|
||||
table: "VaultSecrets",
|
||||
column: "MongoDatabaseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MongoBackups_MongoClusterId_Name",
|
||||
table: "MongoBackups",
|
||||
columns: new[] { "MongoClusterId", "Name" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MongoClusters_KubernetesClusterId_Name_Namespace",
|
||||
table: "MongoClusters",
|
||||
columns: new[] { "KubernetesClusterId", "Name", "Namespace" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MongoClusters_StorageLinkId",
|
||||
table: "MongoClusters",
|
||||
column: "StorageLinkId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MongoClusters_TenantId",
|
||||
table: "MongoClusters",
|
||||
column: "TenantId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MongoDatabases_MongoClusterId_Name",
|
||||
table: "MongoDatabases",
|
||||
columns: new[] { "MongoClusterId", "Name" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_VaultSecrets_MongoDatabases_MongoDatabaseId",
|
||||
table: "VaultSecrets",
|
||||
column: "MongoDatabaseId",
|
||||
principalTable: "MongoDatabases",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_VaultSecrets_MongoDatabases_MongoDatabaseId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MongoBackups");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MongoDatabases");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MongoClusters");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_VaultSecrets_MongoDatabaseId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MongoDatabaseId",
|
||||
table: "VaultSecrets");
|
||||
}
|
||||
}
|
||||
}
|
||||
2002
src/EntKube.Web/Data/Migrations/Postgres/20260518095816_AddDatabaseBindings.Designer.cs
generated
Normal file
2002
src/EntKube.Web/Data/Migrations/Postgres/20260518095816_AddDatabaseBindings.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EntKube.Web.Data.Migrations.Postgres
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDatabaseBindings : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DatabaseBindings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CnpgDatabaseId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
MongoDatabaseId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
AppDeploymentId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
KubernetesSecretName = table.Column<string>(type: "character varying(253)", maxLength: 253, nullable: false),
|
||||
SyncEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
LastSyncedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DatabaseBindings", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_DatabaseBindings_AppDeployments_AppDeploymentId",
|
||||
column: x => x.AppDeploymentId,
|
||||
principalTable: "AppDeployments",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_DatabaseBindings_CnpgDatabases_CnpgDatabaseId",
|
||||
column: x => x.CnpgDatabaseId,
|
||||
principalTable: "CnpgDatabases",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_DatabaseBindings_MongoDatabases_MongoDatabaseId",
|
||||
column: x => x.MongoDatabaseId,
|
||||
principalTable: "MongoDatabases",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DatabaseBindings_AppDeploymentId",
|
||||
table: "DatabaseBindings",
|
||||
column: "AppDeploymentId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DatabaseBindings_CnpgDatabaseId",
|
||||
table: "DatabaseBindings",
|
||||
column: "CnpgDatabaseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DatabaseBindings_MongoDatabaseId",
|
||||
table: "DatabaseBindings",
|
||||
column: "MongoDatabaseId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "DatabaseBindings");
|
||||
}
|
||||
}
|
||||
}
|
||||
2014
src/EntKube.Web/Data/Migrations/Postgres/20260518142646_AddMongoClusterVaultSecret.Designer.cs
generated
Normal file
2014
src/EntKube.Web/Data/Migrations/Postgres/20260518142646_AddMongoClusterVaultSecret.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EntKube.Web.Data.Migrations.Postgres
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddMongoClusterVaultSecret : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "MongoClusterId",
|
||||
table: "VaultSecrets",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VaultSecrets_MongoClusterId",
|
||||
table: "VaultSecrets",
|
||||
column: "MongoClusterId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_VaultSecrets_MongoClusters_MongoClusterId",
|
||||
table: "VaultSecrets",
|
||||
column: "MongoClusterId",
|
||||
principalTable: "MongoClusters",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_VaultSecrets_MongoClusters_MongoClusterId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_VaultSecrets_MongoClusterId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MongoClusterId",
|
||||
table: "VaultSecrets");
|
||||
}
|
||||
}
|
||||
}
|
||||
2221
src/EntKube.Web/Data/Migrations/Postgres/20260519191900_AddKeycloakComponentConfig.Designer.cs
generated
Normal file
2221
src/EntKube.Web/Data/Migrations/Postgres/20260519191900_AddKeycloakComponentConfig.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,200 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EntKube.Web.Data.Migrations.Postgres
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddKeycloakComponentConfig : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_KeycloakRealms_KeycloakConnections_KeycloakConnectionId",
|
||||
table: "KeycloakRealms");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_VaultSecrets_KeycloakConnections_KeycloakConnectionId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "KeycloakConnections");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_VaultSecrets_KeycloakConnectionId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "KeycloakConnectionId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "KeycloakConnectionId",
|
||||
table: "KeycloakRealms",
|
||||
newName: "KeycloakComponentConfigId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_KeycloakRealms_KeycloakConnectionId_RealmName",
|
||||
table: "KeycloakRealms",
|
||||
newName: "IX_KeycloakRealms_KeycloakComponentConfigId_RealmName");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "KeycloakComponentConfigs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ClusterComponentId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CnpgDatabaseId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
AdminUsername = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
AdminUrl = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_KeycloakComponentConfigs", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_KeycloakComponentConfigs_ClusterComponents_ClusterComponent~",
|
||||
column: x => x.ClusterComponentId,
|
||||
principalTable: "ClusterComponents",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_KeycloakComponentConfigs_CnpgDatabases_CnpgDatabaseId",
|
||||
column: x => x.CnpgDatabaseId,
|
||||
principalTable: "CnpgDatabases",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_KeycloakComponentConfigs_Tenants_TenantId",
|
||||
column: x => x.TenantId,
|
||||
principalTable: "Tenants",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_KeycloakComponentConfigs_ClusterComponentId",
|
||||
table: "KeycloakComponentConfigs",
|
||||
column: "ClusterComponentId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_KeycloakComponentConfigs_CnpgDatabaseId",
|
||||
table: "KeycloakComponentConfigs",
|
||||
column: "CnpgDatabaseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_KeycloakComponentConfigs_TenantId",
|
||||
table: "KeycloakComponentConfigs",
|
||||
column: "TenantId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_KeycloakRealms_KeycloakComponentConfigs_KeycloakComponentCo~",
|
||||
table: "KeycloakRealms",
|
||||
column: "KeycloakComponentConfigId",
|
||||
principalTable: "KeycloakComponentConfigs",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_KeycloakRealms_KeycloakComponentConfigs_KeycloakComponentCo~",
|
||||
table: "KeycloakRealms");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "KeycloakComponentConfigs");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "KeycloakComponentConfigId",
|
||||
table: "KeycloakRealms",
|
||||
newName: "KeycloakConnectionId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_KeycloakRealms_KeycloakComponentConfigId_RealmName",
|
||||
table: "KeycloakRealms",
|
||||
newName: "IX_KeycloakRealms_KeycloakConnectionId_RealmName");
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "KeycloakConnectionId",
|
||||
table: "VaultSecrets",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "KeycloakConnections",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
AdminPasswordSecretId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
KubernetesClusterId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
AdminUrl = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
|
||||
AdminUsername = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_KeycloakConnections", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_KeycloakConnections_KubernetesClusters_KubernetesClusterId",
|
||||
column: x => x.KubernetesClusterId,
|
||||
principalTable: "KubernetesClusters",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_KeycloakConnections_Tenants_TenantId",
|
||||
column: x => x.TenantId,
|
||||
principalTable: "Tenants",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_KeycloakConnections_VaultSecrets_AdminPasswordSecretId",
|
||||
column: x => x.AdminPasswordSecretId,
|
||||
principalTable: "VaultSecrets",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VaultSecrets_KeycloakConnectionId",
|
||||
table: "VaultSecrets",
|
||||
column: "KeycloakConnectionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_KeycloakConnections_AdminPasswordSecretId",
|
||||
table: "KeycloakConnections",
|
||||
column: "AdminPasswordSecretId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_KeycloakConnections_KubernetesClusterId",
|
||||
table: "KeycloakConnections",
|
||||
column: "KubernetesClusterId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_KeycloakConnections_TenantId",
|
||||
table: "KeycloakConnections",
|
||||
column: "TenantId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_KeycloakRealms_KeycloakConnections_KeycloakConnectionId",
|
||||
table: "KeycloakRealms",
|
||||
column: "KeycloakConnectionId",
|
||||
principalTable: "KeycloakConnections",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_VaultSecrets_KeycloakConnections_KeycloakConnectionId",
|
||||
table: "VaultSecrets",
|
||||
column: "KeycloakConnectionId",
|
||||
principalTable: "KeycloakConnections",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -342,6 +342,9 @@ namespace EntKube.Web.Data.Migrations.Postgres
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<int>("RetentionDays")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
@@ -449,6 +452,46 @@ namespace EntKube.Web.Data.Migrations.Postgres
|
||||
b.ToTable("CustomerAccesses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.DatabaseBinding", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AppDeploymentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("CnpgDatabaseId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("KubernetesSecretName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(253)
|
||||
.HasColumnType("character varying(253)");
|
||||
|
||||
b.Property<DateTime?>("LastSyncedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("MongoDatabaseId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("SyncEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AppDeploymentId");
|
||||
|
||||
b.HasIndex("CnpgDatabaseId");
|
||||
|
||||
b.HasIndex("MongoDatabaseId");
|
||||
|
||||
b.ToTable("DatabaseBindings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.DeploymentManifest", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -679,6 +722,140 @@ namespace EntKube.Web.Data.Migrations.Postgres
|
||||
b.ToTable("GroupMemberships");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakBackup", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("KeycloakRealmId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("LastError")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ObjectKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<string>("RealmName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<long>("SizeBytes")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid>("StorageLinkId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("KeycloakRealmId");
|
||||
|
||||
b.HasIndex("StorageLinkId");
|
||||
|
||||
b.ToTable("KeycloakBackups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakComponentConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AdminUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("AdminUsername")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<Guid>("ClusterComponentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("CnpgDatabaseId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClusterComponentId");
|
||||
|
||||
b.HasIndex("CnpgDatabaseId");
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
|
||||
b.ToTable("KeycloakComponentConfigs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakRealm", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AccountTheme")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<Guid>("KeycloakComponentConfigId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("LinkedAppId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("LoginTheme")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("RealmName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("LinkedAppId");
|
||||
|
||||
b.HasIndex("KeycloakComponentConfigId", "RealmName")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("KeycloakRealms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KubernetesCluster", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -720,6 +897,141 @@ namespace EntKube.Web.Data.Migrations.Postgres
|
||||
b.ToTable("KubernetesClusters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoBackup", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("MongoClusterId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(253)
|
||||
.HasColumnType("character varying(253)");
|
||||
|
||||
b.Property<long?>("SizeBytes")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MongoClusterId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MongoBackups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoCluster", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("BackupSchedule")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("KubernetesClusterId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("LastError")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Members")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("MongoVersion")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(63)
|
||||
.HasColumnType("character varying(63)");
|
||||
|
||||
b.Property<string>("Namespace")
|
||||
.IsRequired()
|
||||
.HasMaxLength(63)
|
||||
.HasColumnType("character varying(63)");
|
||||
|
||||
b.Property<int>("RetentionDays")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("StorageLinkId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("StorageSize")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StorageLinkId");
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
|
||||
b.HasIndex("KubernetesClusterId", "Name", "Namespace")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MongoClusters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoDatabase", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("MongoClusterId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(63)
|
||||
.HasColumnType("character varying(63)");
|
||||
|
||||
b.Property<string>("Owner")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MongoClusterId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MongoDatabases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.OpenStackConnection", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -988,6 +1300,12 @@ namespace EntKube.Web.Data.Migrations.Postgres
|
||||
.HasMaxLength(253)
|
||||
.HasColumnType("character varying(253)");
|
||||
|
||||
b.Property<Guid?>("MongoClusterId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("MongoDatabaseId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
@@ -1020,6 +1338,10 @@ namespace EntKube.Web.Data.Migrations.Postgres
|
||||
|
||||
b.HasIndex("ComponentId");
|
||||
|
||||
b.HasIndex("MongoClusterId");
|
||||
|
||||
b.HasIndex("MongoDatabaseId");
|
||||
|
||||
b.HasIndex("StorageLinkId");
|
||||
|
||||
b.HasIndex("VaultId", "AppId", "Name")
|
||||
@@ -1309,6 +1631,31 @@ namespace EntKube.Web.Data.Migrations.Postgres
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.DatabaseBinding", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.AppDeployment", "AppDeployment")
|
||||
.WithMany("DatabaseBindings")
|
||||
.HasForeignKey("AppDeploymentId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("EntKube.Web.Data.CnpgDatabase", "CnpgDatabase")
|
||||
.WithMany("DatabaseBindings")
|
||||
.HasForeignKey("CnpgDatabaseId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("EntKube.Web.Data.MongoDatabase", "MongoDatabase")
|
||||
.WithMany("DatabaseBindings")
|
||||
.HasForeignKey("MongoDatabaseId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("AppDeployment");
|
||||
|
||||
b.Navigation("CnpgDatabase");
|
||||
|
||||
b.Navigation("MongoDatabase");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.DeploymentManifest", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.AppDeployment", "Deployment")
|
||||
@@ -1390,6 +1737,69 @@ namespace EntKube.Web.Data.Migrations.Postgres
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakBackup", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.KeycloakRealm", "Realm")
|
||||
.WithMany("Backups")
|
||||
.HasForeignKey("KeycloakRealmId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("EntKube.Web.Data.StorageLink", "StorageLink")
|
||||
.WithMany()
|
||||
.HasForeignKey("StorageLinkId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Realm");
|
||||
|
||||
b.Navigation("StorageLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakComponentConfig", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.ClusterComponent", "ClusterComponent")
|
||||
.WithMany()
|
||||
.HasForeignKey("ClusterComponentId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("EntKube.Web.Data.CnpgDatabase", "CnpgDatabase")
|
||||
.WithMany()
|
||||
.HasForeignKey("CnpgDatabaseId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("EntKube.Web.Data.Tenant", "Tenant")
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ClusterComponent");
|
||||
|
||||
b.Navigation("CnpgDatabase");
|
||||
|
||||
b.Navigation("Tenant");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakRealm", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.KeycloakComponentConfig", "ComponentConfig")
|
||||
.WithMany("Realms")
|
||||
.HasForeignKey("KeycloakComponentConfigId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("EntKube.Web.Data.App", "LinkedApp")
|
||||
.WithMany()
|
||||
.HasForeignKey("LinkedAppId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("ComponentConfig");
|
||||
|
||||
b.Navigation("LinkedApp");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KubernetesCluster", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.Environment", "Environment")
|
||||
@@ -1409,6 +1819,54 @@ namespace EntKube.Web.Data.Migrations.Postgres
|
||||
b.Navigation("Tenant");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoBackup", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.MongoCluster", "MongoCluster")
|
||||
.WithMany("Backups")
|
||||
.HasForeignKey("MongoClusterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("MongoCluster");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoCluster", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.KubernetesCluster", "KubernetesCluster")
|
||||
.WithMany()
|
||||
.HasForeignKey("KubernetesClusterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("EntKube.Web.Data.StorageLink", "StorageLink")
|
||||
.WithMany()
|
||||
.HasForeignKey("StorageLinkId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("EntKube.Web.Data.Tenant", "Tenant")
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("KubernetesCluster");
|
||||
|
||||
b.Navigation("StorageLink");
|
||||
|
||||
b.Navigation("Tenant");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoDatabase", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.MongoCluster", "MongoCluster")
|
||||
.WithMany("Databases")
|
||||
.HasForeignKey("MongoClusterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("MongoCluster");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.OpenStackConnection", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.Tenant", "Tenant")
|
||||
@@ -1542,6 +2000,16 @@ namespace EntKube.Web.Data.Migrations.Postgres
|
||||
.HasForeignKey("ComponentId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("EntKube.Web.Data.MongoCluster", "MongoCluster")
|
||||
.WithMany()
|
||||
.HasForeignKey("MongoClusterId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("EntKube.Web.Data.MongoDatabase", "MongoDatabase")
|
||||
.WithMany()
|
||||
.HasForeignKey("MongoDatabaseId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("EntKube.Web.Data.StorageLink", "StorageLink")
|
||||
.WithMany()
|
||||
.HasForeignKey("StorageLinkId");
|
||||
@@ -1558,6 +2026,10 @@ namespace EntKube.Web.Data.Migrations.Postgres
|
||||
|
||||
b.Navigation("Component");
|
||||
|
||||
b.Navigation("MongoCluster");
|
||||
|
||||
b.Navigation("MongoDatabase");
|
||||
|
||||
b.Navigation("StorageLink");
|
||||
|
||||
b.Navigation("Vault");
|
||||
@@ -1625,6 +2097,8 @@ namespace EntKube.Web.Data.Migrations.Postgres
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.AppDeployment", b =>
|
||||
{
|
||||
b.Navigation("DatabaseBindings");
|
||||
|
||||
b.Navigation("Manifests");
|
||||
|
||||
b.Navigation("Resources");
|
||||
@@ -1648,6 +2122,11 @@ namespace EntKube.Web.Data.Migrations.Postgres
|
||||
b.Navigation("Databases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.CnpgDatabase", b =>
|
||||
{
|
||||
b.Navigation("DatabaseBindings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.Customer", b =>
|
||||
{
|
||||
b.Navigation("Apps");
|
||||
@@ -1670,11 +2149,33 @@ namespace EntKube.Web.Data.Migrations.Postgres
|
||||
b.Navigation("Memberships");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakComponentConfig", b =>
|
||||
{
|
||||
b.Navigation("Realms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakRealm", b =>
|
||||
{
|
||||
b.Navigation("Backups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KubernetesCluster", b =>
|
||||
{
|
||||
b.Navigation("Components");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoCluster", b =>
|
||||
{
|
||||
b.Navigation("Backups");
|
||||
|
||||
b.Navigation("Databases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoDatabase", b =>
|
||||
{
|
||||
b.Navigation("DatabaseBindings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.OpenStackConnection", b =>
|
||||
{
|
||||
b.Navigation("StorageLinks");
|
||||
|
||||
1725
src/EntKube.Web/Data/Migrations/SqlServer/20260518072630_AddCnpgRetentionDays.Designer.cs
generated
Normal file
1725
src/EntKube.Web/Data/Migrations/SqlServer/20260518072630_AddCnpgRetentionDays.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,176 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EntKube.Web.Data.Migrations.SqlServer
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddCnpgRetentionDays : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "CnpgDatabaseId",
|
||||
table: "VaultSecrets",
|
||||
type: "uniqueidentifier",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CnpgClusters",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
KubernetesClusterId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(63)", maxLength: 63, nullable: false),
|
||||
Namespace = table.Column<string>(type: "nvarchar(63)", maxLength: 63, nullable: false),
|
||||
PostgresVersion = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false),
|
||||
Instances = table.Column<int>(type: "int", nullable: false),
|
||||
StorageSize = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
|
||||
StorageLinkId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
BackupSchedule = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
RetentionDays = table.Column<int>(type: "int", nullable: false),
|
||||
Status = table.Column<int>(type: "int", nullable: false),
|
||||
LastError = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CnpgClusters", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_CnpgClusters_KubernetesClusters_KubernetesClusterId",
|
||||
column: x => x.KubernetesClusterId,
|
||||
principalTable: "KubernetesClusters",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_CnpgClusters_StorageLinks_StorageLinkId",
|
||||
column: x => x.StorageLinkId,
|
||||
principalTable: "StorageLinks",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_CnpgClusters_Tenants_TenantId",
|
||||
column: x => x.TenantId,
|
||||
principalTable: "Tenants",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CnpgBackups",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
CnpgClusterId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(253)", maxLength: 253, nullable: false),
|
||||
Type = table.Column<int>(type: "int", nullable: false),
|
||||
Status = table.Column<int>(type: "int", nullable: false),
|
||||
StartedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CompletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
SizeBytes = table.Column<long>(type: "bigint", nullable: true),
|
||||
LastError = table.Column<string>(type: "nvarchar(max)", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CnpgBackups", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_CnpgBackups_CnpgClusters_CnpgClusterId",
|
||||
column: x => x.CnpgClusterId,
|
||||
principalTable: "CnpgClusters",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CnpgDatabases",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
CnpgClusterId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(63)", maxLength: 63, nullable: false),
|
||||
Owner = table.Column<string>(type: "nvarchar(63)", maxLength: 63, nullable: false),
|
||||
Status = table.Column<int>(type: "int", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CnpgDatabases", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_CnpgDatabases_CnpgClusters_CnpgClusterId",
|
||||
column: x => x.CnpgClusterId,
|
||||
principalTable: "CnpgClusters",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VaultSecrets_CnpgDatabaseId",
|
||||
table: "VaultSecrets",
|
||||
column: "CnpgDatabaseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CnpgBackups_CnpgClusterId_Name",
|
||||
table: "CnpgBackups",
|
||||
columns: new[] { "CnpgClusterId", "Name" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CnpgClusters_KubernetesClusterId_Name_Namespace",
|
||||
table: "CnpgClusters",
|
||||
columns: new[] { "KubernetesClusterId", "Name", "Namespace" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CnpgClusters_StorageLinkId",
|
||||
table: "CnpgClusters",
|
||||
column: "StorageLinkId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CnpgClusters_TenantId",
|
||||
table: "CnpgClusters",
|
||||
column: "TenantId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CnpgDatabases_CnpgClusterId_Name",
|
||||
table: "CnpgDatabases",
|
||||
columns: new[] { "CnpgClusterId", "Name" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_VaultSecrets_CnpgDatabases_CnpgDatabaseId",
|
||||
table: "VaultSecrets",
|
||||
column: "CnpgDatabaseId",
|
||||
principalTable: "CnpgDatabases",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_VaultSecrets_CnpgDatabases_CnpgDatabaseId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "CnpgBackups");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "CnpgDatabases");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "CnpgClusters");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_VaultSecrets_CnpgDatabaseId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CnpgDatabaseId",
|
||||
table: "VaultSecrets");
|
||||
}
|
||||
}
|
||||
}
|
||||
1927
src/EntKube.Web/Data/Migrations/SqlServer/20260518082355_AddMongoEntities.Designer.cs
generated
Normal file
1927
src/EntKube.Web/Data/Migrations/SqlServer/20260518082355_AddMongoEntities.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EntKube.Web.Data.Migrations.SqlServer
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddMongoEntities : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "MongoDatabaseId",
|
||||
table: "VaultSecrets",
|
||||
type: "uniqueidentifier",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MongoClusters",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
KubernetesClusterId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(63)", maxLength: 63, nullable: false),
|
||||
Namespace = table.Column<string>(type: "nvarchar(63)", maxLength: 63, nullable: false),
|
||||
MongoVersion = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
|
||||
Members = table.Column<int>(type: "int", nullable: false),
|
||||
StorageSize = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
|
||||
StorageLinkId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
BackupSchedule = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
RetentionDays = table.Column<int>(type: "int", nullable: false),
|
||||
Status = table.Column<int>(type: "int", nullable: false),
|
||||
LastError = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MongoClusters", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_MongoClusters_KubernetesClusters_KubernetesClusterId",
|
||||
column: x => x.KubernetesClusterId,
|
||||
principalTable: "KubernetesClusters",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_MongoClusters_StorageLinks_StorageLinkId",
|
||||
column: x => x.StorageLinkId,
|
||||
principalTable: "StorageLinks",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_MongoClusters_Tenants_TenantId",
|
||||
column: x => x.TenantId,
|
||||
principalTable: "Tenants",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MongoBackups",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
MongoClusterId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(253)", maxLength: 253, nullable: false),
|
||||
Type = table.Column<int>(type: "int", nullable: false),
|
||||
Status = table.Column<int>(type: "int", nullable: false),
|
||||
StartedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CompletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
SizeBytes = table.Column<long>(type: "bigint", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MongoBackups", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_MongoBackups_MongoClusters_MongoClusterId",
|
||||
column: x => x.MongoClusterId,
|
||||
principalTable: "MongoClusters",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MongoDatabases",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
MongoClusterId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(63)", maxLength: 63, nullable: false),
|
||||
Owner = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
Status = table.Column<int>(type: "int", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MongoDatabases", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_MongoDatabases_MongoClusters_MongoClusterId",
|
||||
column: x => x.MongoClusterId,
|
||||
principalTable: "MongoClusters",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VaultSecrets_MongoDatabaseId",
|
||||
table: "VaultSecrets",
|
||||
column: "MongoDatabaseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MongoBackups_MongoClusterId_Name",
|
||||
table: "MongoBackups",
|
||||
columns: new[] { "MongoClusterId", "Name" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MongoClusters_KubernetesClusterId_Name_Namespace",
|
||||
table: "MongoClusters",
|
||||
columns: new[] { "KubernetesClusterId", "Name", "Namespace" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MongoClusters_StorageLinkId",
|
||||
table: "MongoClusters",
|
||||
column: "StorageLinkId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MongoClusters_TenantId",
|
||||
table: "MongoClusters",
|
||||
column: "TenantId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MongoDatabases_MongoClusterId_Name",
|
||||
table: "MongoDatabases",
|
||||
columns: new[] { "MongoClusterId", "Name" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_VaultSecrets_MongoDatabases_MongoDatabaseId",
|
||||
table: "VaultSecrets",
|
||||
column: "MongoDatabaseId",
|
||||
principalTable: "MongoDatabases",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_VaultSecrets_MongoDatabases_MongoDatabaseId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MongoBackups");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MongoDatabases");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MongoClusters");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_VaultSecrets_MongoDatabaseId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MongoDatabaseId",
|
||||
table: "VaultSecrets");
|
||||
}
|
||||
}
|
||||
}
|
||||
2004
src/EntKube.Web/Data/Migrations/SqlServer/20260518095821_AddDatabaseBindings.Designer.cs
generated
Normal file
2004
src/EntKube.Web/Data/Migrations/SqlServer/20260518095821_AddDatabaseBindings.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EntKube.Web.Data.Migrations.SqlServer
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDatabaseBindings : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DatabaseBindings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
CnpgDatabaseId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
MongoDatabaseId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
AppDeploymentId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
KubernetesSecretName = table.Column<string>(type: "nvarchar(253)", maxLength: 253, nullable: false),
|
||||
SyncEnabled = table.Column<bool>(type: "bit", nullable: false),
|
||||
LastSyncedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DatabaseBindings", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_DatabaseBindings_AppDeployments_AppDeploymentId",
|
||||
column: x => x.AppDeploymentId,
|
||||
principalTable: "AppDeployments",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_DatabaseBindings_CnpgDatabases_CnpgDatabaseId",
|
||||
column: x => x.CnpgDatabaseId,
|
||||
principalTable: "CnpgDatabases",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_DatabaseBindings_MongoDatabases_MongoDatabaseId",
|
||||
column: x => x.MongoDatabaseId,
|
||||
principalTable: "MongoDatabases",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DatabaseBindings_AppDeploymentId",
|
||||
table: "DatabaseBindings",
|
||||
column: "AppDeploymentId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DatabaseBindings_CnpgDatabaseId",
|
||||
table: "DatabaseBindings",
|
||||
column: "CnpgDatabaseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DatabaseBindings_MongoDatabaseId",
|
||||
table: "DatabaseBindings",
|
||||
column: "MongoDatabaseId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "DatabaseBindings");
|
||||
}
|
||||
}
|
||||
}
|
||||
2016
src/EntKube.Web/Data/Migrations/SqlServer/20260518142650_AddMongoClusterVaultSecret.Designer.cs
generated
Normal file
2016
src/EntKube.Web/Data/Migrations/SqlServer/20260518142650_AddMongoClusterVaultSecret.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EntKube.Web.Data.Migrations.SqlServer
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddMongoClusterVaultSecret : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "MongoClusterId",
|
||||
table: "VaultSecrets",
|
||||
type: "uniqueidentifier",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VaultSecrets_MongoClusterId",
|
||||
table: "VaultSecrets",
|
||||
column: "MongoClusterId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_VaultSecrets_MongoClusters_MongoClusterId",
|
||||
table: "VaultSecrets",
|
||||
column: "MongoClusterId",
|
||||
principalTable: "MongoClusters",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_VaultSecrets_MongoClusters_MongoClusterId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_VaultSecrets_MongoClusterId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MongoClusterId",
|
||||
table: "VaultSecrets");
|
||||
}
|
||||
}
|
||||
}
|
||||
2223
src/EntKube.Web/Data/Migrations/SqlServer/20260519191904_AddKeycloakComponentConfig.Designer.cs
generated
Normal file
2223
src/EntKube.Web/Data/Migrations/SqlServer/20260519191904_AddKeycloakComponentConfig.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,200 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EntKube.Web.Data.Migrations.SqlServer
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddKeycloakComponentConfig : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_KeycloakRealms_KeycloakConnections_KeycloakConnectionId",
|
||||
table: "KeycloakRealms");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_VaultSecrets_KeycloakConnections_KeycloakConnectionId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "KeycloakConnections");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_VaultSecrets_KeycloakConnectionId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "KeycloakConnectionId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "KeycloakConnectionId",
|
||||
table: "KeycloakRealms",
|
||||
newName: "KeycloakComponentConfigId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_KeycloakRealms_KeycloakConnectionId_RealmName",
|
||||
table: "KeycloakRealms",
|
||||
newName: "IX_KeycloakRealms_KeycloakComponentConfigId_RealmName");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "KeycloakComponentConfigs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
ClusterComponentId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
CnpgDatabaseId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
AdminUsername = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
AdminUrl = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_KeycloakComponentConfigs", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_KeycloakComponentConfigs_ClusterComponents_ClusterComponentId",
|
||||
column: x => x.ClusterComponentId,
|
||||
principalTable: "ClusterComponents",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_KeycloakComponentConfigs_CnpgDatabases_CnpgDatabaseId",
|
||||
column: x => x.CnpgDatabaseId,
|
||||
principalTable: "CnpgDatabases",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_KeycloakComponentConfigs_Tenants_TenantId",
|
||||
column: x => x.TenantId,
|
||||
principalTable: "Tenants",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_KeycloakComponentConfigs_ClusterComponentId",
|
||||
table: "KeycloakComponentConfigs",
|
||||
column: "ClusterComponentId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_KeycloakComponentConfigs_CnpgDatabaseId",
|
||||
table: "KeycloakComponentConfigs",
|
||||
column: "CnpgDatabaseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_KeycloakComponentConfigs_TenantId",
|
||||
table: "KeycloakComponentConfigs",
|
||||
column: "TenantId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_KeycloakRealms_KeycloakComponentConfigs_KeycloakComponentConfigId",
|
||||
table: "KeycloakRealms",
|
||||
column: "KeycloakComponentConfigId",
|
||||
principalTable: "KeycloakComponentConfigs",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_KeycloakRealms_KeycloakComponentConfigs_KeycloakComponentConfigId",
|
||||
table: "KeycloakRealms");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "KeycloakComponentConfigs");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "KeycloakComponentConfigId",
|
||||
table: "KeycloakRealms",
|
||||
newName: "KeycloakConnectionId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_KeycloakRealms_KeycloakComponentConfigId_RealmName",
|
||||
table: "KeycloakRealms",
|
||||
newName: "IX_KeycloakRealms_KeycloakConnectionId_RealmName");
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "KeycloakConnectionId",
|
||||
table: "VaultSecrets",
|
||||
type: "uniqueidentifier",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "KeycloakConnections",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
AdminPasswordSecretId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
KubernetesClusterId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
AdminUrl = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false),
|
||||
AdminUsername = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_KeycloakConnections", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_KeycloakConnections_KubernetesClusters_KubernetesClusterId",
|
||||
column: x => x.KubernetesClusterId,
|
||||
principalTable: "KubernetesClusters",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_KeycloakConnections_Tenants_TenantId",
|
||||
column: x => x.TenantId,
|
||||
principalTable: "Tenants",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_KeycloakConnections_VaultSecrets_AdminPasswordSecretId",
|
||||
column: x => x.AdminPasswordSecretId,
|
||||
principalTable: "VaultSecrets",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VaultSecrets_KeycloakConnectionId",
|
||||
table: "VaultSecrets",
|
||||
column: "KeycloakConnectionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_KeycloakConnections_AdminPasswordSecretId",
|
||||
table: "KeycloakConnections",
|
||||
column: "AdminPasswordSecretId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_KeycloakConnections_KubernetesClusterId",
|
||||
table: "KeycloakConnections",
|
||||
column: "KubernetesClusterId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_KeycloakConnections_TenantId",
|
||||
table: "KeycloakConnections",
|
||||
column: "TenantId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_KeycloakRealms_KeycloakConnections_KeycloakConnectionId",
|
||||
table: "KeycloakRealms",
|
||||
column: "KeycloakConnectionId",
|
||||
principalTable: "KeycloakConnections",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_VaultSecrets_KeycloakConnections_KeycloakConnectionId",
|
||||
table: "VaultSecrets",
|
||||
column: "KeycloakConnectionId",
|
||||
principalTable: "KeycloakConnections",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,6 +266,145 @@ namespace EntKube.Web.Data.Migrations.SqlServer
|
||||
b.ToTable("ClusterComponents");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.CnpgBackup", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("CnpgClusterId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("LastError")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(253)
|
||||
.HasColumnType("nvarchar(253)");
|
||||
|
||||
b.Property<long?>("SizeBytes")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("StartedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CnpgClusterId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CnpgBackups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.CnpgCluster", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("BackupSchedule")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("Instances")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid>("KubernetesClusterId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("LastError")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(63)
|
||||
.HasColumnType("nvarchar(63)");
|
||||
|
||||
b.Property<string>("Namespace")
|
||||
.IsRequired()
|
||||
.HasMaxLength(63)
|
||||
.HasColumnType("nvarchar(63)");
|
||||
|
||||
b.Property<string>("PostgresVersion")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("nvarchar(10)");
|
||||
|
||||
b.Property<int>("RetentionDays")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid?>("StorageLinkId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("StorageSize")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StorageLinkId");
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
|
||||
b.HasIndex("KubernetesClusterId", "Name", "Namespace")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CnpgClusters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.CnpgDatabase", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("CnpgClusterId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(63)
|
||||
.HasColumnType("nvarchar(63)");
|
||||
|
||||
b.Property<string>("Owner")
|
||||
.IsRequired()
|
||||
.HasMaxLength(63)
|
||||
.HasColumnType("nvarchar(63)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CnpgClusterId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CnpgDatabases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.Customer", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -314,6 +453,46 @@ namespace EntKube.Web.Data.Migrations.SqlServer
|
||||
b.ToTable("CustomerAccesses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.DatabaseBinding", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("AppDeploymentId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("CnpgDatabaseId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("KubernetesSecretName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(253)
|
||||
.HasColumnType("nvarchar(253)");
|
||||
|
||||
b.Property<DateTime?>("LastSyncedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("MongoDatabaseId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<bool>("SyncEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AppDeploymentId");
|
||||
|
||||
b.HasIndex("CnpgDatabaseId");
|
||||
|
||||
b.HasIndex("MongoDatabaseId");
|
||||
|
||||
b.ToTable("DatabaseBindings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.DeploymentManifest", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -544,6 +723,140 @@ namespace EntKube.Web.Data.Migrations.SqlServer
|
||||
b.ToTable("GroupMemberships");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakBackup", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid>("KeycloakRealmId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("LastError")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("ObjectKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("nvarchar(1024)");
|
||||
|
||||
b.Property<string>("RealmName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<long>("SizeBytes")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid>("StorageLinkId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("KeycloakRealmId");
|
||||
|
||||
b.HasIndex("StorageLinkId");
|
||||
|
||||
b.ToTable("KeycloakBackups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakComponentConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("AdminUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<string>("AdminUsername")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<Guid>("ClusterComponentId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("CnpgDatabaseId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClusterComponentId");
|
||||
|
||||
b.HasIndex("CnpgDatabaseId");
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
|
||||
b.ToTable("KeycloakComponentConfigs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakRealm", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("AccountTheme")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<Guid>("KeycloakComponentConfigId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("LinkedAppId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("LoginTheme")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("RealmName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("LinkedAppId");
|
||||
|
||||
b.HasIndex("KeycloakComponentConfigId", "RealmName")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("KeycloakRealms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KubernetesCluster", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -585,6 +898,141 @@ namespace EntKube.Web.Data.Migrations.SqlServer
|
||||
b.ToTable("KubernetesClusters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoBackup", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid>("MongoClusterId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(253)
|
||||
.HasColumnType("nvarchar(253)");
|
||||
|
||||
b.Property<long?>("SizeBytes")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("StartedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MongoClusterId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MongoBackups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoCluster", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("BackupSchedule")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid>("KubernetesClusterId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("LastError")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("Members")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("MongoVersion")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(63)
|
||||
.HasColumnType("nvarchar(63)");
|
||||
|
||||
b.Property<string>("Namespace")
|
||||
.IsRequired()
|
||||
.HasMaxLength(63)
|
||||
.HasColumnType("nvarchar(63)");
|
||||
|
||||
b.Property<int>("RetentionDays")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<Guid?>("StorageLinkId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("StorageSize")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StorageLinkId");
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
|
||||
b.HasIndex("KubernetesClusterId", "Name", "Namespace")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MongoClusters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoDatabase", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid>("MongoClusterId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(63)
|
||||
.HasColumnType("nvarchar(63)");
|
||||
|
||||
b.Property<string>("Owner")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MongoClusterId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MongoDatabases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.OpenStackConnection", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -832,6 +1280,9 @@ namespace EntKube.Web.Data.Migrations.SqlServer
|
||||
b.Property<Guid?>("AppId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("CnpgDatabaseId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("ComponentId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
@@ -850,6 +1301,12 @@ namespace EntKube.Web.Data.Migrations.SqlServer
|
||||
.HasMaxLength(253)
|
||||
.HasColumnType("nvarchar(253)");
|
||||
|
||||
b.Property<Guid?>("MongoClusterId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("MongoDatabaseId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
@@ -878,8 +1335,14 @@ namespace EntKube.Web.Data.Migrations.SqlServer
|
||||
|
||||
b.HasIndex("AppId");
|
||||
|
||||
b.HasIndex("CnpgDatabaseId");
|
||||
|
||||
b.HasIndex("ComponentId");
|
||||
|
||||
b.HasIndex("MongoClusterId");
|
||||
|
||||
b.HasIndex("MongoDatabaseId");
|
||||
|
||||
b.HasIndex("StorageLinkId");
|
||||
|
||||
b.HasIndex("VaultId", "AppId", "Name")
|
||||
@@ -1092,6 +1555,54 @@ namespace EntKube.Web.Data.Migrations.SqlServer
|
||||
b.Navigation("Cluster");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.CnpgBackup", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.CnpgCluster", "CnpgCluster")
|
||||
.WithMany("Backups")
|
||||
.HasForeignKey("CnpgClusterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("CnpgCluster");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.CnpgCluster", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.KubernetesCluster", "KubernetesCluster")
|
||||
.WithMany()
|
||||
.HasForeignKey("KubernetesClusterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("EntKube.Web.Data.StorageLink", "StorageLink")
|
||||
.WithMany()
|
||||
.HasForeignKey("StorageLinkId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("EntKube.Web.Data.Tenant", "Tenant")
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("KubernetesCluster");
|
||||
|
||||
b.Navigation("StorageLink");
|
||||
|
||||
b.Navigation("Tenant");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.CnpgDatabase", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.CnpgCluster", "CnpgCluster")
|
||||
.WithMany("Databases")
|
||||
.HasForeignKey("CnpgClusterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("CnpgCluster");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.Customer", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.Tenant", "Tenant")
|
||||
@@ -1122,6 +1633,31 @@ namespace EntKube.Web.Data.Migrations.SqlServer
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.DatabaseBinding", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.AppDeployment", "AppDeployment")
|
||||
.WithMany("DatabaseBindings")
|
||||
.HasForeignKey("AppDeploymentId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("EntKube.Web.Data.CnpgDatabase", "CnpgDatabase")
|
||||
.WithMany("DatabaseBindings")
|
||||
.HasForeignKey("CnpgDatabaseId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("EntKube.Web.Data.MongoDatabase", "MongoDatabase")
|
||||
.WithMany("DatabaseBindings")
|
||||
.HasForeignKey("MongoDatabaseId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("AppDeployment");
|
||||
|
||||
b.Navigation("CnpgDatabase");
|
||||
|
||||
b.Navigation("MongoDatabase");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.DeploymentManifest", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.AppDeployment", "Deployment")
|
||||
@@ -1203,6 +1739,69 @@ namespace EntKube.Web.Data.Migrations.SqlServer
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakBackup", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.KeycloakRealm", "Realm")
|
||||
.WithMany("Backups")
|
||||
.HasForeignKey("KeycloakRealmId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("EntKube.Web.Data.StorageLink", "StorageLink")
|
||||
.WithMany()
|
||||
.HasForeignKey("StorageLinkId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Realm");
|
||||
|
||||
b.Navigation("StorageLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakComponentConfig", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.ClusterComponent", "ClusterComponent")
|
||||
.WithMany()
|
||||
.HasForeignKey("ClusterComponentId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("EntKube.Web.Data.CnpgDatabase", "CnpgDatabase")
|
||||
.WithMany()
|
||||
.HasForeignKey("CnpgDatabaseId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("EntKube.Web.Data.Tenant", "Tenant")
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ClusterComponent");
|
||||
|
||||
b.Navigation("CnpgDatabase");
|
||||
|
||||
b.Navigation("Tenant");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakRealm", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.KeycloakComponentConfig", "ComponentConfig")
|
||||
.WithMany("Realms")
|
||||
.HasForeignKey("KeycloakComponentConfigId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("EntKube.Web.Data.App", "LinkedApp")
|
||||
.WithMany()
|
||||
.HasForeignKey("LinkedAppId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("ComponentConfig");
|
||||
|
||||
b.Navigation("LinkedApp");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KubernetesCluster", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.Environment", "Environment")
|
||||
@@ -1222,6 +1821,54 @@ namespace EntKube.Web.Data.Migrations.SqlServer
|
||||
b.Navigation("Tenant");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoBackup", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.MongoCluster", "MongoCluster")
|
||||
.WithMany("Backups")
|
||||
.HasForeignKey("MongoClusterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("MongoCluster");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoCluster", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.KubernetesCluster", "KubernetesCluster")
|
||||
.WithMany()
|
||||
.HasForeignKey("KubernetesClusterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("EntKube.Web.Data.StorageLink", "StorageLink")
|
||||
.WithMany()
|
||||
.HasForeignKey("StorageLinkId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("EntKube.Web.Data.Tenant", "Tenant")
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("KubernetesCluster");
|
||||
|
||||
b.Navigation("StorageLink");
|
||||
|
||||
b.Navigation("Tenant");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoDatabase", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.MongoCluster", "MongoCluster")
|
||||
.WithMany("Databases")
|
||||
.HasForeignKey("MongoClusterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("MongoCluster");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.OpenStackConnection", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.Tenant", "Tenant")
|
||||
@@ -1345,11 +1992,26 @@ namespace EntKube.Web.Data.Migrations.SqlServer
|
||||
.HasForeignKey("AppId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("EntKube.Web.Data.CnpgDatabase", "CnpgDatabase")
|
||||
.WithMany()
|
||||
.HasForeignKey("CnpgDatabaseId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("EntKube.Web.Data.ClusterComponent", "Component")
|
||||
.WithMany("Secrets")
|
||||
.HasForeignKey("ComponentId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("EntKube.Web.Data.MongoCluster", "MongoCluster")
|
||||
.WithMany()
|
||||
.HasForeignKey("MongoClusterId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("EntKube.Web.Data.MongoDatabase", "MongoDatabase")
|
||||
.WithMany()
|
||||
.HasForeignKey("MongoDatabaseId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("EntKube.Web.Data.StorageLink", "StorageLink")
|
||||
.WithMany()
|
||||
.HasForeignKey("StorageLinkId");
|
||||
@@ -1362,8 +2024,14 @@ namespace EntKube.Web.Data.Migrations.SqlServer
|
||||
|
||||
b.Navigation("App");
|
||||
|
||||
b.Navigation("CnpgDatabase");
|
||||
|
||||
b.Navigation("Component");
|
||||
|
||||
b.Navigation("MongoCluster");
|
||||
|
||||
b.Navigation("MongoDatabase");
|
||||
|
||||
b.Navigation("StorageLink");
|
||||
|
||||
b.Navigation("Vault");
|
||||
@@ -1431,6 +2099,8 @@ namespace EntKube.Web.Data.Migrations.SqlServer
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.AppDeployment", b =>
|
||||
{
|
||||
b.Navigation("DatabaseBindings");
|
||||
|
||||
b.Navigation("Manifests");
|
||||
|
||||
b.Navigation("Resources");
|
||||
@@ -1447,6 +2117,18 @@ namespace EntKube.Web.Data.Migrations.SqlServer
|
||||
b.Navigation("StorageBindings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.CnpgCluster", b =>
|
||||
{
|
||||
b.Navigation("Backups");
|
||||
|
||||
b.Navigation("Databases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.CnpgDatabase", b =>
|
||||
{
|
||||
b.Navigation("DatabaseBindings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.Customer", b =>
|
||||
{
|
||||
b.Navigation("Apps");
|
||||
@@ -1469,11 +2151,33 @@ namespace EntKube.Web.Data.Migrations.SqlServer
|
||||
b.Navigation("Memberships");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakComponentConfig", b =>
|
||||
{
|
||||
b.Navigation("Realms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakRealm", b =>
|
||||
{
|
||||
b.Navigation("Backups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KubernetesCluster", b =>
|
||||
{
|
||||
b.Navigation("Components");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoCluster", b =>
|
||||
{
|
||||
b.Navigation("Backups");
|
||||
|
||||
b.Navigation("Databases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoDatabase", b =>
|
||||
{
|
||||
b.Navigation("DatabaseBindings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.OpenStackConnection", b =>
|
||||
{
|
||||
b.Navigation("StorageLinks");
|
||||
|
||||
1714
src/EntKube.Web/Data/Migrations/Sqlite/20260518072608_AddCnpgRetentionDays.Designer.cs
generated
Normal file
1714
src/EntKube.Web/Data/Migrations/Sqlite/20260518072608_AddCnpgRetentionDays.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EntKube.Web.Data.Migrations.Sqlite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddCnpgRetentionDays : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "RetentionDays",
|
||||
table: "CnpgClusters",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RetentionDays",
|
||||
table: "CnpgClusters");
|
||||
}
|
||||
}
|
||||
}
|
||||
1916
src/EntKube.Web/Data/Migrations/Sqlite/20260518082333_AddMongoEntities.Designer.cs
generated
Normal file
1916
src/EntKube.Web/Data/Migrations/Sqlite/20260518082333_AddMongoEntities.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EntKube.Web.Data.Migrations.Sqlite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddMongoEntities : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "MongoDatabaseId",
|
||||
table: "VaultSecrets",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MongoClusters",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
KubernetesClusterId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 63, nullable: false),
|
||||
Namespace = table.Column<string>(type: "TEXT", maxLength: 63, nullable: false),
|
||||
MongoVersion = table.Column<string>(type: "TEXT", maxLength: 20, nullable: false),
|
||||
Members = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
StorageSize = table.Column<string>(type: "TEXT", maxLength: 20, nullable: false),
|
||||
StorageLinkId = table.Column<Guid>(type: "TEXT", nullable: true),
|
||||
BackupSchedule = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||
RetentionDays = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Status = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
LastError = table.Column<string>(type: "TEXT", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MongoClusters", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_MongoClusters_KubernetesClusters_KubernetesClusterId",
|
||||
column: x => x.KubernetesClusterId,
|
||||
principalTable: "KubernetesClusters",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_MongoClusters_StorageLinks_StorageLinkId",
|
||||
column: x => x.StorageLinkId,
|
||||
principalTable: "StorageLinks",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_MongoClusters_Tenants_TenantId",
|
||||
column: x => x.TenantId,
|
||||
principalTable: "Tenants",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MongoBackups",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
MongoClusterId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 253, nullable: false),
|
||||
Type = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Status = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
StartedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
CompletedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
SizeBytes = table.Column<long>(type: "INTEGER", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MongoBackups", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_MongoBackups_MongoClusters_MongoClusterId",
|
||||
column: x => x.MongoClusterId,
|
||||
principalTable: "MongoClusters",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MongoDatabases",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
MongoClusterId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 63, nullable: false),
|
||||
Owner = table.Column<string>(type: "TEXT", nullable: false),
|
||||
Status = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MongoDatabases", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_MongoDatabases_MongoClusters_MongoClusterId",
|
||||
column: x => x.MongoClusterId,
|
||||
principalTable: "MongoClusters",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VaultSecrets_MongoDatabaseId",
|
||||
table: "VaultSecrets",
|
||||
column: "MongoDatabaseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MongoBackups_MongoClusterId_Name",
|
||||
table: "MongoBackups",
|
||||
columns: new[] { "MongoClusterId", "Name" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MongoClusters_KubernetesClusterId_Name_Namespace",
|
||||
table: "MongoClusters",
|
||||
columns: new[] { "KubernetesClusterId", "Name", "Namespace" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MongoClusters_StorageLinkId",
|
||||
table: "MongoClusters",
|
||||
column: "StorageLinkId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MongoClusters_TenantId",
|
||||
table: "MongoClusters",
|
||||
column: "TenantId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MongoDatabases_MongoClusterId_Name",
|
||||
table: "MongoDatabases",
|
||||
columns: new[] { "MongoClusterId", "Name" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_VaultSecrets_MongoDatabases_MongoDatabaseId",
|
||||
table: "VaultSecrets",
|
||||
column: "MongoDatabaseId",
|
||||
principalTable: "MongoDatabases",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_VaultSecrets_MongoDatabases_MongoDatabaseId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MongoBackups");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MongoDatabases");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MongoClusters");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_VaultSecrets_MongoDatabaseId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MongoDatabaseId",
|
||||
table: "VaultSecrets");
|
||||
}
|
||||
}
|
||||
}
|
||||
1993
src/EntKube.Web/Data/Migrations/Sqlite/20260518095805_AddDatabaseBindings.Designer.cs
generated
Normal file
1993
src/EntKube.Web/Data/Migrations/Sqlite/20260518095805_AddDatabaseBindings.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EntKube.Web.Data.Migrations.Sqlite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDatabaseBindings : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DatabaseBindings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
CnpgDatabaseId = table.Column<Guid>(type: "TEXT", nullable: true),
|
||||
MongoDatabaseId = table.Column<Guid>(type: "TEXT", nullable: true),
|
||||
AppDeploymentId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
KubernetesSecretName = table.Column<string>(type: "TEXT", maxLength: 253, nullable: false),
|
||||
SyncEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
LastSyncedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DatabaseBindings", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_DatabaseBindings_AppDeployments_AppDeploymentId",
|
||||
column: x => x.AppDeploymentId,
|
||||
principalTable: "AppDeployments",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_DatabaseBindings_CnpgDatabases_CnpgDatabaseId",
|
||||
column: x => x.CnpgDatabaseId,
|
||||
principalTable: "CnpgDatabases",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_DatabaseBindings_MongoDatabases_MongoDatabaseId",
|
||||
column: x => x.MongoDatabaseId,
|
||||
principalTable: "MongoDatabases",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DatabaseBindings_AppDeploymentId",
|
||||
table: "DatabaseBindings",
|
||||
column: "AppDeploymentId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DatabaseBindings_CnpgDatabaseId",
|
||||
table: "DatabaseBindings",
|
||||
column: "CnpgDatabaseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DatabaseBindings_MongoDatabaseId",
|
||||
table: "DatabaseBindings",
|
||||
column: "MongoDatabaseId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "DatabaseBindings");
|
||||
}
|
||||
}
|
||||
}
|
||||
2005
src/EntKube.Web/Data/Migrations/Sqlite/20260518142543_AddMongoClusterVaultSecret.Designer.cs
generated
Normal file
2005
src/EntKube.Web/Data/Migrations/Sqlite/20260518142543_AddMongoClusterVaultSecret.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EntKube.Web.Data.Migrations.Sqlite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddMongoClusterVaultSecret : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "MongoClusterId",
|
||||
table: "VaultSecrets",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VaultSecrets_MongoClusterId",
|
||||
table: "VaultSecrets",
|
||||
column: "MongoClusterId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_VaultSecrets_MongoClusters_MongoClusterId",
|
||||
table: "VaultSecrets",
|
||||
column: "MongoClusterId",
|
||||
principalTable: "MongoClusters",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_VaultSecrets_MongoClusters_MongoClusterId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_VaultSecrets_MongoClusterId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MongoClusterId",
|
||||
table: "VaultSecrets");
|
||||
}
|
||||
}
|
||||
}
|
||||
2212
src/EntKube.Web/Data/Migrations/Sqlite/20260519191909_AddKeycloakComponentConfig.Designer.cs
generated
Normal file
2212
src/EntKube.Web/Data/Migrations/Sqlite/20260519191909_AddKeycloakComponentConfig.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,200 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace EntKube.Web.Data.Migrations.Sqlite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddKeycloakComponentConfig : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_KeycloakRealms_KeycloakConnections_KeycloakConnectionId",
|
||||
table: "KeycloakRealms");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_VaultSecrets_KeycloakConnections_KeycloakConnectionId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "KeycloakConnections");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_VaultSecrets_KeycloakConnectionId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "KeycloakConnectionId",
|
||||
table: "VaultSecrets");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "KeycloakConnectionId",
|
||||
table: "KeycloakRealms",
|
||||
newName: "KeycloakComponentConfigId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_KeycloakRealms_KeycloakConnectionId_RealmName",
|
||||
table: "KeycloakRealms",
|
||||
newName: "IX_KeycloakRealms_KeycloakComponentConfigId_RealmName");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "KeycloakComponentConfigs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
ClusterComponentId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
CnpgDatabaseId = table.Column<Guid>(type: "TEXT", nullable: true),
|
||||
AdminUsername = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||
AdminUrl = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_KeycloakComponentConfigs", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_KeycloakComponentConfigs_ClusterComponents_ClusterComponentId",
|
||||
column: x => x.ClusterComponentId,
|
||||
principalTable: "ClusterComponents",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_KeycloakComponentConfigs_CnpgDatabases_CnpgDatabaseId",
|
||||
column: x => x.CnpgDatabaseId,
|
||||
principalTable: "CnpgDatabases",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_KeycloakComponentConfigs_Tenants_TenantId",
|
||||
column: x => x.TenantId,
|
||||
principalTable: "Tenants",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_KeycloakComponentConfigs_ClusterComponentId",
|
||||
table: "KeycloakComponentConfigs",
|
||||
column: "ClusterComponentId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_KeycloakComponentConfigs_CnpgDatabaseId",
|
||||
table: "KeycloakComponentConfigs",
|
||||
column: "CnpgDatabaseId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_KeycloakComponentConfigs_TenantId",
|
||||
table: "KeycloakComponentConfigs",
|
||||
column: "TenantId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_KeycloakRealms_KeycloakComponentConfigs_KeycloakComponentConfigId",
|
||||
table: "KeycloakRealms",
|
||||
column: "KeycloakComponentConfigId",
|
||||
principalTable: "KeycloakComponentConfigs",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_KeycloakRealms_KeycloakComponentConfigs_KeycloakComponentConfigId",
|
||||
table: "KeycloakRealms");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "KeycloakComponentConfigs");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "KeycloakComponentConfigId",
|
||||
table: "KeycloakRealms",
|
||||
newName: "KeycloakConnectionId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_KeycloakRealms_KeycloakComponentConfigId_RealmName",
|
||||
table: "KeycloakRealms",
|
||||
newName: "IX_KeycloakRealms_KeycloakConnectionId_RealmName");
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "KeycloakConnectionId",
|
||||
table: "VaultSecrets",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "KeycloakConnections",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
AdminPasswordSecretId = table.Column<Guid>(type: "TEXT", nullable: true),
|
||||
KubernetesClusterId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
AdminUrl = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false),
|
||||
AdminUsername = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_KeycloakConnections", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_KeycloakConnections_KubernetesClusters_KubernetesClusterId",
|
||||
column: x => x.KubernetesClusterId,
|
||||
principalTable: "KubernetesClusters",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_KeycloakConnections_Tenants_TenantId",
|
||||
column: x => x.TenantId,
|
||||
principalTable: "Tenants",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_KeycloakConnections_VaultSecrets_AdminPasswordSecretId",
|
||||
column: x => x.AdminPasswordSecretId,
|
||||
principalTable: "VaultSecrets",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VaultSecrets_KeycloakConnectionId",
|
||||
table: "VaultSecrets",
|
||||
column: "KeycloakConnectionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_KeycloakConnections_AdminPasswordSecretId",
|
||||
table: "KeycloakConnections",
|
||||
column: "AdminPasswordSecretId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_KeycloakConnections_KubernetesClusterId",
|
||||
table: "KeycloakConnections",
|
||||
column: "KubernetesClusterId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_KeycloakConnections_TenantId",
|
||||
table: "KeycloakConnections",
|
||||
column: "TenantId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_KeycloakRealms_KeycloakConnections_KeycloakConnectionId",
|
||||
table: "KeycloakRealms",
|
||||
column: "KeycloakConnectionId",
|
||||
principalTable: "KeycloakConnections",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_VaultSecrets_KeycloakConnections_KeycloakConnectionId",
|
||||
table: "VaultSecrets",
|
||||
column: "KeycloakConnectionId",
|
||||
principalTable: "KeycloakConnections",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,6 +337,9 @@ namespace EntKube.Web.Data.Migrations.Sqlite
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("RetentionDays")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -444,6 +447,46 @@ namespace EntKube.Web.Data.Migrations.Sqlite
|
||||
b.ToTable("CustomerAccesses");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.DatabaseBinding", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AppDeploymentId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("CnpgDatabaseId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("KubernetesSecretName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(253)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("LastSyncedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("MongoDatabaseId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("SyncEnabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AppDeploymentId");
|
||||
|
||||
b.HasIndex("CnpgDatabaseId");
|
||||
|
||||
b.HasIndex("MongoDatabaseId");
|
||||
|
||||
b.ToTable("DatabaseBindings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.DeploymentManifest", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -674,6 +717,140 @@ namespace EntKube.Web.Data.Migrations.Sqlite
|
||||
b.ToTable("GroupMemberships");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakBackup", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("KeycloakRealmId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LastError")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ObjectKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RealmName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("SizeBytes")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("StorageLinkId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("KeycloakRealmId");
|
||||
|
||||
b.HasIndex("StorageLinkId");
|
||||
|
||||
b.ToTable("KeycloakBackups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakComponentConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AdminUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AdminUsername")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ClusterComponentId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("CnpgDatabaseId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClusterComponentId");
|
||||
|
||||
b.HasIndex("CnpgDatabaseId");
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
|
||||
b.ToTable("KeycloakComponentConfigs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakRealm", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AccountTheme")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("KeycloakComponentConfigId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("LinkedAppId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LoginTheme")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RealmName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("LinkedAppId");
|
||||
|
||||
b.HasIndex("KeycloakComponentConfigId", "RealmName")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("KeycloakRealms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KubernetesCluster", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -715,6 +892,141 @@ namespace EntKube.Web.Data.Migrations.Sqlite
|
||||
b.ToTable("KubernetesClusters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoBackup", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("MongoClusterId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(253)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("SizeBytes")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("StartedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MongoClusterId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MongoBackups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoCluster", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("BackupSchedule")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("KubernetesClusterId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LastError")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Members")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("MongoVersion")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(63)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Namespace")
|
||||
.IsRequired()
|
||||
.HasMaxLength(63)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("RetentionDays")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("StorageLinkId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StorageSize")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("StorageLinkId");
|
||||
|
||||
b.HasIndex("TenantId");
|
||||
|
||||
b.HasIndex("KubernetesClusterId", "Name", "Namespace")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MongoClusters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoDatabase", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("MongoClusterId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(63)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Owner")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("MongoClusterId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MongoDatabases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.OpenStackConnection", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -983,6 +1295,12 @@ namespace EntKube.Web.Data.Migrations.Sqlite
|
||||
.HasMaxLength(253)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("MongoClusterId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("MongoDatabaseId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
@@ -1015,6 +1333,10 @@ namespace EntKube.Web.Data.Migrations.Sqlite
|
||||
|
||||
b.HasIndex("ComponentId");
|
||||
|
||||
b.HasIndex("MongoClusterId");
|
||||
|
||||
b.HasIndex("MongoDatabaseId");
|
||||
|
||||
b.HasIndex("StorageLinkId");
|
||||
|
||||
b.HasIndex("VaultId", "AppId", "Name")
|
||||
@@ -1300,6 +1622,31 @@ namespace EntKube.Web.Data.Migrations.Sqlite
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.DatabaseBinding", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.AppDeployment", "AppDeployment")
|
||||
.WithMany("DatabaseBindings")
|
||||
.HasForeignKey("AppDeploymentId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("EntKube.Web.Data.CnpgDatabase", "CnpgDatabase")
|
||||
.WithMany("DatabaseBindings")
|
||||
.HasForeignKey("CnpgDatabaseId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("EntKube.Web.Data.MongoDatabase", "MongoDatabase")
|
||||
.WithMany("DatabaseBindings")
|
||||
.HasForeignKey("MongoDatabaseId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("AppDeployment");
|
||||
|
||||
b.Navigation("CnpgDatabase");
|
||||
|
||||
b.Navigation("MongoDatabase");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.DeploymentManifest", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.AppDeployment", "Deployment")
|
||||
@@ -1381,6 +1728,69 @@ namespace EntKube.Web.Data.Migrations.Sqlite
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakBackup", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.KeycloakRealm", "Realm")
|
||||
.WithMany("Backups")
|
||||
.HasForeignKey("KeycloakRealmId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("EntKube.Web.Data.StorageLink", "StorageLink")
|
||||
.WithMany()
|
||||
.HasForeignKey("StorageLinkId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Realm");
|
||||
|
||||
b.Navigation("StorageLink");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakComponentConfig", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.ClusterComponent", "ClusterComponent")
|
||||
.WithMany()
|
||||
.HasForeignKey("ClusterComponentId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("EntKube.Web.Data.CnpgDatabase", "CnpgDatabase")
|
||||
.WithMany()
|
||||
.HasForeignKey("CnpgDatabaseId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("EntKube.Web.Data.Tenant", "Tenant")
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ClusterComponent");
|
||||
|
||||
b.Navigation("CnpgDatabase");
|
||||
|
||||
b.Navigation("Tenant");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakRealm", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.KeycloakComponentConfig", "ComponentConfig")
|
||||
.WithMany("Realms")
|
||||
.HasForeignKey("KeycloakComponentConfigId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("EntKube.Web.Data.App", "LinkedApp")
|
||||
.WithMany()
|
||||
.HasForeignKey("LinkedAppId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("ComponentConfig");
|
||||
|
||||
b.Navigation("LinkedApp");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KubernetesCluster", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.Environment", "Environment")
|
||||
@@ -1400,6 +1810,54 @@ namespace EntKube.Web.Data.Migrations.Sqlite
|
||||
b.Navigation("Tenant");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoBackup", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.MongoCluster", "MongoCluster")
|
||||
.WithMany("Backups")
|
||||
.HasForeignKey("MongoClusterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("MongoCluster");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoCluster", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.KubernetesCluster", "KubernetesCluster")
|
||||
.WithMany()
|
||||
.HasForeignKey("KubernetesClusterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("EntKube.Web.Data.StorageLink", "StorageLink")
|
||||
.WithMany()
|
||||
.HasForeignKey("StorageLinkId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("EntKube.Web.Data.Tenant", "Tenant")
|
||||
.WithMany()
|
||||
.HasForeignKey("TenantId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("KubernetesCluster");
|
||||
|
||||
b.Navigation("StorageLink");
|
||||
|
||||
b.Navigation("Tenant");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoDatabase", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.MongoCluster", "MongoCluster")
|
||||
.WithMany("Databases")
|
||||
.HasForeignKey("MongoClusterId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("MongoCluster");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.OpenStackConnection", b =>
|
||||
{
|
||||
b.HasOne("EntKube.Web.Data.Tenant", "Tenant")
|
||||
@@ -1533,6 +1991,16 @@ namespace EntKube.Web.Data.Migrations.Sqlite
|
||||
.HasForeignKey("ComponentId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("EntKube.Web.Data.MongoCluster", "MongoCluster")
|
||||
.WithMany()
|
||||
.HasForeignKey("MongoClusterId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("EntKube.Web.Data.MongoDatabase", "MongoDatabase")
|
||||
.WithMany()
|
||||
.HasForeignKey("MongoDatabaseId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("EntKube.Web.Data.StorageLink", "StorageLink")
|
||||
.WithMany()
|
||||
.HasForeignKey("StorageLinkId");
|
||||
@@ -1549,6 +2017,10 @@ namespace EntKube.Web.Data.Migrations.Sqlite
|
||||
|
||||
b.Navigation("Component");
|
||||
|
||||
b.Navigation("MongoCluster");
|
||||
|
||||
b.Navigation("MongoDatabase");
|
||||
|
||||
b.Navigation("StorageLink");
|
||||
|
||||
b.Navigation("Vault");
|
||||
@@ -1616,6 +2088,8 @@ namespace EntKube.Web.Data.Migrations.Sqlite
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.AppDeployment", b =>
|
||||
{
|
||||
b.Navigation("DatabaseBindings");
|
||||
|
||||
b.Navigation("Manifests");
|
||||
|
||||
b.Navigation("Resources");
|
||||
@@ -1639,6 +2113,11 @@ namespace EntKube.Web.Data.Migrations.Sqlite
|
||||
b.Navigation("Databases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.CnpgDatabase", b =>
|
||||
{
|
||||
b.Navigation("DatabaseBindings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.Customer", b =>
|
||||
{
|
||||
b.Navigation("Apps");
|
||||
@@ -1661,11 +2140,33 @@ namespace EntKube.Web.Data.Migrations.Sqlite
|
||||
b.Navigation("Memberships");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakComponentConfig", b =>
|
||||
{
|
||||
b.Navigation("Realms");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KeycloakRealm", b =>
|
||||
{
|
||||
b.Navigation("Backups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.KubernetesCluster", b =>
|
||||
{
|
||||
b.Navigation("Components");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoCluster", b =>
|
||||
{
|
||||
b.Navigation("Backups");
|
||||
|
||||
b.Navigation("Databases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.MongoDatabase", b =>
|
||||
{
|
||||
b.Navigation("DatabaseBindings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("EntKube.Web.Data.OpenStackConnection", b =>
|
||||
{
|
||||
b.Navigation("StorageLinks");
|
||||
|
||||
186
src/EntKube.Web/Data/MongoCluster.cs
Normal file
186
src/EntKube.Web/Data/MongoCluster.cs
Normal file
@@ -0,0 +1,186 @@
|
||||
namespace EntKube.Web.Data;
|
||||
|
||||
/// <summary>
|
||||
/// The lifecycle status of a managed Percona MongoDB cluster. Tracks where the
|
||||
/// cluster is in the provisioning/operational/teardown cycle.
|
||||
/// </summary>
|
||||
public enum MongoClusterStatus
|
||||
{
|
||||
Creating,
|
||||
Running,
|
||||
Upgrading,
|
||||
Restoring,
|
||||
Failed,
|
||||
Deleting
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A managed Percona Server for MongoDB cluster that EntKube provisions and controls.
|
||||
/// Each cluster lives on a Kubernetes cluster where the Percona MongoDB operator is installed.
|
||||
/// Optionally backed by a StorageLink (S3 bucket) for automated backups and point-in-time restore.
|
||||
///
|
||||
/// The cluster is represented as a PerconaServerMongoDB CRD (psmdb.percona.com/v1) in Kubernetes.
|
||||
/// EntKube owns the full lifecycle: create, upgrade, backup, restore, delete.
|
||||
/// </summary>
|
||||
public class MongoCluster
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The tenant that owns this cluster.
|
||||
/// </summary>
|
||||
public Guid TenantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Kubernetes cluster where this MongoDB cluster runs.
|
||||
/// Must have the Percona MongoDB operator installed.
|
||||
/// </summary>
|
||||
public Guid KubernetesClusterId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the PerconaServerMongoDB resource in Kubernetes (metadata.name).
|
||||
/// Lowercase, DNS-safe, max 63 chars.
|
||||
/// </summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Kubernetes namespace where the cluster lives.
|
||||
/// </summary>
|
||||
public required string Namespace { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The MongoDB version (e.g. "8.0", "7.0", "6.0").
|
||||
/// Maps to the Percona Server for MongoDB container image tag.
|
||||
/// </summary>
|
||||
public required string MongoVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of replica set members (1 = standalone, 3 = HA with automatic failover).
|
||||
/// </summary>
|
||||
public int Members { get; set; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// PVC storage size for each member (e.g. "10Gi", "50Gi").
|
||||
/// </summary>
|
||||
public required string StorageSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional link to an S3 bucket for automated backups and point-in-time restore.
|
||||
/// When set, the cluster is configured with Percona Backup for MongoDB (PBM).
|
||||
/// </summary>
|
||||
public Guid? StorageLinkId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cron schedule for automated backups (e.g. "0 2 * * *" for daily at 2 AM).
|
||||
/// Null means no scheduled backups — only on-demand.
|
||||
/// </summary>
|
||||
public string? BackupSchedule { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of days to retain backups. Percona Backup for MongoDB uses this
|
||||
/// to auto-delete old backups. Default is 30 days.
|
||||
/// </summary>
|
||||
public int RetentionDays { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Current lifecycle status of the cluster.
|
||||
/// </summary>
|
||||
public MongoClusterStatus Status { get; set; } = MongoClusterStatus.Creating;
|
||||
|
||||
/// <summary>
|
||||
/// The last error encountered during a lifecycle operation.
|
||||
/// Cleared on successful operations.
|
||||
/// </summary>
|
||||
public string? LastError { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation
|
||||
public Tenant Tenant { get; set; } = null!;
|
||||
public KubernetesCluster KubernetesCluster { get; set; } = null!;
|
||||
public StorageLink? StorageLink { get; set; }
|
||||
public ICollection<MongoDatabase> Databases { get; set; } = [];
|
||||
public ICollection<MongoBackup> Backups { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The status of a database within a managed MongoDB cluster.
|
||||
/// </summary>
|
||||
public enum MongoDatabaseStatus
|
||||
{
|
||||
Creating,
|
||||
Ready,
|
||||
Failed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A database (and its owner user) within a managed Percona MongoDB cluster.
|
||||
/// MongoDB databases are created implicitly when data is first written, but
|
||||
/// EntKube creates a dedicated user with readWrite permissions and stores
|
||||
/// the connection string in the vault.
|
||||
/// </summary>
|
||||
public class MongoDatabase
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid MongoClusterId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The database name in MongoDB.
|
||||
/// </summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The username that owns this database (has readWrite role).
|
||||
/// </summary>
|
||||
public required string Owner { get; set; }
|
||||
|
||||
public MongoDatabaseStatus Status { get; set; } = MongoDatabaseStatus.Creating;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation
|
||||
public MongoCluster MongoCluster { get; set; } = null!;
|
||||
public ICollection<DatabaseBinding> DatabaseBindings { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The type of a MongoDB backup — scheduled or triggered on-demand.
|
||||
/// </summary>
|
||||
public enum MongoBackupType
|
||||
{
|
||||
Scheduled,
|
||||
OnDemand
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The status of a MongoDB backup operation.
|
||||
/// </summary>
|
||||
public enum MongoBackupStatus
|
||||
{
|
||||
Running,
|
||||
Completed,
|
||||
Failed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A backup record for a managed Percona MongoDB cluster.
|
||||
/// Corresponds to a PerconaServerMongoDBBackup CRD in Kubernetes.
|
||||
/// </summary>
|
||||
public class MongoBackup
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid MongoClusterId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the backup CR in Kubernetes.
|
||||
/// </summary>
|
||||
public required string Name { get; set; }
|
||||
|
||||
public MongoBackupType Type { get; set; } = MongoBackupType.OnDemand;
|
||||
public MongoBackupStatus Status { get; set; } = MongoBackupStatus.Running;
|
||||
public DateTime StartedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? CompletedAt { get; set; }
|
||||
public long? SizeBytes { get; set; }
|
||||
|
||||
// Navigation
|
||||
public MongoCluster MongoCluster { get; set; } = null!;
|
||||
}
|
||||
@@ -56,6 +56,16 @@ public class VaultSecret
|
||||
/// </summary>
|
||||
public Guid? CnpgDatabaseId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If set, this secret belongs to a MongoDB database (connection credentials).
|
||||
/// </summary>
|
||||
public Guid? MongoDatabaseId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If set, this secret belongs to a managed MongoDB cluster (e.g. the admin password).
|
||||
/// </summary>
|
||||
public Guid? MongoClusterId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When true, this secret will be synced to Kubernetes as a Secret resource.
|
||||
/// </summary>
|
||||
@@ -82,4 +92,6 @@ public class VaultSecret
|
||||
public ClusterComponent? Component { get; set; }
|
||||
public StorageLink? StorageLink { get; set; }
|
||||
public CnpgDatabase? CnpgDatabase { get; set; }
|
||||
public MongoDatabase? MongoDatabase { get; set; }
|
||||
public MongoCluster? MongoCluster { get; set; }
|
||||
}
|
||||
|
||||
@@ -108,10 +108,12 @@ public class Program
|
||||
builder.Services.AddScoped<ExternalRouteService>();
|
||||
builder.Services.AddScoped<DatabaseService>();
|
||||
builder.Services.AddScoped<CnpgService>();
|
||||
builder.Services.AddScoped<MongoService>();
|
||||
builder.Services.AddScoped<IKubernetesClientFactory, KubernetesClientFactory>();
|
||||
builder.Services.AddScoped<OpenStackS3Service>();
|
||||
builder.Services.AddScoped<StorageService>();
|
||||
builder.Services.AddScoped<ComponentScanService>();
|
||||
builder.Services.AddScoped<KeycloakService>();
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -60,14 +60,6 @@ public class CatalogEntry
|
||||
/// </summary>
|
||||
public IReadOnlyList<DependencyRequirement> RequiresOneOf { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Companion components that should be auto-installed alongside this one.
|
||||
/// When this component installs successfully, any companions that don't already
|
||||
/// exist on the cluster will be registered and installed automatically.
|
||||
/// Unlike Dependencies (which block install if missing), companions are additive.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> CompanionKeys { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Form fields that provide a user-friendly way to configure the most common
|
||||
/// Helm values. These render as simple form controls (text boxes, selects,
|
||||
@@ -112,6 +104,22 @@ public static class ComponentCatalog
|
||||
[
|
||||
// ── Ingress ──
|
||||
|
||||
new CatalogEntry
|
||||
{
|
||||
Key = "gateway-api-crds",
|
||||
DisplayName = "Gateway API CRDs",
|
||||
Description = "Installs the Kubernetes Gateway API CRDs (experimental channel): HTTPRoute, TCPRoute, TLSRoute, UDPRoute, GRPCRoute, Gateway, GatewayClass. Required by Traefik and Istio Gateway API support.",
|
||||
Icon = "bi-diagram-2",
|
||||
Category = "Ingress",
|
||||
ComponentType = "ManifestUrl",
|
||||
HelmRepoUrl = "https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/experimental-install.yaml",
|
||||
HelmChartName = "",
|
||||
DefaultNamespace = "gateway-system",
|
||||
DefaultReleaseName = "gateway-api-crds",
|
||||
FormFields = [],
|
||||
DefaultValues = ""
|
||||
},
|
||||
|
||||
new CatalogEntry
|
||||
{
|
||||
Key = "traefik",
|
||||
@@ -146,10 +154,10 @@ public static class ComponentCatalog
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "gateway-api-crds", Label = "Install Gateway API CRDs",
|
||||
Key = "gateway-api-provider", Label = "Enable Gateway API Provider",
|
||||
YamlPath = "gatewayAPI.enabled", Type = FormFieldType.Toggle,
|
||||
DefaultValue = "true",
|
||||
HelpText = "Install the Gateway API CRD definitions with this chart"
|
||||
HelpText = "Enable Traefik's Gateway API provider (requires gateway-api-crds installed)"
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
@@ -203,10 +211,10 @@ public static class ComponentCatalog
|
||||
memory: 128Mi
|
||||
cpu: 100m
|
||||
|
||||
# Install Gateway API CRDs
|
||||
gatewayAPI:
|
||||
enabled: true
|
||||
"""
|
||||
""",
|
||||
Dependencies = ["gateway-api-crds"]
|
||||
},
|
||||
|
||||
new CatalogEntry
|
||||
@@ -220,7 +228,7 @@ public static class ComponentCatalog
|
||||
HelmChartName = "gateway",
|
||||
DefaultNamespace = "istio-system",
|
||||
DefaultReleaseName = "istio-ingress-external",
|
||||
Dependencies = ["istio-base"],
|
||||
Dependencies = ["gateway-api-crds", "istio-base"],
|
||||
FormFields =
|
||||
[
|
||||
new ComponentFormField
|
||||
@@ -270,7 +278,7 @@ public static class ComponentCatalog
|
||||
HelmChartName = "gateway",
|
||||
DefaultNamespace = "istio-system",
|
||||
DefaultReleaseName = "istio-ingress-internal",
|
||||
Dependencies = ["istio-base"],
|
||||
Dependencies = ["gateway-api-crds", "istio-base"],
|
||||
FormFields =
|
||||
[
|
||||
new ComponentFormField
|
||||
@@ -325,50 +333,39 @@ public static class ComponentCatalog
|
||||
new CatalogEntry
|
||||
{
|
||||
Key = "istio-base",
|
||||
DisplayName = "Istio Base (istiod)",
|
||||
Description = "Istio control plane (istiod). Required foundation for Istio service mesh — provides the control plane that manages proxies, certificates, and configuration.",
|
||||
DisplayName = "Istio Control Plane",
|
||||
Description = "Installs Istio CRDs (base) and the istiod control plane in one step. Required foundation for Istio service mesh — manages proxies, certificates, and Gateway API routing.",
|
||||
Icon = "bi-diagram-3",
|
||||
Category = "Ingress",
|
||||
HelmRepoUrl = "https://istio-release.storage.googleapis.com/charts",
|
||||
HelmChartName = "istiod",
|
||||
HelmChartName = "base",
|
||||
DefaultNamespace = "istio-system",
|
||||
DefaultReleaseName = "istiod",
|
||||
DefaultReleaseName = "istio-base",
|
||||
FormFields =
|
||||
[
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "gateway-api", Label = "Enable Gateway API",
|
||||
YamlPath = "pilot.env.PILOT_ENABLE_GATEWAY_API", Type = FormFieldType.Toggle,
|
||||
Key = "install-istiod", Label = "Install istiod control plane",
|
||||
YamlPath = "subchart:istiod", Type = FormFieldType.Toggle,
|
||||
DefaultValue = "true",
|
||||
HelpText = "Enable Gateway API support in the Istio control plane"
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "cpu-request", Label = "CPU Request",
|
||||
YamlPath = "resources.requests.cpu", Type = FormFieldType.Text,
|
||||
DefaultValue = "100m", Placeholder = "e.g. 100m, 250m"
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "memory-request", Label = "Memory Request",
|
||||
YamlPath = "resources.requests.memory", Type = FormFieldType.Text,
|
||||
DefaultValue = "256Mi", Placeholder = "e.g. 256Mi, 512Mi"
|
||||
HelpText = "Install the Istio control plane (istiod) alongside the base CRDs",
|
||||
SubchartDefaultValues = """
|
||||
# Enable Gateway API support in istiod
|
||||
pilot:
|
||||
env:
|
||||
PILOT_ENABLE_GATEWAY_API: "true"
|
||||
PILOT_ENABLE_GATEWAY_API_STATUS: "true"
|
||||
PILOT_ENABLE_GATEWAY_API_DEPLOYMENT_CONTROLLER: "true"
|
||||
|
||||
# Resource allocation
|
||||
resources:
|
||||
requests:
|
||||
memory: 256Mi
|
||||
cpu: 100m
|
||||
"""
|
||||
}
|
||||
],
|
||||
DefaultValues = """
|
||||
# Enable Gateway API support in istiod
|
||||
pilot:
|
||||
env:
|
||||
PILOT_ENABLE_GATEWAY_API: "true"
|
||||
PILOT_ENABLE_GATEWAY_API_STATUS: "true"
|
||||
PILOT_ENABLE_GATEWAY_API_DEPLOYMENT_CONTROLLER: "true"
|
||||
|
||||
# Resource allocation
|
||||
resources:
|
||||
requests:
|
||||
memory: 256Mi
|
||||
cpu: 100m
|
||||
"""
|
||||
Dependencies = ["gateway-api-crds"]
|
||||
},
|
||||
|
||||
// ── Certificate Management ──
|
||||
@@ -394,13 +391,6 @@ public static class ComponentCatalog
|
||||
HelpText = "Install cert-manager Custom Resource Definitions"
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "gateway-api", Label = "Gateway API Integration",
|
||||
YamlPath = "featureGates", Type = FormFieldType.Text,
|
||||
DefaultValue = "ExperimentalGatewayAPISupport=true",
|
||||
HelpText = "Feature gates to enable (Gateway API support recommended)"
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "cpu-request", Label = "CPU Request",
|
||||
YamlPath = "resources.requests.cpu", Type = FormFieldType.Text,
|
||||
@@ -417,11 +407,7 @@ public static class ComponentCatalog
|
||||
# Install CRDs with the chart
|
||||
crds:
|
||||
enabled: true
|
||||
|
||||
# Enable Gateway API integration
|
||||
# cert-manager will watch Gateway resources for TLS config
|
||||
featureGates: ExperimentalGatewayAPISupport=true
|
||||
|
||||
|
||||
# Resource allocation
|
||||
resources:
|
||||
requests:
|
||||
@@ -434,11 +420,11 @@ public static class ComponentCatalog
|
||||
{
|
||||
Key = "letsencrypt-issuer",
|
||||
DisplayName = "Let's Encrypt ClusterIssuer",
|
||||
Description = "Configures a ClusterIssuer that uses Let's Encrypt to automatically issue and renew TLS certificates. Works with both HTTP-01 and DNS-01 challenge types.",
|
||||
Description = "Configures a ClusterIssuer that uses Let's Encrypt to automatically issue and renew TLS certificates. Supports HTTP-01 (ingress) and DNS-01 (Azure DNS) challenge solvers — both can be active simultaneously.",
|
||||
Icon = "bi-lock",
|
||||
Category = "Certificate Management",
|
||||
HelmRepoUrl = "https://charts.jetstack.io",
|
||||
HelmChartName = "cert-manager",
|
||||
HelmChartName = "letsencrypt-issuer",
|
||||
DefaultNamespace = "cert-manager",
|
||||
DefaultReleaseName = "letsencrypt-issuer",
|
||||
Dependencies = ["cert-manager"],
|
||||
@@ -460,13 +446,60 @@ public static class ComponentCatalog
|
||||
DefaultValue = "https://acme-v02.api.letsencrypt.org/directory",
|
||||
Options = ["https://acme-v02.api.letsencrypt.org/directory", "https://acme-staging-v02.api.letsencrypt.org/directory"],
|
||||
HelpText = "Use staging for testing, production for real certificates"
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "dns-zone", Label = "DNS-01 Hosted Zone",
|
||||
YamlPath = "spec.acme.solvers.0.dns01.azureDNS.hostedZoneName",
|
||||
Type = FormFieldType.Text,
|
||||
Placeholder = "example.com",
|
||||
HelpText = "Azure DNS zone name (e.g. example.com)"
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "dns-resource-group", Label = "DNS Resource Group",
|
||||
YamlPath = "spec.acme.solvers.0.dns01.azureDNS.resourceGroupName",
|
||||
Type = FormFieldType.Text,
|
||||
Placeholder = "my-dns-rg",
|
||||
HelpText = "Azure resource group containing the DNS zone"
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "dns-subscription-id", Label = "DNS Subscription ID",
|
||||
YamlPath = "spec.acme.solvers.0.dns01.azureDNS.subscriptionID",
|
||||
Type = FormFieldType.Text,
|
||||
Placeholder = "00000000-0000-0000-0000-000000000000",
|
||||
HelpText = "Azure subscription ID containing the DNS zone"
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "dns-tenant-id", Label = "DNS Tenant ID",
|
||||
YamlPath = "spec.acme.solvers.0.dns01.azureDNS.tenantID",
|
||||
Type = FormFieldType.Text,
|
||||
Placeholder = "00000000-0000-0000-0000-000000000000",
|
||||
HelpText = "Azure AD tenant ID for the service principal"
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "dns-client-id", Label = "DNS Client ID",
|
||||
YamlPath = "spec.acme.solvers.0.dns01.azureDNS.clientID",
|
||||
Type = FormFieldType.Text,
|
||||
Placeholder = "00000000-0000-0000-0000-000000000000",
|
||||
HelpText = "Service principal (app registration) client ID"
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "dns-client-secret", Label = "DNS Client Secret",
|
||||
YamlPath = "",
|
||||
Type = FormFieldType.Password,
|
||||
StoreAsSecret = true,
|
||||
SecretName = "azuredns-client-secret",
|
||||
KubernetesSecretName = "azuredns-config",
|
||||
KubernetesSecretNamespace = "cert-manager",
|
||||
HelpText = "Service principal client secret — stored in vault and synced to K8s Secret 'azuredns-config'"
|
||||
}
|
||||
],
|
||||
DefaultValues = """
|
||||
# This component applies a ClusterIssuer manifest via Helm.
|
||||
# Change the email to your operations contact.
|
||||
|
||||
# Let's Encrypt Production
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
@@ -478,11 +511,17 @@ public static class ComponentCatalog
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-prod-key
|
||||
solvers:
|
||||
- http01:
|
||||
gatewayHTTPRoute:
|
||||
parentRefs:
|
||||
- name: traefik-gateway
|
||||
namespace: traefik
|
||||
- dns01:
|
||||
azureDNS:
|
||||
hostedZoneName: example.com
|
||||
resourceGroupName: my-dns-rg
|
||||
subscriptionID: 00000000-0000-0000-0000-000000000000
|
||||
tenantID: 00000000-0000-0000-0000-000000000000
|
||||
clientID: 00000000-0000-0000-0000-000000000000
|
||||
environment: AzurePublicCloud
|
||||
clientSecretSecretRef:
|
||||
name: azuredns-config
|
||||
key: azuredns-client-secret
|
||||
"""
|
||||
},
|
||||
|
||||
@@ -688,9 +727,14 @@ public static class ComponentCatalog
|
||||
HelmChartName = "cloudnative-pg",
|
||||
DefaultNamespace = "cnpg-system",
|
||||
DefaultReleaseName = "cnpg",
|
||||
CompanionKeys = ["barman-cloud-plugin"],
|
||||
FormFields =
|
||||
[
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "barman-cloud-plugin", Label = "Install Barman Cloud Plugin",
|
||||
YamlPath = "subchart:plugin-barman-cloud", Type = FormFieldType.Toggle,
|
||||
DefaultValue = "true"
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "cpu-request", Label = "CPU Request",
|
||||
@@ -715,27 +759,9 @@ public static class ComponentCatalog
|
||||
|
||||
new CatalogEntry
|
||||
{
|
||||
Key = "barman-cloud-plugin",
|
||||
DisplayName = "Barman Cloud Plugin",
|
||||
Description = "Backup and recovery plugin for CloudNativePG. Provides S3-compatible backup via Barman Cloud, WAL archiving, and point-in-time recovery. Required for CNPG backup/restore operations.",
|
||||
Icon = "bi-cloud-arrow-up",
|
||||
Category = "Databases",
|
||||
HelmRepoUrl = "https://cloudnative-pg.github.io/charts",
|
||||
HelmChartName = "barman-cloud",
|
||||
DefaultNamespace = "cnpg-system",
|
||||
DefaultReleaseName = "barman-cloud",
|
||||
Dependencies = ["cloudnative-pg"],
|
||||
DefaultValues = """
|
||||
# Barman Cloud Plugin configuration
|
||||
# Installed as companion to CloudNativePG operator
|
||||
"""
|
||||
},
|
||||
|
||||
new CatalogEntry
|
||||
{
|
||||
Key = "mongodb-operator",
|
||||
Key = "mongodb-community-operator",
|
||||
DisplayName = "MongoDB Community Operator",
|
||||
Description = "Kubernetes operator for deploying and managing MongoDB replica sets. Handles provisioning, scaling, upgrades, and TLS configuration.",
|
||||
Description = "Official MongoDB Kubernetes operator for deploying and managing MongoDB replica sets. Handles provisioning, scaling, upgrades, and user management. Backups are managed independently via S3-backed Jobs.",
|
||||
Icon = "bi-database-gear",
|
||||
Category = "Databases",
|
||||
HelmRepoUrl = "https://mongodb.github.io/helm-charts",
|
||||
@@ -799,6 +825,70 @@ public static class ComponentCatalog
|
||||
Dependencies = ["cert-manager", "letsencrypt-issuer", "cloudnative-pg"],
|
||||
FormFields =
|
||||
[
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "cnpg-database", Label = "Database",
|
||||
YamlPath = "cnpg:database-id", Type = FormFieldType.CnpgDatabase,
|
||||
HelpText = "Managed PostgreSQL database for Keycloak (credentials synced to K8s Secret)"
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "admin-url", Label = "Admin URL",
|
||||
YamlPath = "cnpg:admin-url", Type = FormFieldType.Text,
|
||||
Placeholder = "https://keycloak.example.com/auth",
|
||||
HelpText = "Public URL of the Keycloak admin console"
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "admin-username", Label = "Admin Username",
|
||||
YamlPath = "cnpg:admin-username", Type = FormFieldType.Text,
|
||||
DefaultValue = "admin"
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "admin-password", Label = "Admin Password",
|
||||
YamlPath = "cnpg:admin-password", Type = FormFieldType.Password,
|
||||
Placeholder = "Strong password for the Keycloak admin account",
|
||||
HelpText = "Stored in vault and synced to Kubernetes Secret"
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "hostname", Label = "Public Hostname",
|
||||
YamlPath = "cnpg:hostname", Type = FormFieldType.Text,
|
||||
Placeholder = "keycloak.example.com",
|
||||
HelpText = "Creates an HTTPRoute and Certificate to publish Keycloak"
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "tls-mode", Label = "TLS Mode",
|
||||
YamlPath = "cnpg:tls-mode", Type = FormFieldType.Select,
|
||||
DefaultValue = "ClusterIssuer",
|
||||
Options = ["ClusterIssuer", "Manual"]
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "cluster-issuer", Label = "Cluster Issuer",
|
||||
YamlPath = "cnpg:cluster-issuer", Type = FormFieldType.ClusterIssuer,
|
||||
DefaultValue = "letsencrypt-prod",
|
||||
HelpText = "cert-manager ClusterIssuer to request the TLS certificate",
|
||||
DependsOnKey = "tls-mode", DependsOnValue = "ClusterIssuer"
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "tls-cert", Label = "TLS Certificate (PEM)",
|
||||
YamlPath = "cnpg:tls-cert", Type = FormFieldType.Password,
|
||||
Placeholder = "-----BEGIN CERTIFICATE-----",
|
||||
HelpText = "Full certificate chain in PEM format",
|
||||
DependsOnKey = "tls-mode", DependsOnValue = "Manual"
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "tls-key", Label = "TLS Private Key (PEM)",
|
||||
YamlPath = "cnpg:tls-key", Type = FormFieldType.Password,
|
||||
Placeholder = "-----BEGIN PRIVATE KEY-----",
|
||||
HelpText = "Private key in PEM format",
|
||||
DependsOnKey = "tls-mode", DependsOnValue = "Manual"
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "http-path", Label = "HTTP Relative Path",
|
||||
@@ -807,14 +897,6 @@ public static class ComponentCatalog
|
||||
HelpText = "Base path where Keycloak is served"
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "db-vendor", Label = "Database Vendor",
|
||||
YamlPath = "database.vendor", Type = FormFieldType.Select,
|
||||
DefaultValue = "postgres",
|
||||
Options = ["postgres", "mariadb", "mysql"],
|
||||
HelpText = "Database backend for Keycloak"
|
||||
},
|
||||
new ComponentFormField
|
||||
{
|
||||
Key = "cpu-request", Label = "CPU Request",
|
||||
YamlPath = "resources.requests.cpu", Type = FormFieldType.Text,
|
||||
@@ -828,14 +910,40 @@ public static class ComponentCatalog
|
||||
}
|
||||
],
|
||||
DefaultValues = """
|
||||
# Database configuration (requires CloudNativePG)
|
||||
# Database vendor — credentials (KC_DB_URL/USERNAME/PASSWORD) are injected via the
|
||||
# K8s Secret below, not set here directly.
|
||||
database:
|
||||
vendor: postgres
|
||||
|
||||
|
||||
# All env vars (KC_BOOTSTRAP_ADMIN_USERNAME, KC_BOOTSTRAP_ADMIN_PASSWORD,
|
||||
# KC_DB_URL, KC_DB_USERNAME, KC_DB_PASSWORD) are written to this secret by
|
||||
# EntKube and read here at startup. The name must match {releaseName}-credentials.
|
||||
extraEnvFrom: |
|
||||
- secretRef:
|
||||
name: keycloak-credentials
|
||||
|
||||
# Disable the chart's legacy proxy setting — it injects KC_PROXY which was
|
||||
# removed in Keycloak 26 and causes kc.sh to exit with the help text.
|
||||
proxy:
|
||||
enabled: false
|
||||
|
||||
# Keycloak 26+ proxy configuration replaces KC_PROXY=edge.
|
||||
# KC_HTTP_ENABLED is set by the chart via http.relativePath — omit here to avoid duplicates.
|
||||
extraEnv: |
|
||||
- name: KC_PROXY_HEADERS
|
||||
value: xforwarded
|
||||
- name: KC_HOSTNAME_STRICT
|
||||
value: "false"
|
||||
|
||||
# Explicit start args — overrides the chart's default which may include
|
||||
# --hostname-strict=false (removed in Keycloak 26, causes help-text crash).
|
||||
args:
|
||||
- start
|
||||
|
||||
# HTTP configuration
|
||||
http:
|
||||
relativePath: /auth
|
||||
|
||||
|
||||
# Resource allocation
|
||||
resources:
|
||||
requests:
|
||||
@@ -845,6 +953,30 @@ public static class ComponentCatalog
|
||||
}
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Returns a mapping of subchart release names to their parent catalog key.
|
||||
/// For example: {"plugin-barman-cloud" → "cloudnative-pg"}.
|
||||
/// Used during scan to identify releases that are subcharts of a parent.
|
||||
/// </summary>
|
||||
public static Dictionary<string, string> GetSubchartParents()
|
||||
{
|
||||
Dictionary<string, string> result = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (CatalogEntry entry in Entries)
|
||||
{
|
||||
foreach (ComponentFormField field in entry.FormFields)
|
||||
{
|
||||
if (field.YamlPath.StartsWith("subchart:", StringComparison.Ordinal))
|
||||
{
|
||||
string subchartName = field.YamlPath["subchart:".Length..];
|
||||
result[subchartName] = entry.Key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a catalog entry by its key. Returns null if not found.
|
||||
/// </summary>
|
||||
@@ -859,11 +991,28 @@ public static class ComponentCatalog
|
||||
/// </summary>
|
||||
public static CatalogEntry? FindByRelease(string releaseName, string? chartName)
|
||||
{
|
||||
return Entries.FirstOrDefault(e =>
|
||||
// First try exact matches on Key, DefaultReleaseName, or HelmChartName.
|
||||
|
||||
CatalogEntry? exact = Entries.FirstOrDefault(e =>
|
||||
string.Equals(e.Key, releaseName, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(e.DefaultReleaseName, releaseName, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(e.HelmChartName, releaseName, StringComparison.OrdinalIgnoreCase)
|
||||
|| (!string.IsNullOrEmpty(chartName) && string.Equals(e.HelmChartName, chartName, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
if (exact is not null)
|
||||
{
|
||||
return exact;
|
||||
}
|
||||
|
||||
// Fall back to partial matching — the release name might contain the
|
||||
// catalog's DefaultReleaseName or Key as a substring, or vice versa.
|
||||
// For example, release "cloudnative-pg-cnpg" matching entry with Key "cloudnative-pg".
|
||||
|
||||
return Entries.FirstOrDefault(e =>
|
||||
(!string.IsNullOrEmpty(e.DefaultReleaseName)
|
||||
&& (releaseName.Contains(e.DefaultReleaseName, StringComparison.OrdinalIgnoreCase)
|
||||
|| e.DefaultReleaseName.Contains(releaseName, StringComparison.OrdinalIgnoreCase)))
|
||||
|| releaseName.Contains(e.Key, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -875,11 +1024,13 @@ public static class ComponentCatalog
|
||||
public static bool IsComponentMatch(string componentName, CatalogEntry entry,
|
||||
string? helmChartName = null, string? releaseName = null)
|
||||
{
|
||||
// Direct matches on Key, DefaultReleaseName, or HelmChartName.
|
||||
// Direct match on Key or DefaultReleaseName.
|
||||
// Intentionally does NOT match componentName against entry.HelmChartName —
|
||||
// that path would cause "cert-manager" to match the letsencrypt-issuer entry.
|
||||
// HelmChartName matching is handled below via the component's own HelmChartName field.
|
||||
|
||||
if (string.Equals(componentName, entry.Key, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(componentName, entry.DefaultReleaseName, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(componentName, entry.HelmChartName, StringComparison.OrdinalIgnoreCase))
|
||||
|| string.Equals(componentName, entry.DefaultReleaseName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -890,9 +1041,14 @@ public static class ComponentCatalog
|
||||
if (!string.IsNullOrEmpty(helmChartName)
|
||||
&& string.Equals(helmChartName, entry.HelmChartName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// HelmChartName alone can be ambiguous (e.g. both istio-external and istio-internal use "gateway").
|
||||
// Also check that the component's HelmRepoUrl matches to disambiguate.
|
||||
// But for now, if the chart name matches, treat it as installed.
|
||||
// Chart name alone is ambiguous when multiple catalog entries share the same chart
|
||||
// (e.g. "istio" and "istio-internal" both use the "gateway" chart).
|
||||
// Disambiguate using the release name when both sides have one.
|
||||
if (!string.IsNullOrEmpty(releaseName) && !string.IsNullOrEmpty(entry.DefaultReleaseName))
|
||||
{
|
||||
return string.Equals(releaseName, entry.DefaultReleaseName, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(releaseName, entry.Key, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -920,7 +1076,18 @@ public static class ComponentCatalog
|
||||
|
||||
foreach ((string name, string? chartName, string? releaseName) in components)
|
||||
{
|
||||
CatalogEntry? match = Entries.FirstOrDefault(e => IsComponentMatch(name, e, chartName, releaseName));
|
||||
// Prefer a direct key/releaseName match to avoid HelmChartName collisions.
|
||||
// Example: letsencrypt-issuer has HelmChartName="cert-manager", so a fuzzy
|
||||
// first-match would incorrectly resolve it to the cert-manager catalog entry.
|
||||
CatalogEntry? match =
|
||||
Entries.FirstOrDefault(e =>
|
||||
string.Equals(name, e.Key, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(name, e.DefaultReleaseName, StringComparison.OrdinalIgnoreCase)
|
||||
|| (!string.IsNullOrEmpty(releaseName)
|
||||
&& (string.Equals(releaseName, e.Key, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(releaseName, e.DefaultReleaseName, StringComparison.OrdinalIgnoreCase))))
|
||||
?? Entries.FirstOrDefault(e => IsComponentMatch(name, e, chartName, releaseName));
|
||||
|
||||
resolved.Add(match?.Key ?? name);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,10 @@ public class HelmCommand
|
||||
public string? Version { get; set; }
|
||||
public bool HasValues { get; set; }
|
||||
public string? ValuesYaml { get; set; }
|
||||
/// <summary>For kubectl-apply-url: the remote manifest URL passed directly to kubectl apply -f.</summary>
|
||||
public string? ManifestUrl { get; set; }
|
||||
/// <summary>When true, skips --wait so Helm returns immediately after applying values.</summary>
|
||||
public bool NoWait { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -178,9 +182,9 @@ public class ComponentLifecycleService(IDbContextFactory<ApplicationDbContext> d
|
||||
"Component has an operation in progress. Wait for it to complete.");
|
||||
}
|
||||
|
||||
// Validate the minimum required fields for a Helm install.
|
||||
// Validate minimum required fields — ManifestUrl components use a URL instead of a chart name.
|
||||
|
||||
if (string.IsNullOrWhiteSpace(component.HelmChartName))
|
||||
if (component.ComponentType != "ManifestUrl" && string.IsNullOrWhiteSpace(component.HelmChartName))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Helm chart name is required. Configure the component before installing.");
|
||||
@@ -227,77 +231,225 @@ public class ComponentLifecycleService(IDbContextFactory<ApplicationDbContext> d
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a successfully installed component has companion charts defined
|
||||
/// in the catalog. If so, registers and installs each companion that doesn't
|
||||
/// already exist on the cluster. Called after a successful install/upgrade.
|
||||
/// Checks if a component has any subchart toggle fields enabled (YamlPath
|
||||
/// starting with "subchart:"). For each enabled subchart, runs a helm
|
||||
/// upgrade --install using the same repo URL and namespace as the parent.
|
||||
/// The chart name is extracted from the YamlPath (e.g. "subchart:barman-cloud").
|
||||
///
|
||||
/// For example: installing cloudnative-pg will auto-install barman-cloud-plugin.
|
||||
/// For example: cloudnative-pg with "barman-cloud-plugin" toggle enabled will
|
||||
/// install the "barman-cloud" chart from the same CNPG charts repo.
|
||||
/// </summary>
|
||||
public async Task<List<HelmExecutionResult>> InstallCompanionsAsync(
|
||||
public async Task<HelmExecutionResult> InstallSubchartsAsync(
|
||||
Guid componentId, CancellationToken ct = default)
|
||||
{
|
||||
List<HelmExecutionResult> results = [];
|
||||
|
||||
using ApplicationDbContext db = dbFactory.CreateDbContext();
|
||||
|
||||
ClusterComponent component = await db.ClusterComponents
|
||||
.Include(c => c.Cluster)
|
||||
.FirstOrDefaultAsync(c => c.Id == componentId, ct)
|
||||
?? throw new InvalidOperationException("Component not found.");
|
||||
|
||||
// Look up the catalog entry for this component to find its companions.
|
||||
// Look up the catalog entry to find subchart toggle fields.
|
||||
|
||||
CatalogEntry? entry = ComponentCatalog.GetByKey(component.Name);
|
||||
|
||||
if (entry is null || entry.CompanionKeys.Count == 0)
|
||||
if (entry is null)
|
||||
{
|
||||
return results;
|
||||
return new HelmExecutionResult { Success = true, Output = "" };
|
||||
}
|
||||
|
||||
// For each companion, check if it already exists on this cluster.
|
||||
// If not, register it and run a full install cycle.
|
||||
// Parse the stored form field values from the component's HelmValues isn't
|
||||
// how toggles work — they're stored directly. We need to read them from
|
||||
// the catalog defaults + any override. For subchart toggles, the value is
|
||||
// stored as part of the component's configuration via editFormFieldValues.
|
||||
// Since these don't go into YAML, we check the catalog default.
|
||||
|
||||
foreach (string companionKey in entry.CompanionKeys)
|
||||
StringBuilder output = new();
|
||||
bool allSuccess = true;
|
||||
|
||||
foreach (ComponentFormField field in entry.FormFields)
|
||||
{
|
||||
CatalogEntry? companion = ComponentCatalog.GetByKey(companionKey);
|
||||
|
||||
if (companion is null)
|
||||
if (!field.YamlPath.StartsWith("subchart:", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool alreadyExists = await db.ClusterComponents
|
||||
.AnyAsync(c => c.ClusterId == component.ClusterId && c.Name == companionKey, ct);
|
||||
// The chart name is after the "subchart:" prefix.
|
||||
|
||||
if (alreadyExists)
|
||||
string subchartName = field.YamlPath["subchart:".Length..];
|
||||
|
||||
// Determine if the toggle is enabled. Check if the component has stored
|
||||
// a "false" override — otherwise default to the catalog default value.
|
||||
|
||||
bool enabled = IsSubchartEnabled(component, field);
|
||||
|
||||
if (!enabled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Register the companion component.
|
||||
// Build and execute the helm install for the subchart using the parent's
|
||||
// repo URL and namespace.
|
||||
|
||||
ComponentRegistration registration = ComponentCatalog.ToRegistration(companion);
|
||||
ClusterComponent companionComponent = await RegisterComponentAsync(
|
||||
component.ClusterId, registration, ct);
|
||||
string repoUrl = component.HelmRepoUrl ?? entry.HelmRepoUrl ?? "";
|
||||
string ns = component.Namespace ?? entry.DefaultNamespace ?? "default";
|
||||
string kubeconfig = component.Cluster.Kubeconfig ?? "";
|
||||
|
||||
// Run the full install cycle: prepare → get command → execute → mark result.
|
||||
|
||||
try
|
||||
if (string.IsNullOrWhiteSpace(kubeconfig))
|
||||
{
|
||||
await PrepareInstallAsync(companionComponent.Id, ct);
|
||||
HelmCommand command = await GetInstallCommandAsync(companionComponent.Id, ct);
|
||||
HelmExecutionResult result = await ExecuteHelmAsync(companionComponent.Id, command, ct);
|
||||
await MarkInstallResultAsync(companionComponent.Id, result.Success,
|
||||
result.Success ? null : result.Output, ct);
|
||||
|
||||
results.Add(result);
|
||||
return new HelmExecutionResult
|
||||
{
|
||||
Success = false,
|
||||
Output = "No kubeconfig stored for this cluster."
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
string? subchartValues = !string.IsNullOrWhiteSpace(field.SubchartDefaultValues)
|
||||
? field.SubchartDefaultValues
|
||||
: null;
|
||||
|
||||
HelmCommand subCommand = new()
|
||||
{
|
||||
await MarkInstallResultAsync(companionComponent.Id, false, ex.Message, ct);
|
||||
results.Add(new HelmExecutionResult { Success = false, Output = ex.Message });
|
||||
Operation = "upgrade --install",
|
||||
ReleaseName = subchartName,
|
||||
ChartReference = $"{repoUrl}/{subchartName}",
|
||||
Namespace = ns,
|
||||
RepoUrl = repoUrl,
|
||||
HasValues = subchartValues is not null,
|
||||
ValuesYaml = subchartValues
|
||||
};
|
||||
|
||||
HelmExecutionResult result = await ExecuteHelmAsync(componentId, subCommand, ct);
|
||||
output.AppendLine($"--- Subchart: {subchartName} ---");
|
||||
output.AppendLine(result.Output);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
allSuccess = false;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
return new HelmExecutionResult
|
||||
{
|
||||
Success = allSuccess,
|
||||
Output = output.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a subchart toggle is enabled for a component by checking
|
||||
/// whether the component's HelmValues contains a marker comment for the field.
|
||||
/// Since subchart toggles don't map to YAML paths, we check the default value
|
||||
/// from the catalog — unless the component has an explicit override stored.
|
||||
/// </summary>
|
||||
private static bool IsSubchartEnabled(ClusterComponent component, ComponentFormField field)
|
||||
{
|
||||
// Subchart toggles store their value as a comment marker in HelmValues:
|
||||
// "# subchart:barman-cloud=true" or "# subchart:barman-cloud=false"
|
||||
// If no marker exists, fall back to the catalog default.
|
||||
|
||||
string marker = $"# {field.YamlPath}=";
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(component.HelmValues))
|
||||
{
|
||||
foreach (string line in component.HelmValues.Split('\n'))
|
||||
{
|
||||
if (line.TrimStart().StartsWith(marker, StringComparison.Ordinal))
|
||||
{
|
||||
string value = line.TrimStart()[marker.Length..].Trim();
|
||||
return string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No explicit override — use catalog default.
|
||||
|
||||
return string.Equals(field.DefaultValue, "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uninstalls any subcharts (e.g. Barman Cloud Plugin) that were installed
|
||||
/// alongside the parent component. The subchart list is derived from the
|
||||
/// catalog entry's toggle fields; only enabled subcharts are removed.
|
||||
/// Returns the combined Helm output for all subchart uninstalls.
|
||||
/// </summary>
|
||||
public async Task<HelmExecutionResult> UninstallSubchartsAsync(
|
||||
Guid componentId, CancellationToken ct = default)
|
||||
{
|
||||
using ApplicationDbContext db = dbFactory.CreateDbContext();
|
||||
|
||||
ClusterComponent component = await db.ClusterComponents
|
||||
.Include(c => c.Cluster)
|
||||
.FirstOrDefaultAsync(c => c.Id == componentId, ct)
|
||||
?? throw new InvalidOperationException("Component not found.");
|
||||
|
||||
CatalogEntry? entry = ComponentCatalog.GetByKey(component.Name);
|
||||
|
||||
if (entry is null)
|
||||
{
|
||||
return new HelmExecutionResult { Success = true, Output = "" };
|
||||
}
|
||||
|
||||
StringBuilder output = new();
|
||||
bool allSuccess = true;
|
||||
|
||||
foreach (ComponentFormField field in entry.FormFields)
|
||||
{
|
||||
if (!field.YamlPath.StartsWith("subchart:", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string subchartName = field.YamlPath["subchart:".Length..];
|
||||
|
||||
if (!IsSubchartEnabled(component, field))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string ns = component.Namespace ?? entry.DefaultNamespace ?? "default";
|
||||
string kubeconfig = component.Cluster.Kubeconfig ?? "";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(kubeconfig))
|
||||
{
|
||||
return new HelmExecutionResult
|
||||
{
|
||||
Success = false,
|
||||
Output = "No kubeconfig stored for this cluster."
|
||||
};
|
||||
}
|
||||
|
||||
HelmCommand subCommand = new()
|
||||
{
|
||||
Operation = "uninstall",
|
||||
ReleaseName = subchartName,
|
||||
Namespace = ns
|
||||
};
|
||||
|
||||
HelmExecutionResult result = await ExecuteHelmAsync(componentId, subCommand, ct);
|
||||
|
||||
// "release: not found" means the subchart was never installed (e.g. the install
|
||||
// failed before Helm recorded the release). Treat this as already-uninstalled
|
||||
// so a missing subchart never blocks the parent component from being removed.
|
||||
if (!result.Success && result.Output.Contains("not found", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result = new HelmExecutionResult { Success = true, Output = result.Output };
|
||||
}
|
||||
|
||||
output.AppendLine($"--- Subchart: {subchartName} ---");
|
||||
output.AppendLine(result.Output);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
allSuccess = false;
|
||||
}
|
||||
}
|
||||
|
||||
return new HelmExecutionResult
|
||||
{
|
||||
Success = allSuccess,
|
||||
Output = output.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -408,6 +560,19 @@ public class ComponentLifecycleService(IDbContextFactory<ApplicationDbContext> d
|
||||
};
|
||||
}
|
||||
|
||||
// ManifestUrl components use kubectl apply -f <url> directly.
|
||||
// HelmRepoUrl holds the manifest URL; no local YAML needed.
|
||||
|
||||
if (component.ComponentType == "ManifestUrl")
|
||||
{
|
||||
return new HelmCommand
|
||||
{
|
||||
Operation = "kubectl-apply-url",
|
||||
ReleaseName = component.ReleaseName ?? component.Name,
|
||||
ManifestUrl = component.HelmRepoUrl
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve any vault secrets and inject them into the values YAML.
|
||||
// Secret form fields (like Grafana admin password) are stored encrypted
|
||||
// in the vault rather than in plain text in HelmValues. At install time,
|
||||
@@ -462,6 +627,17 @@ public class ComponentLifecycleService(IDbContextFactory<ApplicationDbContext> d
|
||||
};
|
||||
}
|
||||
|
||||
// ManifestUrl CRD bundles are cluster-scoped infra — skip uninstall to avoid breaking dependents.
|
||||
|
||||
if (component.ComponentType == "ManifestUrl")
|
||||
{
|
||||
return new HelmCommand
|
||||
{
|
||||
Operation = "noop",
|
||||
ReleaseName = releaseName
|
||||
};
|
||||
}
|
||||
|
||||
return new HelmCommand
|
||||
{
|
||||
Operation = "uninstall",
|
||||
@@ -614,6 +790,10 @@ public class ComponentLifecycleService(IDbContextFactory<ApplicationDbContext> d
|
||||
string k8sSecretName = group.Key.SecretName;
|
||||
string ns = group.Key.Namespace;
|
||||
|
||||
// Ensure the namespace exists before writing the secret.
|
||||
// The pod needs the secret at startup, which may be before Helm creates the namespace.
|
||||
await RunProcessAsync("kubectl", $"create namespace {ns} --kubeconfig {tempKubeconfig}", ct);
|
||||
|
||||
// Decrypt each secret value and build --from-literal args.
|
||||
|
||||
List<string> literals = [];
|
||||
@@ -707,11 +887,21 @@ public class ComponentLifecycleService(IDbContextFactory<ApplicationDbContext> d
|
||||
|
||||
// Route to the appropriate executor based on operation type.
|
||||
|
||||
if (command.Operation == "noop")
|
||||
{
|
||||
return new HelmExecutionResult { Success = true, Output = "No action taken (CRD bundles are not uninstalled automatically)." };
|
||||
}
|
||||
|
||||
if (command.Operation is "kubectl-apply" or "kubectl-delete")
|
||||
{
|
||||
return await ExecuteKubectlAsync(command, tempKubeconfig, ct);
|
||||
}
|
||||
|
||||
if (command.Operation == "kubectl-apply-url")
|
||||
{
|
||||
return await ExecuteKubectlUrlAsync(command, tempKubeconfig, ct);
|
||||
}
|
||||
|
||||
return await ExecuteHelmCliAsync(command, tempKubeconfig, component.Cluster.Kubeconfig, ct);
|
||||
}
|
||||
finally
|
||||
@@ -775,6 +965,94 @@ public class ComponentLifecycleService(IDbContextFactory<ApplicationDbContext> d
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a remote manifest URL directly via kubectl apply -f <url>.
|
||||
/// Used for components like Gateway API CRDs where the authoritative source
|
||||
/// is a GitHub release manifest rather than a Helm chart.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Applies all ExternalRoute resources for a component to its cluster via kubectl.
|
||||
/// Generates an HTTPRoute + Certificate manifest for each route and applies them.
|
||||
/// Safe to call repeatedly — kubectl apply is idempotent.
|
||||
/// </summary>
|
||||
public async Task<HelmExecutionResult> ApplyExternalRoutesAsync(
|
||||
Guid componentId, CancellationToken ct = default)
|
||||
{
|
||||
using ApplicationDbContext db = dbFactory.CreateDbContext();
|
||||
|
||||
// Load the full cluster (all components + their routes) so the Gateway manifest
|
||||
// covers every exposed hostname — not just this component's routes.
|
||||
ClusterComponent component = await db.ClusterComponents
|
||||
.Include(c => c.Cluster)
|
||||
.ThenInclude(cl => cl.Components)
|
||||
.ThenInclude(comp => comp.ExternalRoutes)
|
||||
.FirstOrDefaultAsync(c => c.Id == componentId, ct)
|
||||
?? throw new InvalidOperationException("Component not found.");
|
||||
|
||||
List<ExternalRoute> allRoutes = [];
|
||||
foreach (ClusterComponent comp in component.Cluster.Components)
|
||||
{
|
||||
foreach (ExternalRoute r in comp.ExternalRoutes)
|
||||
r.Component = comp;
|
||||
allRoutes.AddRange(comp.ExternalRoutes);
|
||||
}
|
||||
|
||||
if (allRoutes.Count == 0)
|
||||
{
|
||||
return new HelmExecutionResult { Success = true, Output = "" };
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(component.Cluster.Kubeconfig))
|
||||
{
|
||||
return new HelmExecutionResult { Success = false, Output = "No kubeconfig stored for this cluster." };
|
||||
}
|
||||
|
||||
(string gatewayName, string gatewayNamespace) = ExternalRouteService.ResolveGateway(
|
||||
component.Cluster.Components);
|
||||
|
||||
// Gateway resource (HTTPS listeners + HTTP redirect + per-hostname Certificates
|
||||
// in the gateway namespace so the Gateway's certificateRefs can resolve them).
|
||||
string gatewayYaml = ExternalRouteService.GenerateGatewayYaml(
|
||||
gatewayName, gatewayNamespace, allRoutes);
|
||||
|
||||
// One HTTPRoute per route — TLS is terminated at the Gateway, no per-route
|
||||
// Certificate needed here.
|
||||
IEnumerable<string> httpRoutes = allRoutes.Select(ExternalRouteService.GenerateHttpRouteYaml);
|
||||
|
||||
string combinedYaml = string.Join("\n---\n", new[] { gatewayYaml }.Concat(httpRoutes));
|
||||
|
||||
string tempKubeconfig = Path.Combine(Path.GetTempPath(), $"entkube-{Guid.NewGuid()}.kubeconfig");
|
||||
string tempManifest = Path.Combine(Path.GetTempPath(), $"entkube-routes-{Guid.NewGuid()}.yaml");
|
||||
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(tempKubeconfig, component.Cluster.Kubeconfig, ct);
|
||||
await File.WriteAllTextAsync(tempManifest, combinedYaml, ct);
|
||||
return await RunProcessAsync("kubectl", $"apply -f {tempManifest} --kubeconfig {tempKubeconfig}", ct);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempKubeconfig)) File.Delete(tempKubeconfig);
|
||||
if (File.Exists(tempManifest)) File.Delete(tempManifest);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<HelmExecutionResult> ExecuteKubectlUrlAsync(
|
||||
HelmCommand command, string kubeconfigPath, CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(command.ManifestUrl))
|
||||
{
|
||||
return new HelmExecutionResult
|
||||
{
|
||||
Success = false,
|
||||
Output = "No manifest URL configured for this component."
|
||||
};
|
||||
}
|
||||
|
||||
string arguments = $"apply -f {command.ManifestUrl} --kubeconfig {kubeconfigPath}";
|
||||
return await RunProcessAsync("kubectl", arguments, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a Helm CLI command — handles repo add, upgrade --install, or uninstall.
|
||||
/// When no repo URL is configured for an existing release, extracts the chart
|
||||
@@ -836,9 +1114,12 @@ public class ComponentLifecycleService(IDbContextFactory<ApplicationDbContext> d
|
||||
|
||||
args.Add("--kubeconfig");
|
||||
args.Add(kubeconfigPath);
|
||||
args.Add("--wait");
|
||||
args.Add("--timeout");
|
||||
args.Add("5m0s");
|
||||
if (!command.NoWait)
|
||||
{
|
||||
args.Add("--wait");
|
||||
args.Add("--timeout");
|
||||
args.Add("10m0s");
|
||||
}
|
||||
|
||||
// If there's a repo URL, add the repo first and resolve the chart reference.
|
||||
|
||||
@@ -1101,11 +1382,17 @@ public class ComponentLifecycleService(IDbContextFactory<ApplicationDbContext> d
|
||||
string stdout = await stdoutTask;
|
||||
string stderr = await stderrTask;
|
||||
|
||||
// Always combine stdout and stderr so errors aren't silently dropped.
|
||||
// helm prints informational messages (e.g. "Installing it now.") to stdout
|
||||
// and errors (timeouts, render failures) to stderr. If we only show stdout,
|
||||
// the real failure reason is invisible.
|
||||
string combined = (stdout.Trim() + (string.IsNullOrWhiteSpace(stderr) ? "" : "\n" + stderr.Trim())).Trim();
|
||||
|
||||
return new HelmExecutionResult
|
||||
{
|
||||
Success = process.ExitCode == 0,
|
||||
ExitCode = process.ExitCode,
|
||||
Output = string.IsNullOrWhiteSpace(stdout) ? stderr : stdout
|
||||
Output = combined
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -28,6 +28,13 @@ public class DiscoveredHelmRelease
|
||||
/// True if a ClusterComponent with this name already exists on the cluster.
|
||||
/// </summary>
|
||||
public bool AlreadyTracked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If this release is a known subchart of another component, the parent
|
||||
/// catalog key is stored here (e.g. "cloudnative-pg" for plugin-barman-cloud).
|
||||
/// The UI can use this to hide subcharts or group them with their parent.
|
||||
/// </summary>
|
||||
public string? ParentComponentKey { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -109,12 +116,24 @@ public class ComponentScanService(IDbContextFactory<ApplicationDbContext> dbFact
|
||||
// Get existing components on this cluster to mark already-tracked ones.
|
||||
|
||||
List<ClusterComponent> existingComponents = await vaultService.GetComponentsAsync(cluster.Id, ct);
|
||||
HashSet<string> trackedNames = existingComponents
|
||||
.Select(c => c.Name.ToLowerInvariant())
|
||||
.ToHashSet();
|
||||
|
||||
// Include both Name and ReleaseName so that components registered via the
|
||||
// catalog (where Name = catalog key, e.g. "cloudnative-pg") are still
|
||||
// matched when the Helm release name differs (e.g. "cnpg").
|
||||
HashSet<string> trackedNames = new(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (ClusterComponent c in existingComponents)
|
||||
{
|
||||
trackedNames.Add(c.Name);
|
||||
if (!string.IsNullOrEmpty(c.ReleaseName))
|
||||
{
|
||||
trackedNames.Add(c.ReleaseName);
|
||||
}
|
||||
}
|
||||
|
||||
// Decode each latest-revision release secret into a DiscoveredHelmRelease.
|
||||
|
||||
Dictionary<string, string> subchartParents = ComponentCatalog.GetSubchartParents();
|
||||
|
||||
foreach (KeyValuePair<string, (V1Secret secret, int revision)> entry in latestByRelease)
|
||||
{
|
||||
(V1Secret secret, int revision) = entry.Value;
|
||||
@@ -122,7 +141,15 @@ public class ComponentScanService(IDbContextFactory<ApplicationDbContext> dbFact
|
||||
|
||||
if (release is not null)
|
||||
{
|
||||
release.AlreadyTracked = trackedNames.Contains(release.Name.ToLowerInvariant());
|
||||
release.AlreadyTracked = trackedNames.Contains(release.Name);
|
||||
|
||||
// If this release is a known subchart, tag it with its parent key.
|
||||
|
||||
if (subchartParents.TryGetValue(release.Name, out string? parentKey))
|
||||
{
|
||||
release.ParentComponentKey = parentKey;
|
||||
}
|
||||
|
||||
releases.Add(release);
|
||||
}
|
||||
}
|
||||
@@ -244,7 +271,7 @@ public class ComponentScanService(IDbContextFactory<ApplicationDbContext> dbFact
|
||||
int imported = 0;
|
||||
List<string> errors = [];
|
||||
|
||||
foreach (DiscoveredHelmRelease release in releases.Where(r => !r.AlreadyTracked))
|
||||
foreach (DiscoveredHelmRelease release in releases.Where(r => !r.AlreadyTracked && r.ParentComponentKey is null))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -670,12 +697,17 @@ public class ComponentScanService(IDbContextFactory<ApplicationDbContext> dbFact
|
||||
/// <summary>
|
||||
/// Decodes a Helm release secret into a DiscoveredHelmRelease.
|
||||
/// The encoding chain is: K8s base64 → Helm base64 → gzip → JSON.
|
||||
/// Falls back to label-based discovery if data decoding fails.
|
||||
/// </summary>
|
||||
private static DiscoveredHelmRelease? DecodeHelmRelease(V1Secret secret, int revision)
|
||||
{
|
||||
// If the secret data is missing or doesn't have the "release" key,
|
||||
// fall back to building a minimal release from the secret's labels.
|
||||
// This can happen when RBAC strips data or storage format differs.
|
||||
|
||||
if (secret.Data is null || !secret.Data.TryGetValue("release", out byte[]? rawData))
|
||||
{
|
||||
return null;
|
||||
return BuildFallbackRelease(secret, revision);
|
||||
}
|
||||
|
||||
try
|
||||
@@ -684,15 +716,29 @@ public class ComponentScanService(IDbContextFactory<ApplicationDbContext> dbFact
|
||||
// Step 2: Helm base64-encodes the release data before storing.
|
||||
|
||||
string helmBase64 = Encoding.UTF8.GetString(rawData);
|
||||
byte[] gzipped = Convert.FromBase64String(helmBase64);
|
||||
byte[] compressed = Convert.FromBase64String(helmBase64);
|
||||
|
||||
// Step 3: Decompress gzip.
|
||||
// Step 3: Decompress. Helm uses gzip by default, but newer versions
|
||||
// or custom builds may use zstd (magic bytes 0x28 0xB5 0x2F 0xFD).
|
||||
// Detect by the first two bytes (gzip magic: 0x1F 0x8B).
|
||||
|
||||
using MemoryStream compressedStream = new(gzipped);
|
||||
using GZipStream gzipStream = new(compressedStream, CompressionMode.Decompress);
|
||||
using MemoryStream decompressedStream = new();
|
||||
gzipStream.CopyTo(decompressedStream);
|
||||
byte[] jsonBytes = decompressedStream.ToArray();
|
||||
byte[] jsonBytes;
|
||||
|
||||
if (compressed.Length >= 2 && compressed[0] == 0x1F && compressed[1] == 0x8B)
|
||||
{
|
||||
// Standard gzip compression.
|
||||
using MemoryStream compressedStream = new(compressed);
|
||||
using GZipStream gzipStream = new(compressedStream, CompressionMode.Decompress);
|
||||
using MemoryStream decompressedStream = new();
|
||||
gzipStream.CopyTo(decompressedStream);
|
||||
jsonBytes = decompressedStream.ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not gzip — try interpreting as raw JSON (some Helm drivers
|
||||
// store uncompressed data), or fall back to labels.
|
||||
jsonBytes = compressed;
|
||||
}
|
||||
|
||||
// Step 4: Parse JSON release object.
|
||||
|
||||
@@ -773,19 +819,34 @@ public class ComponentScanService(IDbContextFactory<ApplicationDbContext> dbFact
|
||||
{
|
||||
// If decoding fails for any reason, build a minimal release from labels.
|
||||
|
||||
string? fallbackName = secret.Metadata?.Labels?.TryGetValue("name", out string? fn) == true ? fn : null;
|
||||
string? fallbackStatus = secret.Metadata?.Labels?.TryGetValue("status", out string? fs) == true ? fs : null;
|
||||
|
||||
return new DiscoveredHelmRelease
|
||||
{
|
||||
Name = fallbackName ?? "unknown",
|
||||
Namespace = secret.Metadata?.NamespaceProperty ?? "default",
|
||||
Status = fallbackStatus,
|
||||
Revision = revision
|
||||
};
|
||||
return BuildFallbackRelease(secret, revision);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a minimal DiscoveredHelmRelease from the secret's labels when full
|
||||
/// decoding isn't possible. This ensures the release still appears in scan
|
||||
/// results even if data decoding fails.
|
||||
/// </summary>
|
||||
private static DiscoveredHelmRelease? BuildFallbackRelease(V1Secret secret, int revision)
|
||||
{
|
||||
string? fallbackName = secret.Metadata?.Labels?.TryGetValue("name", out string? fn) == true ? fn : null;
|
||||
string? fallbackStatus = secret.Metadata?.Labels?.TryGetValue("status", out string? fs) == true ? fs : null;
|
||||
|
||||
if (fallbackName is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DiscoveredHelmRelease
|
||||
{
|
||||
Name = fallbackName,
|
||||
Namespace = secret.Metadata?.NamespaceProperty ?? "default",
|
||||
Status = fallbackStatus,
|
||||
Revision = revision
|
||||
};
|
||||
}
|
||||
|
||||
private static ComponentStatus MapStatus(string? helmStatus)
|
||||
{
|
||||
return helmStatus?.ToLowerInvariant() switch
|
||||
@@ -847,6 +908,60 @@ public class ComponentScanService(IDbContextFactory<ApplicationDbContext> dbFact
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the Gateway API CRDs (httproutes.gateway.networking.k8s.io) are
|
||||
/// installed on the cluster. Returns false if unreachable or CRDs are absent.
|
||||
/// </summary>
|
||||
public async Task<bool> CheckGatewayApiCrdsAsync(
|
||||
KubernetesCluster cluster, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cluster.Kubeconfig))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Kubernetes client = CreateClient(cluster.Kubeconfig);
|
||||
await client.ApiextensionsV1.ReadCustomResourceDefinitionAsync(
|
||||
"httproutes.gateway.networking.k8s.io", cancellationToken: ct);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether cert-manager has been started with Gateway API support enabled.
|
||||
/// Returns false if cert-manager is not installed or does not have the feature gate.
|
||||
/// </summary>
|
||||
public async Task<bool> CheckCertManagerGatewayApiAsync(
|
||||
KubernetesCluster cluster, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cluster.Kubeconfig))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Kubernetes client = CreateClient(cluster.Kubeconfig);
|
||||
k8s.Models.V1Deployment deployment = await client.AppsV1.ReadNamespacedDeploymentAsync(
|
||||
"cert-manager", "cert-manager", cancellationToken: ct);
|
||||
|
||||
return deployment.Spec?.Template?.Spec?.Containers?.Any(c =>
|
||||
c.Args?.Any(a =>
|
||||
a.Contains("GatewayAPI=true", StringComparison.OrdinalIgnoreCase) ||
|
||||
a.Contains("ExperimentalGatewayAPISupport=true", StringComparison.OrdinalIgnoreCase)) == true) == true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Kubernetes CreateClient(string kubeconfig)
|
||||
{
|
||||
using MemoryStream stream = new(Encoding.UTF8.GetBytes(kubeconfig));
|
||||
|
||||
@@ -80,11 +80,15 @@ public class DatabaseService(IDbContextFactory<ApplicationDbContext> dbFactory)
|
||||
.Include(c => c.Cluster)
|
||||
.Where(c => c.Cluster.TenantId == tenantId
|
||||
&& c.Status == ComponentStatus.Installed
|
||||
&& (c.Name == "cloudnative-pg" || c.Name == "mongodb-operator"))
|
||||
&& (c.Name == "cloudnative-pg"
|
||||
|| c.Name == "mongodb-community-operator" || c.Name == "mongodb-operator"
|
||||
|| c.ReleaseName == "mongodb-community-operator" || c.ReleaseName == "mongodb-operator"))
|
||||
.ToListAsync(ct);
|
||||
|
||||
ClusterComponent? cnpg = installedComponents.FirstOrDefault(c => c.Name == "cloudnative-pg");
|
||||
ClusterComponent? mongo = installedComponents.FirstOrDefault(c => c.Name == "mongodb-operator");
|
||||
ClusterComponent? mongo = installedComponents.FirstOrDefault(c =>
|
||||
c.Name is "mongodb-community-operator" or "mongodb-operator"
|
||||
|| c.ReleaseName is "mongodb-community-operator" or "mongodb-operator");
|
||||
|
||||
return new DatabaseOperatorStatus
|
||||
{
|
||||
@@ -112,6 +116,26 @@ public class DatabaseService(IDbContextFactory<ApplicationDbContext> dbFactory)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all Kubernetes clusters that have the Percona MongoDB operator installed.
|
||||
/// Used by the UI to populate the target cluster dropdown when creating
|
||||
/// a new managed MongoDB cluster.
|
||||
/// </summary>
|
||||
public async Task<List<KubernetesCluster>> GetMongoEnabledClustersAsync(
|
||||
Guid tenantId, CancellationToken ct = default)
|
||||
{
|
||||
using ApplicationDbContext db = dbFactory.CreateDbContext();
|
||||
|
||||
return await db.KubernetesClusters
|
||||
.Where(k => k.TenantId == tenantId
|
||||
&& k.Components.Any(c =>
|
||||
(c.Name == "mongodb-community-operator" || c.Name == "mongodb-operator"
|
||||
|| c.ReleaseName == "mongodb-community-operator" || c.ReleaseName == "mongodb-operator")
|
||||
&& c.Status == ComponentStatus.Installed))
|
||||
.OrderBy(k => k.Name)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
// ──────── CNPG Discovery ────────
|
||||
|
||||
/// <summary>
|
||||
@@ -271,9 +295,9 @@ public class DatabaseService(IDbContextFactory<ApplicationDbContext> dbFactory)
|
||||
// ──────── MongoDB Discovery ────────
|
||||
|
||||
/// <summary>
|
||||
/// Discovers all MongoDB Community resources across the tenant's clusters.
|
||||
/// Reads the mongodbcommunity.mongodb.com/v1 MongoDBCommunity CRD from every
|
||||
/// cluster that has the mongodb-operator installed.
|
||||
/// Discovers all Percona Server for MongoDB resources across the tenant's clusters.
|
||||
/// Reads the psmdb.percona.com/v1 PerconaServerMongoDB CRD from every
|
||||
/// cluster that has the mongodb-community-operator installed.
|
||||
/// </summary>
|
||||
public async Task<List<DatabaseClusterInfo>> GetMongoDbClustersAsync(
|
||||
Guid tenantId, CancellationToken ct = default)
|
||||
@@ -283,7 +307,10 @@ public class DatabaseService(IDbContextFactory<ApplicationDbContext> dbFactory)
|
||||
List<KubernetesCluster> clusters = await db.KubernetesClusters
|
||||
.Include(k => k.Environment)
|
||||
.Where(k => k.TenantId == tenantId
|
||||
&& k.Components.Any(c => c.Name == "mongodb-operator" && c.Status == ComponentStatus.Installed))
|
||||
&& k.Components.Any(c =>
|
||||
(c.Name == "mongodb-community-operator" || c.Name == "mongodb-operator"
|
||||
|| c.ReleaseName == "mongodb-community-operator" || c.ReleaseName == "mongodb-operator")
|
||||
&& c.Status == ComponentStatus.Installed))
|
||||
.ToListAsync(ct);
|
||||
|
||||
List<DatabaseClusterInfo> results = [];
|
||||
@@ -351,32 +378,28 @@ public class DatabaseService(IDbContextFactory<ApplicationDbContext> dbFactory)
|
||||
string name = metadata.GetProperty("name").GetString() ?? "unknown";
|
||||
string ns = metadata.GetProperty("namespace").GetString() ?? "default";
|
||||
|
||||
int members = spec.TryGetProperty("members", out JsonElement membersEl)
|
||||
? membersEl.GetInt32() : 1;
|
||||
// Community Operator uses spec.members directly.
|
||||
|
||||
string? version = spec.TryGetProperty("version", out JsonElement verEl)
|
||||
? verEl.GetString() : null;
|
||||
int members = 1;
|
||||
|
||||
// Extract database users and their associated databases.
|
||||
if (spec.TryGetProperty("members", out JsonElement membersEl))
|
||||
{
|
||||
members = membersEl.GetInt32();
|
||||
}
|
||||
|
||||
// Version is in spec.version (e.g. "8.0.8").
|
||||
|
||||
string? version = null;
|
||||
|
||||
if (spec.TryGetProperty("version", out JsonElement versionEl))
|
||||
{
|
||||
version = versionEl.GetString();
|
||||
}
|
||||
|
||||
// Databases are not listed in the CRD — implicit in MongoDB.
|
||||
|
||||
List<string> databases = [];
|
||||
|
||||
if (spec.TryGetProperty("users", out JsonElement users))
|
||||
{
|
||||
foreach (JsonElement user in users.EnumerateArray())
|
||||
{
|
||||
if (user.TryGetProperty("db", out JsonElement dbEl))
|
||||
{
|
||||
string? dbName = dbEl.GetString();
|
||||
|
||||
if (!string.IsNullOrEmpty(dbName) && !databases.Contains(dbName))
|
||||
{
|
||||
databases.Add(dbName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Status from the CR.
|
||||
|
||||
string status = "Unknown";
|
||||
|
||||
@@ -167,6 +167,19 @@ public class ExternalRouteService(IDbContextFactory<ApplicationDbContext> dbFact
|
||||
return GenerateHttpRouteYaml(route);
|
||||
}
|
||||
|
||||
public async Task<string> GenerateFullManifestYamlAsync(
|
||||
Guid routeId, CancellationToken ct = default)
|
||||
{
|
||||
using ApplicationDbContext db = dbFactory.CreateDbContext();
|
||||
|
||||
ExternalRoute route = await db.ExternalRoutes
|
||||
.Include(r => r.Component)
|
||||
.FirstOrDefaultAsync(r => r.Id == routeId, ct)
|
||||
?? throw new InvalidOperationException("Route not found.");
|
||||
|
||||
return GenerateFullManifestYaml(route);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates HTTPRoute YAML from an in-memory route object.
|
||||
/// Useful for previewing before saving.
|
||||
@@ -175,26 +188,9 @@ public class ExternalRouteService(IDbContextFactory<ApplicationDbContext> dbFact
|
||||
{
|
||||
string ns = route.Component?.Namespace ?? "default";
|
||||
string routeName = $"{route.ServiceName}-route";
|
||||
string secretName = $"{route.ServiceName}-tls";
|
||||
|
||||
// Build the annotations section based on TLS mode.
|
||||
|
||||
string annotations = route.TlsMode == TlsMode.ClusterIssuer
|
||||
? $" cert-manager.io/cluster-issuer: \"{route.ClusterIssuerName}\""
|
||||
: $" entkube.io/tls-mode: \"manual\"";
|
||||
|
||||
// Build the TLS section — for manual, reference a Secret that holds the cert.
|
||||
|
||||
string tlsSection = route.TlsMode == TlsMode.Manual
|
||||
? $"""
|
||||
tls:
|
||||
certificateRefs:
|
||||
- name: {secretName}
|
||||
kind: Secret
|
||||
"""
|
||||
: "";
|
||||
|
||||
// Build path match rules.
|
||||
// TLS is terminated at the Gateway listener — HTTPRoute only routes by hostname/path.
|
||||
// No TLS section belongs in HTTPRoute.spec.
|
||||
|
||||
string pathMatch = route.PathPrefix != "/"
|
||||
? $"""
|
||||
@@ -212,25 +208,66 @@ public class ExternalRouteService(IDbContextFactory<ApplicationDbContext> dbFact
|
||||
port: {route.ServicePort}
|
||||
""";
|
||||
|
||||
string yaml = $"""
|
||||
return $"""
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: {routeName}
|
||||
namespace: {ns}
|
||||
annotations:
|
||||
{annotations}
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: {route.GatewayName}
|
||||
namespace: {route.GatewayNamespace}
|
||||
{tlsSection} hostnames:
|
||||
hostnames:
|
||||
- {route.Hostname}
|
||||
rules:
|
||||
{pathMatch}
|
||||
""";
|
||||
}
|
||||
|
||||
return yaml;
|
||||
/// <summary>
|
||||
/// Generates a cert-manager Certificate resource for ClusterIssuer TLS mode.
|
||||
/// cert-manager will provision and renew the TLS secret automatically.
|
||||
/// Returns empty string for Manual TLS (user supplies the certificate).
|
||||
/// </summary>
|
||||
public static string GenerateCertificateYaml(ExternalRoute route)
|
||||
{
|
||||
if (route.TlsMode != TlsMode.ClusterIssuer || string.IsNullOrWhiteSpace(route.ClusterIssuerName))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
string ns = route.Component?.Namespace ?? "default";
|
||||
string secretName = $"{route.ServiceName}-tls";
|
||||
|
||||
return $"""
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: {secretName}
|
||||
namespace: {ns}
|
||||
spec:
|
||||
secretName: {secretName}
|
||||
issuerRef:
|
||||
name: {route.ClusterIssuerName}
|
||||
kind: ClusterIssuer
|
||||
dnsNames:
|
||||
- {route.Hostname}
|
||||
""";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the complete manifest for a route: HTTPRoute plus a Certificate
|
||||
/// resource (when using ClusterIssuer TLS). Apply this single YAML to the cluster.
|
||||
/// </summary>
|
||||
public static string GenerateFullManifestYaml(ExternalRoute route)
|
||||
{
|
||||
string httpRoute = GenerateHttpRouteYaml(route);
|
||||
string certificate = GenerateCertificateYaml(route);
|
||||
|
||||
return string.IsNullOrEmpty(certificate)
|
||||
? httpRoute
|
||||
: $"{httpRoute}\n---\n{certificate}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -270,43 +307,186 @@ public class ExternalRouteService(IDbContextFactory<ApplicationDbContext> dbFact
|
||||
return yaml;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a <c>gateway.networking.k8s.io/v1/Gateway</c> resource with one HTTPS
|
||||
/// listener per unique hostname in <paramref name="routes"/>, plus an HTTP-to-HTTPS
|
||||
/// redirect listener, and explicit cert-manager Certificate resources placed in
|
||||
/// <paramref name="gatewayNamespace"/> so they can be referenced by the Gateway's
|
||||
/// <c>certificateRefs</c> without cross-namespace ReferenceGrants.
|
||||
///
|
||||
/// The returned YAML contains multiple documents separated by <c>---</c>:
|
||||
/// 1. The Gateway resource
|
||||
/// 2. An HTTP→HTTPS redirect HTTPRoute
|
||||
/// 3. One Certificate resource per ClusterIssuer-mode hostname
|
||||
/// </summary>
|
||||
public static string GenerateGatewayYaml(
|
||||
string gatewayName,
|
||||
string gatewayNamespace,
|
||||
IEnumerable<ExternalRoute> routes)
|
||||
{
|
||||
var grouped = routes
|
||||
.GroupBy(r => r.Hostname)
|
||||
.Select(g => (
|
||||
Hostname: g.Key,
|
||||
ListenerName: ToListenerName(g.Key),
|
||||
CertSecretName: ToCertSecretName(g.Key),
|
||||
ClusterIssuerName: g.Select(r => r.ClusterIssuerName).FirstOrDefault(n => n != null),
|
||||
IsCertIssuer: g.Any(r => r.TlsMode == TlsMode.ClusterIssuer)
|
||||
))
|
||||
.ToList();
|
||||
|
||||
IEnumerable<string> httpsListeners = grouped.Select(g =>
|
||||
$" - name: {g.ListenerName}\n" +
|
||||
$" hostname: {g.Hostname}\n" +
|
||||
$" port: 443\n" +
|
||||
$" protocol: HTTPS\n" +
|
||||
$" tls:\n" +
|
||||
$" mode: Terminate\n" +
|
||||
$" certificateRefs:\n" +
|
||||
$" - name: {g.CertSecretName}\n" +
|
||||
$" allowedRoutes:\n" +
|
||||
$" namespaces:\n" +
|
||||
$" from: All");
|
||||
|
||||
const string httpListener =
|
||||
" - name: http-redirect\n" +
|
||||
" port: 80\n" +
|
||||
" protocol: HTTP\n" +
|
||||
" allowedRoutes:\n" +
|
||||
" namespaces:\n" +
|
||||
" from: Same";
|
||||
|
||||
string allListeners = string.Join("\n", httpsListeners.Append(httpListener));
|
||||
|
||||
// No cert-manager.io/cluster-issuer annotation on the Gateway — we create
|
||||
// explicit Certificate resources below, so the gateway-shim is not needed.
|
||||
// Using the annotation alongside explicit Certificates causes the gateway-shim
|
||||
// to fight over the same Certificate objects, which prevents cert-manager
|
||||
// from ever starting the ACME order.
|
||||
string gatewayYaml =
|
||||
$"apiVersion: gateway.networking.k8s.io/v1\n" +
|
||||
$"kind: Gateway\n" +
|
||||
$"metadata:\n" +
|
||||
$" name: {gatewayName}\n" +
|
||||
$" namespace: {gatewayNamespace}\n" +
|
||||
$" annotations:\n" +
|
||||
$" app.kubernetes.io/managed-by: entkube\n" +
|
||||
$"spec:\n" +
|
||||
$" gatewayClassName: istio\n" +
|
||||
$" addresses:\n" +
|
||||
$" - type: Hostname\n" +
|
||||
$" value: {gatewayName}.{gatewayNamespace}.svc.cluster.local\n" +
|
||||
$" listeners:\n" +
|
||||
allListeners;
|
||||
|
||||
string httpRedirectRoute =
|
||||
$"apiVersion: gateway.networking.k8s.io/v1\n" +
|
||||
$"kind: HTTPRoute\n" +
|
||||
$"metadata:\n" +
|
||||
$" name: http-to-https-redirect\n" +
|
||||
$" namespace: {gatewayNamespace}\n" +
|
||||
$"spec:\n" +
|
||||
$" parentRefs:\n" +
|
||||
$" - name: {gatewayName}\n" +
|
||||
$" namespace: {gatewayNamespace}\n" +
|
||||
$" sectionName: http-redirect\n" +
|
||||
$" rules:\n" +
|
||||
$" - filters:\n" +
|
||||
$" - type: RequestRedirect\n" +
|
||||
$" requestRedirect:\n" +
|
||||
$" scheme: https\n" +
|
||||
$" statusCode: 301";
|
||||
|
||||
List<string> parts = [gatewayYaml, httpRedirectRoute];
|
||||
|
||||
foreach (var g in grouped.Where(g => g.IsCertIssuer && !string.IsNullOrWhiteSpace(g.ClusterIssuerName)))
|
||||
{
|
||||
parts.Add(
|
||||
$"apiVersion: cert-manager.io/v1\n" +
|
||||
$"kind: Certificate\n" +
|
||||
$"metadata:\n" +
|
||||
$" name: {g.CertSecretName}\n" +
|
||||
$" namespace: {gatewayNamespace}\n" +
|
||||
$"spec:\n" +
|
||||
$" secretName: {g.CertSecretName}\n" +
|
||||
$" issuerRef:\n" +
|
||||
$" name: {g.ClusterIssuerName}\n" +
|
||||
$" kind: ClusterIssuer\n" +
|
||||
$" dnsNames:\n" +
|
||||
$" - {g.Hostname}");
|
||||
}
|
||||
|
||||
return string.Join("\n---\n", parts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes a hostname into a valid Kubernetes resource name / listener name
|
||||
/// by replacing non-alphanumeric characters with dashes, trimming edge dashes,
|
||||
/// and capping at 63 characters (DNS label limit).
|
||||
/// </summary>
|
||||
private static string ToListenerName(string hostname)
|
||||
{
|
||||
string sanitized = new string(hostname.ToLowerInvariant()
|
||||
.Select(c => char.IsLetterOrDigit(c) ? c : '-')
|
||||
.ToArray())
|
||||
.Trim('-');
|
||||
return sanitized.Length > 63 ? sanitized[..63] : sanitized;
|
||||
}
|
||||
|
||||
/// <summary>Derives the TLS secret name from a hostname (used in both the Gateway listener and Certificate).</summary>
|
||||
public static string ToCertSecretName(string hostname) => ToListenerName(hostname) + "-tls";
|
||||
|
||||
// ── Private helpers ──
|
||||
|
||||
/// <summary>
|
||||
/// Determines the Gateway resource name based on which ingress controller
|
||||
/// is installed on the cluster. Traefik creates "traefik-gateway", Istio
|
||||
/// uses its own naming convention.
|
||||
/// Returns the (gatewayName, gatewayNamespace) for the ingress controller installed
|
||||
/// on the cluster. Checks both by component Name and by ReleaseName/HelmChartName
|
||||
/// to handle imported components and custom release names.
|
||||
/// </summary>
|
||||
private static string ResolveGatewayName(IEnumerable<ClusterComponent> components)
|
||||
public static (string Name, string Namespace) ResolveGateway(IEnumerable<ClusterComponent> components)
|
||||
{
|
||||
if (components.Any(c => string.Equals(c.Name, "traefik", StringComparison.OrdinalIgnoreCase)))
|
||||
List<ClusterComponent> list = components.ToList();
|
||||
|
||||
bool hasTraefik = list.Any(c =>
|
||||
string.Equals(c.Name, "traefik", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(c.HelmChartName, "traefik", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (hasTraefik)
|
||||
{
|
||||
return "traefik-gateway";
|
||||
return ("traefik-gateway", "traefik");
|
||||
}
|
||||
|
||||
if (components.Any(c => string.Equals(c.Name, "istio", StringComparison.OrdinalIgnoreCase)))
|
||||
ClusterComponent? istio = list.FirstOrDefault(c =>
|
||||
string.Equals(c.Name, "istio", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(c.HelmChartName, "gateway", StringComparison.OrdinalIgnoreCase)
|
||||
&& (string.Equals(c.Namespace, "istio-system", StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
if (istio is not null)
|
||||
{
|
||||
return "istio-ingress";
|
||||
string name = istio.ReleaseName ?? istio.Name;
|
||||
return (name, istio.Namespace ?? "istio-system");
|
||||
}
|
||||
|
||||
return "default-gateway";
|
||||
return ("default-gateway", "default");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the Gateway namespace based on the installed ingress controller.
|
||||
/// Returns the Kubernetes ingressClassName for cert-manager HTTP-01 ACME challenges.
|
||||
/// Uses standard Ingress (not gatewayHTTPRoute) so no cert-manager experimental
|
||||
/// feature gates are needed. Istio handles ingressClassName "istio" natively.
|
||||
/// </summary>
|
||||
private static string ResolveGatewayNamespace(IEnumerable<ClusterComponent> components)
|
||||
public static string ResolveIngressClass(IEnumerable<ClusterComponent> components)
|
||||
{
|
||||
if (components.Any(c => string.Equals(c.Name, "traefik", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return "traefik";
|
||||
}
|
||||
bool hasTraefik = components.Any(c =>
|
||||
string.Equals(c.Name, "traefik", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(c.HelmChartName, "traefik", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (components.Any(c => string.Equals(c.Name, "istio", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return "istio-system";
|
||||
}
|
||||
|
||||
return "default";
|
||||
return hasTraefik ? "traefik" : "istio";
|
||||
}
|
||||
|
||||
private static string ResolveGatewayName(IEnumerable<ClusterComponent> components) =>
|
||||
ResolveGateway(components).Name;
|
||||
|
||||
private static string ResolveGatewayNamespace(IEnumerable<ClusterComponent> components) =>
|
||||
ResolveGateway(components).Namespace;
|
||||
}
|
||||
|
||||
@@ -38,4 +38,19 @@ public interface IKubernetesClientFactory
|
||||
/// Uses kubectl exec to connect to the primary and run psql.
|
||||
/// </summary>
|
||||
Task ExecuteSqlAsync(string clusterName, string ns, string sql, string kubeconfig, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Executes a MongoDB script against the primary pod via kubectl exec + mongosh.
|
||||
/// The primary pod is the first StatefulSet member: {clusterName}-0.
|
||||
/// When username and password are provided, mongosh connects with SCRAM credentials.
|
||||
/// </summary>
|
||||
Task ExecuteMongoAsync(string clusterName, string ns, string script, string kubeconfig,
|
||||
string? username = null, string? password = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Reads a single key from a Kubernetes Secret and returns the decoded value.
|
||||
/// Returns null if the secret or key does not exist.
|
||||
/// </summary>
|
||||
Task<string?> GetSecretValueAsync(string secretName, string key, string ns, string kubeconfig,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
1409
src/EntKube.Web/Services/KeycloakService.cs
Normal file
1409
src/EntKube.Web/Services/KeycloakService.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace EntKube.Web.Services;
|
||||
|
||||
@@ -103,11 +104,12 @@ public class KubernetesClientFactory : IKubernetesClientFactory
|
||||
// CNPG primary pod follows the naming pattern: {cluster}-1
|
||||
string primaryPod = $"{clusterName}-1";
|
||||
|
||||
// Escape single quotes in SQL for shell safety.
|
||||
string escapedSql = sql.Replace("'", "'\\''");
|
||||
// Pipe SQL via stdin to avoid shell argument splitting issues.
|
||||
// The -i flag tells kubectl exec to pass stdin to the container.
|
||||
|
||||
await RunKubectlAsync(
|
||||
$"exec {primaryPod} -n {ns} --kubeconfig={kubeconfigPath} -- psql -U postgres -c '{escapedSql}'", ct);
|
||||
await RunKubectlWithStdinAsync(
|
||||
$"exec -i {primaryPod} -n {ns} --kubeconfig={kubeconfigPath} -- psql -U postgres",
|
||||
sql, ct);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -115,6 +117,58 @@ public class KubernetesClientFactory : IKubernetesClientFactory
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ExecuteMongoAsync(
|
||||
string clusterName, string ns, string script, string kubeconfig,
|
||||
string? username = null, string? password = null, CancellationToken ct = default)
|
||||
{
|
||||
string kubeconfigPath = Path.GetTempFileName();
|
||||
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(kubeconfigPath, kubeconfig, ct);
|
||||
|
||||
// MongoDB Community Operator names StatefulSet pods {cluster}-0, {cluster}-1, ...
|
||||
string primaryPod = $"{clusterName}-0";
|
||||
|
||||
// When credentials are provided, authenticate with SCRAM.
|
||||
// Passwords generated by MongoService are alphanumeric-safe (no shell escaping needed).
|
||||
|
||||
string mongoshArgs = !string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password)
|
||||
? $"--username {username} --password {password} --authenticationDatabase admin --quiet"
|
||||
: "--quiet";
|
||||
|
||||
await RunKubectlWithStdinAsync(
|
||||
$"exec -i {primaryPod} -n {ns} --kubeconfig={kubeconfigPath} -- mongosh {mongoshArgs}",
|
||||
script, ct);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(kubeconfigPath);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> GetSecretValueAsync(
|
||||
string secretName, string key, string ns, string kubeconfig, CancellationToken ct = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
string json = await GetJsonAsync($"secret/{secretName}", ns, kubeconfig, ct: ct);
|
||||
using JsonDocument doc = JsonDocument.Parse(json);
|
||||
if (doc.RootElement.TryGetProperty("data", out JsonElement data)
|
||||
&& data.TryGetProperty(key, out JsonElement valEl))
|
||||
{
|
||||
string? b64 = valEl.GetString();
|
||||
if (string.IsNullOrEmpty(b64)) return null;
|
||||
return Encoding.UTF8.GetString(Convert.FromBase64String(b64));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> RunKubectlAsync(string arguments, CancellationToken ct)
|
||||
{
|
||||
using Process process = new()
|
||||
@@ -151,4 +205,51 @@ public class KubernetesClientFactory : IKubernetesClientFactory
|
||||
|
||||
return output.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs kubectl with input piped via stdin. Used for executing SQL/scripts
|
||||
/// where passing the content as command-line arguments would break due to
|
||||
/// shell escaping and argument splitting in Process.
|
||||
/// </summary>
|
||||
private static async Task<string> RunKubectlWithStdinAsync(
|
||||
string arguments, string stdinContent, CancellationToken ct)
|
||||
{
|
||||
using Process process = new()
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "kubectl",
|
||||
Arguments = arguments,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
|
||||
// Write the script/SQL to stdin and close it so the process knows input is done.
|
||||
|
||||
await process.StandardInput.WriteAsync(stdinContent.AsMemory(), ct);
|
||||
await process.StandardInput.FlushAsync(ct);
|
||||
process.StandardInput.Close();
|
||||
|
||||
Task<string> outputTask = process.StandardOutput.ReadToEndAsync(ct);
|
||||
Task<string> errorTask = process.StandardError.ReadToEndAsync(ct);
|
||||
|
||||
await process.WaitForExitAsync(ct);
|
||||
|
||||
string output = await outputTask;
|
||||
string error = await errorTask;
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"kubectl failed (exit {process.ExitCode}): {error}");
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
1889
src/EntKube.Web/Services/MongoService.cs
Normal file
1889
src/EntKube.Web/Services/MongoService.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -79,7 +79,7 @@ public class OpenStackS3Service(VaultService vaultService, IHttpClientFactory ht
|
||||
|
||||
// Step 2: Authenticate against Keystone to get a scoped token, user ID, and project ID.
|
||||
|
||||
(string token, string userId, string projectId) = await AuthenticateKeystoneAsync(connection, password, ct);
|
||||
(string token, string userId, string projectId, _) = await AuthenticateKeystoneAsync(connection, password, ct);
|
||||
|
||||
// Step 3: Create EC2 credentials for S3 access.
|
||||
|
||||
@@ -121,7 +121,7 @@ public class OpenStackS3Service(VaultService vaultService, IHttpClientFactory ht
|
||||
/// Authenticates against OpenStack Keystone v3 using password auth.
|
||||
/// Returns the scoped auth token and the user ID (needed for EC2 credential creation).
|
||||
/// </summary>
|
||||
private async Task<(string Token, string UserId, string ProjectId)> AuthenticateKeystoneAsync(
|
||||
private async Task<(string Token, string UserId, string ProjectId, string? SwiftEndpoint)> AuthenticateKeystoneAsync(
|
||||
OpenStackConnection connection, string password, CancellationToken ct)
|
||||
{
|
||||
// Build the Keystone v3 auth request body.
|
||||
@@ -185,10 +185,10 @@ public class OpenStackS3Service(VaultService vaultService, IHttpClientFactory ht
|
||||
|
||||
string token = response.Headers.GetValues("X-Subject-Token").First();
|
||||
|
||||
// Extract the user ID and project ID from the response body.
|
||||
// The project ID here is always a UUID, even if we authenticated by project name.
|
||||
// Extract the user ID, project ID, and Swift endpoint from the response body.
|
||||
|
||||
using JsonDocument doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync(ct));
|
||||
string body = await response.Content.ReadAsStringAsync(ct);
|
||||
using JsonDocument doc = JsonDocument.Parse(body);
|
||||
JsonElement tokenElement = doc.RootElement.GetProperty("token");
|
||||
|
||||
string userId = tokenElement
|
||||
@@ -201,7 +201,104 @@ public class OpenStackS3Service(VaultService vaultService, IHttpClientFactory ht
|
||||
.GetProperty("id")
|
||||
.GetString()!;
|
||||
|
||||
return (token, userId, projectId);
|
||||
// Find the public Swift (object-store) endpoint from the service catalog.
|
||||
// This lets us list all containers regardless of which EC2 credential pair was used.
|
||||
|
||||
string? swiftEndpoint = null;
|
||||
|
||||
if (tokenElement.TryGetProperty("catalog", out JsonElement catalog))
|
||||
{
|
||||
foreach (JsonElement service in catalog.EnumerateArray())
|
||||
{
|
||||
if (service.TryGetProperty("type", out JsonElement typeEl)
|
||||
&& typeEl.GetString() == "object-store"
|
||||
&& service.TryGetProperty("endpoints", out JsonElement endpoints))
|
||||
{
|
||||
foreach (JsonElement ep in endpoints.EnumerateArray())
|
||||
{
|
||||
if (ep.TryGetProperty("interface", out JsonElement iface)
|
||||
&& iface.GetString() == "public"
|
||||
&& ep.TryGetProperty("url", out JsonElement urlEl))
|
||||
{
|
||||
swiftEndpoint = urlEl.GetString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (swiftEndpoint is not null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (token, userId, projectId, swiftEndpoint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists all containers in the OpenStack project via the Swift REST API.
|
||||
/// More reliable than S3 ListBuckets against Ceph RADOS Gateway because it
|
||||
/// goes through the project-scoped object-store endpoint rather than the
|
||||
/// per-EC2-credential RGW user scope.
|
||||
/// </summary>
|
||||
public async Task<List<S3BucketInfo>> ListBucketsViaSwiftAsync(
|
||||
OpenStackConnection connection, string password, CancellationToken ct = default)
|
||||
{
|
||||
(string token, _, _, string? swiftEndpoint) = await AuthenticateKeystoneAsync(connection, password, ct);
|
||||
|
||||
if (swiftEndpoint is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"No public Swift (object-store) endpoint found in the Keystone service catalog.");
|
||||
}
|
||||
|
||||
// GET <swift_endpoint>?format=json lists all containers owned by this project.
|
||||
|
||||
using HttpClient client = httpClientFactory.CreateClient();
|
||||
|
||||
string listUrl = swiftEndpoint.TrimEnd('/') + "?format=json";
|
||||
|
||||
using HttpRequestMessage listRequest = new(HttpMethod.Get, listUrl);
|
||||
listRequest.Headers.Add("X-Auth-Token", token);
|
||||
|
||||
using HttpResponseMessage listResponse = await client.SendAsync(listRequest, ct);
|
||||
|
||||
if (!listResponse.IsSuccessStatusCode)
|
||||
{
|
||||
string errorBody = await listResponse.Content.ReadAsStringAsync(ct);
|
||||
throw new InvalidOperationException(
|
||||
$"Swift container listing failed ({listResponse.StatusCode}): {errorBody}");
|
||||
}
|
||||
|
||||
string responseBody = await listResponse.Content.ReadAsStringAsync(ct);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(responseBody) || responseBody == "[]")
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
using JsonDocument doc = JsonDocument.Parse(responseBody);
|
||||
|
||||
return doc.RootElement.EnumerateArray()
|
||||
.Select(container =>
|
||||
{
|
||||
string name = container.TryGetProperty("name", out JsonElement nameEl)
|
||||
? nameEl.GetString() ?? ""
|
||||
: "";
|
||||
|
||||
DateTime lastModified = default;
|
||||
|
||||
if (container.TryGetProperty("last_modified", out JsonElement lmEl)
|
||||
&& DateTime.TryParse(lmEl.GetString(), out DateTime parsed))
|
||||
{
|
||||
lastModified = DateTime.SpecifyKind(parsed, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
return new S3BucketInfo { Name = name, CreatedAt = lastModified };
|
||||
})
|
||||
.Where(b => b.Name.Length > 0)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -335,8 +432,8 @@ public class OpenStackS3Service(VaultService vaultService, IHttpClientFactory ht
|
||||
|
||||
ListBucketsResponse response = await s3Client.ListBucketsAsync(ct);
|
||||
|
||||
return response.Buckets
|
||||
.Select(b => new S3BucketInfo { Name = b.BucketName, CreatedAt = b.CreationDate.GetValueOrDefault() })
|
||||
return (response.Buckets ?? [])
|
||||
.Select(b => new S3BucketInfo { Name = b.BucketName ?? "", CreatedAt = b.CreationDate.GetValueOrDefault() })
|
||||
.ToList();
|
||||
}
|
||||
|
||||
@@ -352,36 +449,47 @@ public class OpenStackS3Service(VaultService vaultService, IHttpClientFactory ht
|
||||
|
||||
// Empty the bucket first — S3 won't delete non-empty buckets.
|
||||
// We page through all objects and delete them in batches of up to 1000.
|
||||
// If the bucket doesn't exist at all we treat it as already gone.
|
||||
|
||||
string? continuationToken = null;
|
||||
|
||||
do
|
||||
try
|
||||
{
|
||||
ListObjectsV2Request listRequest = new()
|
||||
{
|
||||
BucketName = bucketName,
|
||||
MaxKeys = 1000,
|
||||
ContinuationToken = continuationToken
|
||||
};
|
||||
string? continuationToken = null;
|
||||
|
||||
ListObjectsV2Response listResponse = await s3Client.ListObjectsV2Async(listRequest, ct);
|
||||
|
||||
if (listResponse.S3Objects.Count > 0)
|
||||
do
|
||||
{
|
||||
DeleteObjectsRequest deleteObjectsRequest = new()
|
||||
ListObjectsV2Request listRequest = new()
|
||||
{
|
||||
BucketName = bucketName,
|
||||
Objects = listResponse.S3Objects
|
||||
.Select(o => new KeyVersion { Key = o.Key })
|
||||
.ToList()
|
||||
MaxKeys = 1000,
|
||||
ContinuationToken = continuationToken
|
||||
};
|
||||
|
||||
await s3Client.DeleteObjectsAsync(deleteObjectsRequest, ct);
|
||||
}
|
||||
ListObjectsV2Response listResponse = await s3Client.ListObjectsV2Async(listRequest, ct);
|
||||
|
||||
continuationToken = listResponse.IsTruncated == true ? listResponse.NextContinuationToken : null;
|
||||
List<S3Object> objects = listResponse.S3Objects ?? [];
|
||||
|
||||
if (objects.Count > 0)
|
||||
{
|
||||
DeleteObjectsRequest deleteObjectsRequest = new()
|
||||
{
|
||||
BucketName = bucketName,
|
||||
Objects = objects
|
||||
.Select(o => new KeyVersion { Key = o.Key ?? "" })
|
||||
.ToList()
|
||||
};
|
||||
|
||||
await s3Client.DeleteObjectsAsync(deleteObjectsRequest, ct);
|
||||
}
|
||||
|
||||
continuationToken = listResponse.IsTruncated == true ? listResponse.NextContinuationToken : null;
|
||||
}
|
||||
while (continuationToken is not null);
|
||||
}
|
||||
catch (AmazonS3Exception ex) when (ex.ErrorCode == "NoSuchBucket")
|
||||
{
|
||||
// Bucket was already deleted outside of EntKube — nothing left to empty.
|
||||
return;
|
||||
}
|
||||
while (continuationToken is not null);
|
||||
|
||||
// Remove the bucket policy before deletion. Ceph RADOS Gateway (used by Cleura)
|
||||
// may reject DeleteBucket if a policy is still attached.
|
||||
@@ -395,8 +503,15 @@ public class OpenStackS3Service(VaultService vaultService, IHttpClientFactory ht
|
||||
// No policy to remove — that's fine, proceed with deletion.
|
||||
}
|
||||
|
||||
DeleteBucketRequest deleteRequest = new() { BucketName = bucketName };
|
||||
await s3Client.DeleteBucketAsync(deleteRequest, ct);
|
||||
try
|
||||
{
|
||||
DeleteBucketRequest deleteRequest = new() { BucketName = bucketName };
|
||||
await s3Client.DeleteBucketAsync(deleteRequest, ct);
|
||||
}
|
||||
catch (AmazonS3Exception ex) when (ex.ErrorCode == "NoSuchBucket")
|
||||
{
|
||||
// Already gone — nothing to do.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -544,7 +659,7 @@ public class OpenStackS3Service(VaultService vaultService, IHttpClientFactory ht
|
||||
|
||||
// Authenticate and create fresh EC2 credentials.
|
||||
|
||||
(string token, string userId, string projectId) = await AuthenticateKeystoneAsync(connection, password, ct);
|
||||
(string token, string userId, string projectId, _) = await AuthenticateKeystoneAsync(connection, password, ct);
|
||||
(string newAccessKey, string newSecretKey) = await CreateEc2CredentialsAsync(token, userId, projectId, connection.AuthUrl, ct);
|
||||
|
||||
// Verify the new credentials work by listing the bucket.
|
||||
@@ -574,6 +689,11 @@ public class OpenStackS3Service(VaultService vaultService, IHttpClientFactory ht
|
||||
/// </summary>
|
||||
private static AmazonS3Client CreateS3Client(string endpoint, string accessKey, string secretKey, string region)
|
||||
{
|
||||
if (string.IsNullOrEmpty(endpoint))
|
||||
{
|
||||
throw new InvalidOperationException("S3 endpoint is required but was not configured for this storage link.");
|
||||
}
|
||||
|
||||
AmazonS3Config config = new()
|
||||
{
|
||||
ServiceURL = endpoint,
|
||||
|
||||
@@ -392,10 +392,36 @@ public class StorageService(IDbContextFactory<ApplicationDbContext> dbFactory, V
|
||||
Guid tenantId, Guid linkId, CancellationToken ct = default)
|
||||
{
|
||||
StorageLink link = await GetCleuraLinkOrThrowAsync(tenantId, linkId, ct);
|
||||
|
||||
// Prefer the Swift API (via the stored OpenStack connection) because it
|
||||
// enumerates all project containers, whereas S3 ListBuckets scopes to the
|
||||
// specific EC2 credential's RGW user and often returns an empty list.
|
||||
|
||||
if (link.OpenStackConnectionId.HasValue)
|
||||
{
|
||||
using ApplicationDbContext db = dbFactory.CreateDbContext();
|
||||
|
||||
OpenStackConnection? connection = await db.OpenStackConnections
|
||||
.FirstOrDefaultAsync(c => c.Id == link.OpenStackConnectionId.Value, ct);
|
||||
|
||||
if (connection is not null)
|
||||
{
|
||||
string? password = await vaultService.GetOpenStackSecretValueAsync(
|
||||
tenantId, connection.Id, "OS_PASSWORD", ct);
|
||||
|
||||
if (!string.IsNullOrEmpty(password))
|
||||
{
|
||||
return await openStackS3.ListBucketsViaSwiftAsync(connection, password, ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: use the stored EC2 credentials with the S3 API.
|
||||
|
||||
(string accessKey, string secretKey) = await GetStoredCredentialsAsync(tenantId, linkId, ct);
|
||||
|
||||
return await openStackS3.ListBucketsAsync(
|
||||
link.Endpoint!, accessKey, secretKey, link.Region!, ct);
|
||||
link.Endpoint ?? "", accessKey, secretKey, link.Region ?? "", ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -407,12 +433,20 @@ public class StorageService(IDbContextFactory<ApplicationDbContext> dbFactory, V
|
||||
Guid tenantId, Guid linkId, CancellationToken ct = default)
|
||||
{
|
||||
StorageLink link = await GetCleuraLinkOrThrowAsync(tenantId, linkId, ct);
|
||||
(string accessKey, string secretKey) = await GetStoredCredentialsAsync(tenantId, linkId, ct);
|
||||
|
||||
// Delete the actual bucket from the object store.
|
||||
// Only attempt S3 deletion when we have all the connection details.
|
||||
// If any field is missing the bucket may not exist (or was never fully provisioned),
|
||||
// so we fall through and clean up the DB record regardless.
|
||||
|
||||
await openStackS3.DeleteBucketAsync(
|
||||
link.Endpoint!, accessKey, secretKey, link.BucketName!, link.Region!, ct);
|
||||
if (!string.IsNullOrEmpty(link.Endpoint)
|
||||
&& !string.IsNullOrEmpty(link.BucketName)
|
||||
&& !string.IsNullOrEmpty(link.Region))
|
||||
{
|
||||
(string accessKey, string secretKey) = await GetStoredCredentialsAsync(tenantId, linkId, ct);
|
||||
|
||||
await openStackS3.DeleteBucketAsync(
|
||||
link.Endpoint, accessKey, secretKey, link.BucketName, link.Region, ct);
|
||||
}
|
||||
|
||||
// Remove the StorageLink and vault secrets.
|
||||
|
||||
|
||||
@@ -144,10 +144,13 @@ public class VaultService(IDbContextFactory<ApplicationDbContext> dbFactory, Vau
|
||||
// --- Component Secrets ---
|
||||
|
||||
/// <summary>
|
||||
/// Stores or updates a secret for a cluster component.
|
||||
/// Stores or updates a secret for a cluster component. When k8sSecretName is
|
||||
/// provided, the vault sync service will mirror the value into that Kubernetes Secret.
|
||||
/// </summary>
|
||||
public async Task<VaultSecret> SetComponentSecretAsync(
|
||||
Guid tenantId, Guid componentId, string name, string value, CancellationToken ct = default)
|
||||
Guid tenantId, Guid componentId, string name, string value,
|
||||
CancellationToken ct = default,
|
||||
string? k8sSecretName = null, string? k8sNamespace = null)
|
||||
{
|
||||
using ApplicationDbContext db = dbFactory.CreateDbContext();
|
||||
|
||||
@@ -162,6 +165,12 @@ public class VaultService(IDbContextFactory<ApplicationDbContext> dbFactory, Vau
|
||||
{
|
||||
existing.EncryptedValue = ciphertext;
|
||||
existing.Nonce = nonce;
|
||||
if (k8sSecretName is not null)
|
||||
{
|
||||
existing.SyncToKubernetes = true;
|
||||
existing.KubernetesSecretName = k8sSecretName;
|
||||
existing.KubernetesNamespace = k8sNamespace;
|
||||
}
|
||||
existing.UpdatedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync(ct);
|
||||
return existing;
|
||||
@@ -176,7 +185,10 @@ public class VaultService(IDbContextFactory<ApplicationDbContext> dbFactory, Vau
|
||||
Name = name,
|
||||
EncryptedValue = ciphertext,
|
||||
Nonce = nonce,
|
||||
ComponentId = componentId
|
||||
ComponentId = componentId,
|
||||
SyncToKubernetes = k8sSecretName is not null,
|
||||
KubernetesSecretName = k8sSecretName,
|
||||
KubernetesNamespace = k8sNamespace
|
||||
};
|
||||
|
||||
db.Set<VaultSecret>().Add(secret);
|
||||
@@ -439,6 +451,144 @@ public class VaultService(IDbContextFactory<ApplicationDbContext> dbFactory, Vau
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the decrypted password for a CNPG database. Used when another
|
||||
/// service (e.g. Keycloak) needs to read the DB password to propagate it.
|
||||
/// </summary>
|
||||
public async Task<string?> GetCnpgDatabasePasswordAsync(
|
||||
Guid tenantId, Guid cnpgDatabaseId, CancellationToken ct = default)
|
||||
{
|
||||
using ApplicationDbContext db = dbFactory.CreateDbContext();
|
||||
|
||||
VaultSecret? secret = await db.Set<VaultSecret>()
|
||||
.FirstOrDefaultAsync(s => s.Vault.TenantId == tenantId
|
||||
&& s.CnpgDatabaseId == cnpgDatabaseId
|
||||
&& s.Name == "PASSWORD", ct);
|
||||
|
||||
if (secret is null) return null;
|
||||
|
||||
byte[] dataKey = await UnsealVaultAsync(tenantId, ct);
|
||||
return encryption.Decrypt(dataKey, secret.EncryptedValue, secret.Nonce);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates or updates a vault secret scoped to a MongoDB database.
|
||||
/// Mirrors SetCnpgDatabaseSecretAsync but uses MongoDatabaseId.
|
||||
/// </summary>
|
||||
public async Task<VaultSecret> SetMongoDatabaseSecretAsync(
|
||||
Guid tenantId, Guid mongoDatabaseId, string name, string value,
|
||||
string k8sSecretName, string k8sNamespace, CancellationToken ct = default)
|
||||
{
|
||||
using ApplicationDbContext db = dbFactory.CreateDbContext();
|
||||
|
||||
byte[] dataKey = await UnsealVaultAsync(tenantId, ct);
|
||||
|
||||
VaultSecret? existing = await db.Set<VaultSecret>()
|
||||
.FirstOrDefaultAsync(s => s.Vault.TenantId == tenantId && s.MongoDatabaseId == mongoDatabaseId && s.Name == name, ct);
|
||||
|
||||
(byte[] ciphertext, byte[] nonce) = encryption.Encrypt(dataKey, value);
|
||||
|
||||
if (existing is not null)
|
||||
{
|
||||
existing.EncryptedValue = ciphertext;
|
||||
existing.Nonce = nonce;
|
||||
existing.SyncToKubernetes = true;
|
||||
existing.KubernetesSecretName = k8sSecretName;
|
||||
existing.KubernetesNamespace = k8sNamespace;
|
||||
existing.UpdatedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync(ct);
|
||||
return existing;
|
||||
}
|
||||
|
||||
SecretVault vault = (await GetVaultAsync(tenantId, ct))!;
|
||||
|
||||
VaultSecret secret = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
VaultId = vault.Id,
|
||||
Name = name,
|
||||
EncryptedValue = ciphertext,
|
||||
Nonce = nonce,
|
||||
MongoDatabaseId = mongoDatabaseId,
|
||||
SyncToKubernetes = true,
|
||||
KubernetesSecretName = k8sSecretName,
|
||||
KubernetesNamespace = k8sNamespace
|
||||
};
|
||||
|
||||
db.Set<VaultSecret>().Add(secret);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return secret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates or updates a vault secret scoped to a managed MongoDB cluster.
|
||||
/// When SyncToKubernetes is true the vault sync service will keep the K8s Secret
|
||||
/// up to date, but callers that need the Secret immediately should also apply
|
||||
/// the manifest directly via IKubernetesClientFactory.
|
||||
/// </summary>
|
||||
public async Task<VaultSecret> SetMongoClusterSecretAsync(
|
||||
Guid tenantId, Guid mongoClusterId, string name, string value,
|
||||
string k8sSecretName, string k8sNamespace, CancellationToken ct = default)
|
||||
{
|
||||
using ApplicationDbContext db = dbFactory.CreateDbContext();
|
||||
|
||||
byte[] dataKey = await UnsealVaultAsync(tenantId, ct);
|
||||
|
||||
VaultSecret? existing = await db.Set<VaultSecret>()
|
||||
.FirstOrDefaultAsync(s => s.Vault.TenantId == tenantId && s.MongoClusterId == mongoClusterId && s.Name == name, ct);
|
||||
|
||||
(byte[] ciphertext, byte[] nonce) = encryption.Encrypt(dataKey, value);
|
||||
|
||||
if (existing is not null)
|
||||
{
|
||||
existing.EncryptedValue = ciphertext;
|
||||
existing.Nonce = nonce;
|
||||
existing.SyncToKubernetes = true;
|
||||
existing.KubernetesSecretName = k8sSecretName;
|
||||
existing.KubernetesNamespace = k8sNamespace;
|
||||
existing.UpdatedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync(ct);
|
||||
return existing;
|
||||
}
|
||||
|
||||
SecretVault vault = (await GetVaultAsync(tenantId, ct))!;
|
||||
|
||||
VaultSecret secret = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
VaultId = vault.Id,
|
||||
Name = name,
|
||||
EncryptedValue = ciphertext,
|
||||
Nonce = nonce,
|
||||
MongoClusterId = mongoClusterId,
|
||||
SyncToKubernetes = true,
|
||||
KubernetesSecretName = k8sSecretName,
|
||||
KubernetesNamespace = k8sNamespace
|
||||
};
|
||||
|
||||
db.Set<VaultSecret>().Add(secret);
|
||||
await db.SaveChangesAsync(ct);
|
||||
return secret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrypts and returns the value of a secret scoped to a managed MongoDB cluster.
|
||||
/// Returns null if no matching secret is found.
|
||||
/// </summary>
|
||||
public async Task<string?> GetMongoClusterSecretValueAsync(
|
||||
Guid tenantId, Guid mongoClusterId, string name, CancellationToken ct = default)
|
||||
{
|
||||
using ApplicationDbContext db = dbFactory.CreateDbContext();
|
||||
|
||||
VaultSecret? secret = await db.Set<VaultSecret>()
|
||||
.FirstOrDefaultAsync(s => s.Vault.TenantId == tenantId && s.MongoClusterId == mongoClusterId && s.Name == name, ct);
|
||||
|
||||
if (secret is null) return null;
|
||||
|
||||
byte[] dataKey = await UnsealVaultAsync(tenantId, ct);
|
||||
return encryption.Decrypt(dataKey, secret.EncryptedValue, secret.Nonce);
|
||||
}
|
||||
|
||||
// --- Secret Management ---
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -55,6 +55,35 @@ public class ComponentFormField
|
||||
/// Defaults to the field Key if not specified.
|
||||
/// </summary>
|
||||
public string? SecretName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When set, this field is only shown when the sibling field with this key
|
||||
/// has the value specified in <see cref="DependsOnValue"/>.
|
||||
/// Used for conditional fields (e.g. show cert/key inputs only when TLS mode is Manual).
|
||||
/// </summary>
|
||||
public string? DependsOnKey { get; init; }
|
||||
|
||||
/// <summary>The value that <see cref="DependsOnKey"/> must equal for this field to be visible.</summary>
|
||||
public string? DependsOnValue { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When StoreAsSecret is true, automatically sync this vault secret to a Kubernetes Secret
|
||||
/// with this name. The vault secret name becomes the key inside the K8s Secret.
|
||||
/// Useful for secrets that external resources (like cert-manager ClusterIssuers) reference directly.
|
||||
/// </summary>
|
||||
public string? KubernetesSecretName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Namespace for the auto-synced Kubernetes Secret. Defaults to the component's namespace.
|
||||
/// </summary>
|
||||
public string? KubernetesSecretNamespace { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// For subchart toggle fields (YamlPath starting with "subchart:"), the Helm values YAML
|
||||
/// to pass when installing that subchart. Lets a combined component (e.g. istio-base which
|
||||
/// installs both base and istiod) supply sensible defaults for each subchart's install.
|
||||
/// </summary>
|
||||
public string? SubchartDefaultValues { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -66,7 +95,9 @@ public enum FormFieldType
|
||||
Number,
|
||||
Password,
|
||||
Toggle,
|
||||
Select
|
||||
Select,
|
||||
CnpgDatabase,
|
||||
ClusterIssuer
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -147,14 +178,44 @@ public static class YamlFormMerger
|
||||
private static void SetValueAtPath(YamlMappingNode current, string[] segments, string value)
|
||||
{
|
||||
// Walk down the path, creating mappings for intermediate segments.
|
||||
// Numeric segments (e.g. "0", "1") index into sequence nodes.
|
||||
// On the last segment, we set the scalar value.
|
||||
|
||||
for (int i = 0; i < segments.Length - 1; i++)
|
||||
{
|
||||
YamlScalarNode key = new(segments[i]);
|
||||
string seg = segments[i];
|
||||
YamlScalarNode key = new(seg);
|
||||
YamlNode? existing = current.Children.Keys
|
||||
.OfType<YamlScalarNode>()
|
||||
.FirstOrDefault(k => k.Value == segments[i]);
|
||||
.FirstOrDefault(k => k.Value == seg);
|
||||
|
||||
// When the child is a sequence and the next segment is a numeric index,
|
||||
// navigate into the sequence element and continue walking from there.
|
||||
if (existing is not null
|
||||
&& current.Children[existing] is YamlSequenceNode seq
|
||||
&& i + 1 < segments.Length - 1
|
||||
&& int.TryParse(segments[i + 1], out int arrayIndex))
|
||||
{
|
||||
// Ensure the sequence is long enough.
|
||||
while (seq.Children.Count <= arrayIndex)
|
||||
{
|
||||
seq.Children.Add(new YamlMappingNode());
|
||||
}
|
||||
|
||||
if (seq.Children[arrayIndex] is YamlMappingNode seqMapping)
|
||||
{
|
||||
current = seqMapping;
|
||||
}
|
||||
else
|
||||
{
|
||||
YamlMappingNode replacement = new();
|
||||
seq.Children[arrayIndex] = replacement;
|
||||
current = replacement;
|
||||
}
|
||||
|
||||
i++; // skip the numeric index segment — already consumed
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existing is not null && current.Children[existing] is YamlMappingNode childMapping)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user