Add WAL file for database and log instance deployment failures
Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
This commit is contained in:
@@ -8,12 +8,34 @@ public static class AppConstants
|
||||
public const string AdminRole = "Admin";
|
||||
public const string ViewerRole = "Viewer";
|
||||
|
||||
// ── Global Docker secret names ──────────────────────────────────────────
|
||||
|
||||
/// <summary>Docker secret name for the global SMTP password.</summary>
|
||||
public const string GlobalSmtpSecretName = "global_smtp_password";
|
||||
|
||||
/// <summary>Build a per-customer MySQL password secret name.</summary>
|
||||
public static string CustomerMysqlSecretName(string customerName)
|
||||
=> $"{SanitizeName(customerName)}_mysql_password";
|
||||
/// <summary>Docker secret name for the shared MySQL host address.</summary>
|
||||
public const string GlobalMysqlHostSecretName = "global_mysql_host";
|
||||
|
||||
/// <summary>Docker secret name for the shared MySQL port.</summary>
|
||||
public const string GlobalMysqlPortSecretName = "global_mysql_port";
|
||||
|
||||
// ── Per-instance Docker secret name builders ────────────────────────────
|
||||
|
||||
/// <summary>Build a per-instance MySQL password secret name from the 3-letter abbreviation.</summary>
|
||||
public static string CustomerMysqlPasswordSecretName(string abbrev)
|
||||
=> $"{abbrev}-cms-db-password";
|
||||
|
||||
/// <summary>Build a per-instance MySQL username secret name from the 3-letter abbreviation.</summary>
|
||||
public static string CustomerMysqlUserSecretName(string abbrev)
|
||||
=> $"{abbrev}-cms-db-user";
|
||||
|
||||
/// <summary>Returns all per-instance MySQL secret names for a given abbreviation.</summary>
|
||||
public static string[] AllCustomerMysqlSecretNames(string abbrev)
|
||||
=> new[]
|
||||
{
|
||||
CustomerMysqlPasswordSecretName(abbrev),
|
||||
CustomerMysqlUserSecretName(abbrev),
|
||||
};
|
||||
|
||||
/// <summary>Sanitize a customer name for use in Docker/secret names.</summary>
|
||||
public static string SanitizeName(string name)
|
||||
|
||||
@@ -72,5 +72,5 @@ public class InstanceDefaultsOptions
|
||||
public string LibraryShareSubPath { get; set; } = "{abbrev}-cms-library";
|
||||
|
||||
public string MySqlDatabaseTemplate { get; set; } = "{abbrev}_cms_db";
|
||||
public string MySqlUserTemplate { get; set; } = "{abbrev}_cms";
|
||||
public string MySqlUserTemplate { get; set; } = "{abbrev}_cms_user";
|
||||
}
|
||||
|
||||
@@ -15,9 +15,7 @@ public class XiboContext : DbContext
|
||||
_dataProtection = dataProtection;
|
||||
}
|
||||
|
||||
public DbSet<CmsInstance> CmsInstances => Set<CmsInstance>();
|
||||
public DbSet<SshHost> SshHosts => Set<SshHost>();
|
||||
public DbSet<SecretMetadata> SecretMetadata => Set<SecretMetadata>();
|
||||
public DbSet<OperationLog> OperationLogs => Set<OperationLog>();
|
||||
public DbSet<AppSetting> AppSettings => Set<AppSetting>();
|
||||
|
||||
@@ -25,32 +23,6 @@ public class XiboContext : DbContext
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// --- CmsInstance ---
|
||||
modelBuilder.Entity<CmsInstance>(entity =>
|
||||
{
|
||||
entity.HasIndex(e => e.StackName).IsUnique();
|
||||
entity.HasIndex(e => e.CustomerName);
|
||||
entity.HasQueryFilter(e => e.DeletedAt == null);
|
||||
|
||||
entity.HasOne(e => e.SshHost)
|
||||
.WithMany(h => h.Instances)
|
||||
.HasForeignKey(e => e.SshHostId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
if (_dataProtection != null)
|
||||
{
|
||||
var protector = _dataProtection.CreateProtector("OTSSignsOrchestrator.CmsInstance");
|
||||
var pwdConverter = new ValueConverter<string?, string?>(
|
||||
v => v != null ? protector.Protect(v) : null,
|
||||
v => v != null ? protector.Unprotect(v) : null);
|
||||
|
||||
entity.Property(e => e.XiboPassword).HasConversion(pwdConverter);
|
||||
entity.Property(e => e.XiboUsername).HasConversion(pwdConverter);
|
||||
entity.Property(e => e.TemplateRepoPat).HasConversion(pwdConverter);
|
||||
entity.Property(e => e.CifsPassword).HasConversion(pwdConverter);
|
||||
}
|
||||
});
|
||||
|
||||
// --- SshHost ---
|
||||
modelBuilder.Entity<SshHost>(entity =>
|
||||
{
|
||||
@@ -71,17 +43,11 @@ public class XiboContext : DbContext
|
||||
}
|
||||
});
|
||||
|
||||
// --- SecretMetadata ---
|
||||
modelBuilder.Entity<SecretMetadata>(entity =>
|
||||
{
|
||||
entity.HasIndex(e => e.Name).IsUnique();
|
||||
});
|
||||
|
||||
// --- OperationLog ---
|
||||
modelBuilder.Entity<OperationLog>(entity =>
|
||||
{
|
||||
entity.HasIndex(e => e.Timestamp);
|
||||
entity.HasIndex(e => e.InstanceId);
|
||||
entity.HasIndex(e => e.StackName);
|
||||
entity.HasIndex(e => e.Operation);
|
||||
});
|
||||
|
||||
|
||||
339
OTSSignsOrchestrator.Core/Migrations/20260219005507_ReplaceCifsWithNfs.Designer.cs
generated
Normal file
339
OTSSignsOrchestrator.Core/Migrations/20260219005507_ReplaceCifsWithNfs.Designer.cs
generated
Normal file
@@ -0,0 +1,339 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using OTSSignsOrchestrator.Core.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace OTSSignsOrchestrator.Core.Migrations
|
||||
{
|
||||
[DbContext(typeof(XiboContext))]
|
||||
[Migration("20260219005507_ReplaceCifsWithNfs")]
|
||||
partial class ReplaceCifsWithNfs
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.2");
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.AppSetting", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsSensitive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.HasIndex("Category");
|
||||
|
||||
b.ToTable("AppSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.CmsInstance", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CmsServerName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Constraints")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CustomerAbbrev")
|
||||
.IsRequired()
|
||||
.HasMaxLength(3)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CustomerName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("HostHttpPort")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("LibraryHostPath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NfsExport")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NfsExportFolder")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NfsExtraOptions")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NfsServer")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SmtpServer")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SmtpUsername")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("SshHostId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StackName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("TemplateCacheKey")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("TemplateLastFetch")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TemplateRepoPat")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TemplateRepoUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ThemeHostPath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("XiboApiTestStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("XiboApiTestedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("XiboPassword")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("XiboUsername")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CustomerName");
|
||||
|
||||
b.HasIndex("SshHostId");
|
||||
|
||||
b.HasIndex("StackName")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CmsInstances");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.OperationLog", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("DurationMs")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("InstanceId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Operation")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId");
|
||||
|
||||
b.HasIndex("Operation");
|
||||
|
||||
b.HasIndex("Timestamp");
|
||||
|
||||
b.ToTable("OperationLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.SecretMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CustomerName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsGlobal")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("LastRotatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SecretMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.SshHost", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Host")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("KeyPassphrase")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Label")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool?>("LastTestSuccess")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("LastTestedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Port")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PrivateKeyPath")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("UseKeyAuth")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Label")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SshHosts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.CmsInstance", b =>
|
||||
{
|
||||
b.HasOne("OTSSignsOrchestrator.Core.Models.Entities.SshHost", "SshHost")
|
||||
.WithMany("Instances")
|
||||
.HasForeignKey("SshHostId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("SshHost");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.OperationLog", b =>
|
||||
{
|
||||
b.HasOne("OTSSignsOrchestrator.Core.Models.Entities.CmsInstance", "Instance")
|
||||
.WithMany("OperationLogs")
|
||||
.HasForeignKey("InstanceId");
|
||||
|
||||
b.Navigation("Instance");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.CmsInstance", b =>
|
||||
{
|
||||
b.Navigation("OperationLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.SshHost", b =>
|
||||
{
|
||||
b.Navigation("Instances");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace OTSSignsOrchestrator.Core.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ReplaceCifsWithNfs : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// 1. Add new NFS columns
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "NfsServer",
|
||||
table: "CmsInstances",
|
||||
type: "TEXT",
|
||||
maxLength: 200,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "NfsExport",
|
||||
table: "CmsInstances",
|
||||
type: "TEXT",
|
||||
maxLength: 500,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "NfsExportFolder",
|
||||
table: "CmsInstances",
|
||||
type: "TEXT",
|
||||
maxLength: 500,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "NfsExtraOptions",
|
||||
table: "CmsInstances",
|
||||
type: "TEXT",
|
||||
maxLength: 500,
|
||||
nullable: true);
|
||||
|
||||
// 2. Migrate existing CIFS data into NFS columns
|
||||
// NfsServer = CifsServer, NfsExport = '/' + CifsShareName, NfsExportFolder = CifsShareFolder
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
UPDATE CmsInstances
|
||||
SET NfsServer = CifsServer,
|
||||
NfsExport = CASE WHEN CifsShareName IS NOT NULL THEN '/' || CifsShareName ELSE NULL END,
|
||||
NfsExportFolder = CifsShareFolder,
|
||||
NfsExtraOptions = CifsExtraOptions
|
||||
WHERE CifsServer IS NOT NULL;
|
||||
""");
|
||||
|
||||
// 3. Drop old CIFS columns
|
||||
migrationBuilder.DropColumn(name: "CifsServer", table: "CmsInstances");
|
||||
migrationBuilder.DropColumn(name: "CifsShareName", table: "CmsInstances");
|
||||
migrationBuilder.DropColumn(name: "CifsShareFolder", table: "CmsInstances");
|
||||
migrationBuilder.DropColumn(name: "CifsUsername", table: "CmsInstances");
|
||||
migrationBuilder.DropColumn(name: "CifsPassword", table: "CmsInstances");
|
||||
migrationBuilder.DropColumn(name: "CifsExtraOptions", table: "CmsInstances");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Re-add CIFS columns
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CifsServer", table: "CmsInstances", type: "TEXT", maxLength: 200, nullable: true);
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CifsShareName", table: "CmsInstances", type: "TEXT", maxLength: 500, nullable: true);
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CifsShareFolder", table: "CmsInstances", type: "TEXT", maxLength: 500, nullable: true);
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CifsUsername", table: "CmsInstances", type: "TEXT", maxLength: 200, nullable: true);
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CifsPassword", table: "CmsInstances", type: "TEXT", maxLength: 1000, nullable: true);
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CifsExtraOptions", table: "CmsInstances", type: "TEXT", maxLength: 500, nullable: true);
|
||||
|
||||
// Copy NFS data back to CIFS columns
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
UPDATE CmsInstances
|
||||
SET CifsServer = NfsServer,
|
||||
CifsShareName = CASE WHEN NfsExport IS NOT NULL THEN LTRIM(NfsExport, '/') ELSE NULL END,
|
||||
CifsShareFolder = NfsExportFolder,
|
||||
CifsExtraOptions = NfsExtraOptions
|
||||
WHERE NfsServer IS NOT NULL;
|
||||
""");
|
||||
|
||||
// Drop NFS columns
|
||||
migrationBuilder.DropColumn(name: "NfsServer", table: "CmsInstances");
|
||||
migrationBuilder.DropColumn(name: "NfsExport", table: "CmsInstances");
|
||||
migrationBuilder.DropColumn(name: "NfsExportFolder", table: "CmsInstances");
|
||||
migrationBuilder.DropColumn(name: "NfsExtraOptions", table: "CmsInstances");
|
||||
}
|
||||
}
|
||||
}
|
||||
347
OTSSignsOrchestrator.Core/Migrations/20260219020727_AddNewtCredentials.Designer.cs
generated
Normal file
347
OTSSignsOrchestrator.Core/Migrations/20260219020727_AddNewtCredentials.Designer.cs
generated
Normal file
@@ -0,0 +1,347 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using OTSSignsOrchestrator.Core.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace OTSSignsOrchestrator.Core.Migrations
|
||||
{
|
||||
[DbContext(typeof(XiboContext))]
|
||||
[Migration("20260219020727_AddNewtCredentials")]
|
||||
partial class AddNewtCredentials
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.2");
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.AppSetting", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsSensitive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.HasIndex("Category");
|
||||
|
||||
b.ToTable("AppSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.CmsInstance", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CmsServerName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Constraints")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CustomerAbbrev")
|
||||
.IsRequired()
|
||||
.HasMaxLength(3)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CustomerName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("HostHttpPort")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("LibraryHostPath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NewtId")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NewtSecret")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NfsExport")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NfsExportFolder")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NfsExtraOptions")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NfsServer")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SmtpServer")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SmtpUsername")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("SshHostId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StackName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("TemplateCacheKey")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("TemplateLastFetch")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TemplateRepoPat")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TemplateRepoUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ThemeHostPath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("XiboApiTestStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("XiboApiTestedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("XiboPassword")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("XiboUsername")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CustomerName");
|
||||
|
||||
b.HasIndex("SshHostId");
|
||||
|
||||
b.HasIndex("StackName")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CmsInstances");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.OperationLog", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("DurationMs")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("InstanceId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Operation")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId");
|
||||
|
||||
b.HasIndex("Operation");
|
||||
|
||||
b.HasIndex("Timestamp");
|
||||
|
||||
b.ToTable("OperationLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.SecretMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CustomerName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsGlobal")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("LastRotatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SecretMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.SshHost", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Host")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("KeyPassphrase")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Label")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool?>("LastTestSuccess")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("LastTestedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Port")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PrivateKeyPath")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("UseKeyAuth")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Label")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SshHosts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.CmsInstance", b =>
|
||||
{
|
||||
b.HasOne("OTSSignsOrchestrator.Core.Models.Entities.SshHost", "SshHost")
|
||||
.WithMany("Instances")
|
||||
.HasForeignKey("SshHostId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("SshHost");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.OperationLog", b =>
|
||||
{
|
||||
b.HasOne("OTSSignsOrchestrator.Core.Models.Entities.CmsInstance", "Instance")
|
||||
.WithMany("OperationLogs")
|
||||
.HasForeignKey("InstanceId");
|
||||
|
||||
b.Navigation("Instance");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.CmsInstance", b =>
|
||||
{
|
||||
b.Navigation("OperationLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.SshHost", b =>
|
||||
{
|
||||
b.Navigation("Instances");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace OTSSignsOrchestrator.Core.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddNewtCredentials : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "NewtId",
|
||||
table: "CmsInstances",
|
||||
type: "TEXT",
|
||||
maxLength: 500,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "NewtSecret",
|
||||
table: "CmsInstances",
|
||||
type: "TEXT",
|
||||
maxLength: 500,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "NewtId",
|
||||
table: "CmsInstances");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "NewtSecret",
|
||||
table: "CmsInstances");
|
||||
}
|
||||
}
|
||||
}
|
||||
153
OTSSignsOrchestrator.Core/Migrations/20260219121529_RemoveCmsInstancesAndSecretMetadata.Designer.cs
generated
Normal file
153
OTSSignsOrchestrator.Core/Migrations/20260219121529_RemoveCmsInstancesAndSecretMetadata.Designer.cs
generated
Normal file
@@ -0,0 +1,153 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using OTSSignsOrchestrator.Core.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace OTSSignsOrchestrator.Core.Migrations
|
||||
{
|
||||
[DbContext(typeof(XiboContext))]
|
||||
[Migration("20260219121529_RemoveCmsInstancesAndSecretMetadata")]
|
||||
partial class RemoveCmsInstancesAndSecretMetadata
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.2");
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.AppSetting", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsSensitive")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.HasIndex("Category");
|
||||
|
||||
b.ToTable("AppSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.OperationLog", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long?>("DurationMs")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Operation")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("StackName")
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("Timestamp")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UserId")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Operation");
|
||||
|
||||
b.HasIndex("StackName");
|
||||
|
||||
b.HasIndex("Timestamp");
|
||||
|
||||
b.ToTable("OperationLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.SshHost", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Host")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("KeyPassphrase")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Label")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool?>("LastTestSuccess")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("LastTestedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Port")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PrivateKeyPath")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("UseKeyAuth")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Label")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SshHosts");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace OTSSignsOrchestrator.Core.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveCmsInstancesAndSecretMetadata : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_OperationLogs_CmsInstances_InstanceId",
|
||||
table: "OperationLogs");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "CmsInstances");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "SecretMetadata");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_OperationLogs_InstanceId",
|
||||
table: "OperationLogs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "InstanceId",
|
||||
table: "OperationLogs");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "StackName",
|
||||
table: "OperationLogs",
|
||||
type: "TEXT",
|
||||
maxLength: 150,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OperationLogs_StackName",
|
||||
table: "OperationLogs",
|
||||
column: "StackName");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_OperationLogs_StackName",
|
||||
table: "OperationLogs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "StackName",
|
||||
table: "OperationLogs");
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "InstanceId",
|
||||
table: "OperationLogs",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CmsInstances",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
SshHostId = table.Column<Guid>(type: "TEXT", nullable: true),
|
||||
CmsServerName = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
Constraints = table.Column<string>(type: "TEXT", maxLength: 2000, nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
CustomerAbbrev = table.Column<string>(type: "TEXT", maxLength: 3, nullable: false),
|
||||
CustomerName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||
DeletedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
HostHttpPort = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
LibraryHostPath = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false),
|
||||
NewtId = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
||||
NewtSecret = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
||||
NfsExport = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
||||
NfsExportFolder = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
||||
NfsExtraOptions = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
||||
NfsServer = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
|
||||
SmtpServer = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
SmtpUsername = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
|
||||
StackName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
|
||||
Status = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
TemplateCacheKey = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||
TemplateLastFetch = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
TemplateRepoPat = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
|
||||
TemplateRepoUrl = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false),
|
||||
ThemeHostPath = table.Column<string>(type: "TEXT", maxLength: 500, nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
XiboApiTestStatus = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
XiboApiTestedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
XiboPassword = table.Column<string>(type: "TEXT", maxLength: 1000, nullable: true),
|
||||
XiboUsername = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CmsInstances", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_CmsInstances_SshHosts_SshHostId",
|
||||
column: x => x.SshHostId,
|
||||
principalTable: "SshHosts",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SecretMetadata",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
CustomerName = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
|
||||
IsGlobal = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
LastRotatedAt = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SecretMetadata", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OperationLogs_InstanceId",
|
||||
table: "OperationLogs",
|
||||
column: "InstanceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CmsInstances_CustomerName",
|
||||
table: "CmsInstances",
|
||||
column: "CustomerName");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CmsInstances_SshHostId",
|
||||
table: "CmsInstances",
|
||||
column: "SshHostId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CmsInstances_StackName",
|
||||
table: "CmsInstances",
|
||||
column: "StackName",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SecretMetadata_Name",
|
||||
table: "SecretMetadata",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_OperationLogs_CmsInstances_InstanceId",
|
||||
table: "OperationLogs",
|
||||
column: "InstanceId",
|
||||
principalTable: "CmsInstances",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,140 +45,6 @@ namespace OTSSignsOrchestrator.Core.Migrations
|
||||
b.ToTable("AppSettings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.CmsInstance", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CifsExtraOptions")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CifsPassword")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CifsServer")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CifsShareFolder")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CifsShareName")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CifsUsername")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CmsServerName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Constraints")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CustomerAbbrev")
|
||||
.IsRequired()
|
||||
.HasMaxLength(3)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CustomerName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("HostHttpPort")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("LibraryHostPath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SmtpServer")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SmtpUsername")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("SshHostId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StackName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("TemplateCacheKey")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("TemplateLastFetch")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TemplateRepoPat")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("TemplateRepoUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ThemeHostPath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("XiboApiTestStatus")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("XiboApiTestedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("XiboPassword")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("XiboUsername")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CustomerName");
|
||||
|
||||
b.HasIndex("SshHostId");
|
||||
|
||||
b.HasIndex("StackName")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CmsInstances");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.OperationLog", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -188,9 +54,6 @@ namespace OTSSignsOrchestrator.Core.Migrations
|
||||
b.Property<long?>("DurationMs")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid?>("InstanceId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
@@ -198,6 +61,10 @@ namespace OTSSignsOrchestrator.Core.Migrations
|
||||
b.Property<int>("Operation")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("StackName")
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -210,47 +77,15 @@ namespace OTSSignsOrchestrator.Core.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InstanceId");
|
||||
|
||||
b.HasIndex("Operation");
|
||||
|
||||
b.HasIndex("StackName");
|
||||
|
||||
b.HasIndex("Timestamp");
|
||||
|
||||
b.ToTable("OperationLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.SecretMetadata", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CustomerName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsGlobal")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime?>("LastRotatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SecretMetadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.SshHost", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -309,35 +144,6 @@ namespace OTSSignsOrchestrator.Core.Migrations
|
||||
|
||||
b.ToTable("SshHosts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.CmsInstance", b =>
|
||||
{
|
||||
b.HasOne("OTSSignsOrchestrator.Core.Models.Entities.SshHost", "SshHost")
|
||||
.WithMany("Instances")
|
||||
.HasForeignKey("SshHostId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("SshHost");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.OperationLog", b =>
|
||||
{
|
||||
b.HasOne("OTSSignsOrchestrator.Core.Models.Entities.CmsInstance", "Instance")
|
||||
.WithMany("OperationLogs")
|
||||
.HasForeignKey("InstanceId");
|
||||
|
||||
b.Navigation("Instance");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.CmsInstance", b =>
|
||||
{
|
||||
b.Navigation("OperationLogs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OTSSignsOrchestrator.Core.Models.Entities.SshHost", b =>
|
||||
{
|
||||
b.Navigation("Instances");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,24 +22,19 @@ public class CreateInstanceDto
|
||||
[MaxLength(500)]
|
||||
public string? NewtSecret { get; set; }
|
||||
|
||||
// ── CIFS / SMB credentials (optional — falls back to global settings) ──
|
||||
// ── NFS volume settings (optional — falls back to global settings) ──
|
||||
|
||||
[MaxLength(200)]
|
||||
public string? CifsServer { get; set; }
|
||||
public string? NfsServer { get; set; }
|
||||
|
||||
/// <summary>NFS export path on the server (e.g. "/srv/nfs").</summary>
|
||||
[MaxLength(500)]
|
||||
public string? NfsExport { get; set; }
|
||||
|
||||
/// <summary>Optional subfolder within the export (e.g. "ots_cms"). Omit to use the export root.</summary>
|
||||
[MaxLength(500)]
|
||||
public string? NfsExportFolder { get; set; }
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? CifsShareName { get; set; }
|
||||
|
||||
/// <summary>Optional subfolder within the share (e.g. "ots_cms"). Omit to use the share root.</summary>
|
||||
[MaxLength(500)]
|
||||
public string? CifsShareFolder { get; set; }
|
||||
|
||||
[MaxLength(200)]
|
||||
public string? CifsUsername { get; set; }
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? CifsPassword { get; set; }
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? CifsExtraOptions { get; set; }
|
||||
public string? NfsExtraOptions { get; set; }
|
||||
}
|
||||
|
||||
16
OTSSignsOrchestrator.Core/Models/DTOs/NodeInfo.cs
Normal file
16
OTSSignsOrchestrator.Core/Models/DTOs/NodeInfo.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace OTSSignsOrchestrator.Core.Models.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a node in the Docker Swarm cluster,
|
||||
/// parsed from <c>docker node ls</c> output.
|
||||
/// </summary>
|
||||
public class NodeInfo
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Hostname { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = string.Empty;
|
||||
public string Availability { get; set; } = string.Empty;
|
||||
public string ManagerStatus { get; set; } = string.Empty;
|
||||
public string EngineVersion { get; set; } = string.Empty;
|
||||
public string IpAddress { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -4,6 +4,32 @@ namespace OTSSignsOrchestrator.Core.Models.DTOs;
|
||||
|
||||
public class UpdateInstanceDto
|
||||
{
|
||||
// ── Identity / rendering context (populated from live service inspect) ──
|
||||
|
||||
/// <summary>Customer display name (used in log messages and comment header).</summary>
|
||||
[MaxLength(100)]
|
||||
public string? CustomerName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 3-letter abbreviation. If null, derived automatically from the stack name
|
||||
/// by stripping the "-cms-stack" suffix.
|
||||
/// </summary>
|
||||
[MaxLength(3)]
|
||||
public string? CustomerAbbrev { get; set; }
|
||||
|
||||
/// <summary>Public hostname for the CMS (e.g. "acm.ots-signs.com").</summary>
|
||||
[MaxLength(200)]
|
||||
public string? CmsServerName { get; set; }
|
||||
|
||||
/// <summary>Host-side HTTP port (defaults to 80 when null).</summary>
|
||||
public int? HostHttpPort { get; set; }
|
||||
|
||||
/// <summary>Host path bind-mounted as the theme directory.</summary>
|
||||
[MaxLength(500)]
|
||||
public string? ThemeHostPath { get; set; }
|
||||
|
||||
// ── Optional overrides (null = keep / use global settings) ──
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? TemplateRepoUrl { get; set; }
|
||||
|
||||
@@ -18,30 +44,29 @@ public class UpdateInstanceDto
|
||||
|
||||
public List<string>? Constraints { get; set; }
|
||||
|
||||
[MaxLength(200)]
|
||||
public string? XiboUsername { get; set; }
|
||||
// ── NFS volume settings (per-instance) ──
|
||||
|
||||
[MaxLength(200)]
|
||||
public string? XiboPassword { get; set; }
|
||||
public string? NfsServer { get; set; }
|
||||
|
||||
// ── CIFS / SMB credentials (per-instance) ──
|
||||
/// <summary>NFS export path on the server (e.g. "/srv/nfs").</summary>
|
||||
[MaxLength(500)]
|
||||
public string? NfsExport { get; set; }
|
||||
|
||||
[MaxLength(200)]
|
||||
public string? CifsServer { get; set; }
|
||||
/// <summary>Optional subfolder within the export (e.g. "ots_cms"). Omit to use the export root.</summary>
|
||||
[MaxLength(500)]
|
||||
public string? NfsExportFolder { get; set; }
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? CifsShareName { get; set; }
|
||||
public string? NfsExtraOptions { get; set; }
|
||||
|
||||
/// <summary>Optional subfolder within the share (e.g. "ots_cms"). Omit to use the share root.</summary>
|
||||
// ── Pangolin / Newt tunnel settings ──
|
||||
|
||||
/// <summary>Pangolin Newt ID for the tunnel service.</summary>
|
||||
[MaxLength(500)]
|
||||
public string? CifsShareFolder { get; set; }
|
||||
|
||||
[MaxLength(200)]
|
||||
public string? CifsUsername { get; set; }
|
||||
public string? NewtId { get; set; }
|
||||
|
||||
/// <summary>Pangolin Newt Secret for the tunnel service.</summary>
|
||||
[MaxLength(500)]
|
||||
public string? CifsPassword { get; set; }
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? CifsExtraOptions { get; set; }
|
||||
public string? NewtSecret { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace OTSSignsOrchestrator.Core.Models.Entities;
|
||||
|
||||
public enum InstanceStatus
|
||||
{
|
||||
Deploying,
|
||||
Active,
|
||||
Error,
|
||||
Deleted
|
||||
}
|
||||
|
||||
public enum XiboApiTestStatus
|
||||
{
|
||||
Unknown,
|
||||
Success,
|
||||
Failed
|
||||
}
|
||||
|
||||
public class CmsInstance
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[Required, MaxLength(100)]
|
||||
public string CustomerName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Exactly 3 lowercase letters used to derive all resource names.</summary>
|
||||
[MaxLength(3)]
|
||||
public string CustomerAbbrev { get; set; } = string.Empty;
|
||||
|
||||
[Required, MaxLength(100)]
|
||||
public string StackName { get; set; } = string.Empty;
|
||||
|
||||
[Required, MaxLength(200)]
|
||||
public string CmsServerName { get; set; } = string.Empty;
|
||||
|
||||
[Required, Range(1024, 65535)]
|
||||
public int HostHttpPort { get; set; }
|
||||
|
||||
[Required, MaxLength(500)]
|
||||
public string ThemeHostPath { get; set; } = string.Empty;
|
||||
|
||||
[Required, MaxLength(500)]
|
||||
public string LibraryHostPath { get; set; } = string.Empty;
|
||||
|
||||
[Required, MaxLength(200)]
|
||||
public string SmtpServer { get; set; } = string.Empty;
|
||||
|
||||
[Required, MaxLength(200)]
|
||||
public string SmtpUsername { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// JSON array of placement constraints, e.g. ["node.labels.xibo==true"]
|
||||
/// </summary>
|
||||
[MaxLength(2000)]
|
||||
public string? Constraints { get; set; }
|
||||
|
||||
[Required, MaxLength(500)]
|
||||
public string TemplateRepoUrl { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? TemplateRepoPat { get; set; }
|
||||
|
||||
public DateTime? TemplateLastFetch { get; set; }
|
||||
|
||||
[MaxLength(100)]
|
||||
public string? TemplateCacheKey { get; set; }
|
||||
|
||||
public InstanceStatus Status { get; set; } = InstanceStatus.Deploying;
|
||||
|
||||
/// <summary>Encrypted Xibo admin username for API access.</summary>
|
||||
[MaxLength(500)]
|
||||
public string? XiboUsername { get; set; }
|
||||
|
||||
/// <summary>Encrypted Xibo admin password. Never logged; encrypted at rest.</summary>
|
||||
[MaxLength(1000)]
|
||||
public string? XiboPassword { get; set; }
|
||||
|
||||
public XiboApiTestStatus XiboApiTestStatus { get; set; } = XiboApiTestStatus.Unknown;
|
||||
|
||||
public DateTime? XiboApiTestedAt { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
/// <summary>Soft delete marker.</summary>
|
||||
public DateTime? DeletedAt { get; set; }
|
||||
|
||||
// ── CIFS / SMB credentials (per-instance) ─────────────────────────────
|
||||
|
||||
[MaxLength(200)]
|
||||
public string? CifsServer { get; set; }
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? CifsShareName { get; set; }
|
||||
|
||||
/// <summary>Optional subfolder within the share (e.g. "ots_cms"). Omit to use the share root.</summary>
|
||||
[MaxLength(500)]
|
||||
public string? CifsShareFolder { get; set; }
|
||||
|
||||
[MaxLength(200)]
|
||||
public string? CifsUsername { get; set; }
|
||||
|
||||
/// <summary>Encrypted CIFS password. Never logged; encrypted at rest.</summary>
|
||||
[MaxLength(1000)]
|
||||
public string? CifsPassword { get; set; }
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? CifsExtraOptions { get; set; }
|
||||
|
||||
/// <summary>ID of the SshHost this instance is deployed to.</summary>
|
||||
public Guid? SshHostId { get; set; }
|
||||
|
||||
[ForeignKey(nameof(SshHostId))]
|
||||
public SshHost? SshHost { get; set; }
|
||||
|
||||
public ICollection<OperationLog> OperationLogs { get; set; } = new List<OperationLog>();
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace OTSSignsOrchestrator.Core.Models.Entities;
|
||||
|
||||
@@ -28,10 +27,9 @@ public class OperationLog
|
||||
|
||||
public OperationType Operation { get; set; }
|
||||
|
||||
public Guid? InstanceId { get; set; }
|
||||
|
||||
[ForeignKey(nameof(InstanceId))]
|
||||
public CmsInstance? Instance { get; set; }
|
||||
/// <summary>Name of the Docker stack this operation relates to (e.g. "acm-cms-stack").</summary>
|
||||
[MaxLength(150)]
|
||||
public string? StackName { get; set; }
|
||||
|
||||
[MaxLength(200)]
|
||||
public string? UserId { get; set; }
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace OTSSignsOrchestrator.Core.Models.Entities;
|
||||
|
||||
public class SecretMetadata
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[Required, MaxLength(200)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public bool IsGlobal { get; set; }
|
||||
|
||||
[MaxLength(100)]
|
||||
public string? CustomerName { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime? LastRotatedAt { get; set; }
|
||||
}
|
||||
@@ -54,6 +54,4 @@ public class SshHost
|
||||
public DateTime? LastTestedAt { get; set; }
|
||||
|
||||
public bool? LastTestSuccess { get; set; }
|
||||
|
||||
public ICollection<CmsInstance> Instances { get; set; } = new List<CmsInstance>();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace OTSSignsOrchestrator.Core.Services;
|
||||
@@ -28,7 +29,8 @@ public class ComposeRenderService
|
||||
if (string.IsNullOrWhiteSpace(templateYaml))
|
||||
throw new ArgumentException("Template YAML is empty. Ensure template.yml exists in the configured git repository.");
|
||||
|
||||
var cifsOpts = BuildCifsOpts(ctx);
|
||||
var nfsOpts = BuildNfsOpts(ctx);
|
||||
var nfsDevicePrefix = BuildNfsDevicePrefix(ctx);
|
||||
|
||||
return templateYaml
|
||||
.Replace("{{ABBREV}}", ctx.CustomerAbbrev)
|
||||
@@ -59,40 +61,87 @@ public class ComposeRenderService
|
||||
.Replace("{{PANGOLIN_ENDPOINT}}", ctx.PangolinEndpoint)
|
||||
.Replace("{{NEWT_ID}}", ctx.NewtId ?? "CONFIGURE_ME")
|
||||
.Replace("{{NEWT_SECRET}}", ctx.NewtSecret ?? "CONFIGURE_ME")
|
||||
.Replace("{{CIFS_SERVER}}", (ctx.CifsServer ?? string.Empty).TrimEnd('/'))
|
||||
.Replace("{{CIFS_SHARE_NAME}}", BuildSharePath(ctx.CifsShareName, ctx.CifsShareFolder))
|
||||
// Legacy token — was a path component (e.g. "/sharename"), so templates concatenate
|
||||
// it directly after the server: //{{CIFS_SERVER}}{{CIFS_SHARE_BASE_PATH}}/...
|
||||
// We must keep the leading "/" to produce a valid device path.
|
||||
.Replace("{{CIFS_SHARE_BASE_PATH}}", "/" + BuildSharePath(ctx.CifsShareName, ctx.CifsShareFolder))
|
||||
.Replace("{{CIFS_USERNAME}}", ctx.CifsUsername ?? string.Empty)
|
||||
.Replace("{{CIFS_PASSWORD}}", ctx.CifsPassword ?? string.Empty)
|
||||
.Replace("{{CIFS_OPTS}}", cifsOpts);
|
||||
.Replace("{{NFS_DEVICE_PREFIX}}", nfsDevicePrefix)
|
||||
.Replace("{{NFS_OPTS}}", nfsOpts)
|
||||
// ── Legacy CIFS token compatibility ─────────────────────────────
|
||||
// External git template repos may still contain old CIFS tokens.
|
||||
// Map them to NFS equivalents so those templates render correctly.
|
||||
.Replace("{{CIFS_SERVER}}", ctx.NfsServer ?? string.Empty)
|
||||
.Replace("{{CIFS_SHARE_NAME}}", BuildLegacySharePath(ctx))
|
||||
.Replace("{{CIFS_SHARE_BASE_PATH}}", "/" + BuildLegacySharePath(ctx))
|
||||
.Replace("{{CIFS_USERNAME}}", string.Empty)
|
||||
.Replace("{{CIFS_PASSWORD}}", string.Empty)
|
||||
.Replace("{{CIFS_OPTS}}", nfsOpts);
|
||||
}
|
||||
|
||||
private static string BuildCifsOpts(RenderContext ctx)
|
||||
/// <summary>
|
||||
/// Builds a legacy-compatible share path from NFS export + folder for old CIFS templates.
|
||||
/// Maps NFS export/folder to the path that was previously the CIFS share name/folder.
|
||||
/// </summary>
|
||||
private static string BuildLegacySharePath(RenderContext ctx)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ctx.CifsServer))
|
||||
var export = (ctx.NfsExport ?? string.Empty).Trim('/');
|
||||
var folder = (ctx.NfsExportFolder ?? string.Empty).Trim('/');
|
||||
return string.IsNullOrEmpty(folder) ? export : $"{export}/{folder}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the NFS mount options string for Docker volume driver_opts.
|
||||
/// Format: "addr=<server>,nfsvers=4,proto=tcp[,extraOptions]".
|
||||
/// </summary>
|
||||
private static string BuildNfsOpts(RenderContext ctx)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ctx.NfsServer))
|
||||
return string.Empty;
|
||||
|
||||
// vers=3.0 is required by most modern SMB servers (e.g. Hetzner Storage Box).
|
||||
// Without it, mount.cifs may negotiate SMBv1/2.1 which gets rejected as "permission denied".
|
||||
var opts = $"addr={ctx.CifsServer},username={ctx.CifsUsername},password={ctx.CifsPassword},vers=3.0";
|
||||
if (!string.IsNullOrWhiteSpace(ctx.CifsExtraOptions))
|
||||
opts += $",{ctx.CifsExtraOptions}";
|
||||
var opts = $"addr={ctx.NfsServer},nfsvers=4,proto=tcp";
|
||||
if (!string.IsNullOrWhiteSpace(ctx.NfsExtraOptions))
|
||||
opts += $",{ctx.NfsExtraOptions}";
|
||||
return opts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combines share name and optional subfolder into a single path segment.
|
||||
/// e.g. ("u548897-sub1", "ots_cms") → "u548897-sub1/ots_cms"
|
||||
/// ("u548897-sub1", null) → "u548897-sub1"
|
||||
/// Builds the NFS device prefix used in volume definitions.
|
||||
/// Format: ":/export[/subfolder]" (the colon is part of the device path for NFS).
|
||||
/// e.g. ":/srv/nfs/ots_cms" or ":/srv/nfs".
|
||||
/// </summary>
|
||||
private static string BuildSharePath(string? shareName, string? shareFolder)
|
||||
private static string BuildNfsDevicePrefix(RenderContext ctx)
|
||||
{
|
||||
var name = (shareName ?? string.Empty).Trim('/');
|
||||
var folder = (shareFolder ?? string.Empty).Trim('/');
|
||||
return string.IsNullOrEmpty(folder) ? name : $"{name}/{folder}";
|
||||
var export = (ctx.NfsExport ?? string.Empty).Trim('/');
|
||||
var folder = (ctx.NfsExportFolder ?? string.Empty).Trim('/');
|
||||
var path = string.IsNullOrEmpty(folder) ? export : $"{export}/{folder}";
|
||||
return $":/{path}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts NFS volume device paths from rendered compose YAML, then strips the
|
||||
/// NFS export prefix to return just the relative folder paths that need to exist
|
||||
/// on the NFS server. Works regardless of the template's naming convention
|
||||
/// (hierarchical <c>ots/cms-custom</c> vs flat <c>ots-cms-custom</c>).
|
||||
/// </summary>
|
||||
public static List<string> ExtractNfsDeviceFolders(string renderedYaml, string nfsExport, string? nfsExportFolder = null)
|
||||
{
|
||||
// NFS device lines look like: device: ":/mnt/Export/folder/ots-cms-custom"
|
||||
// The colon prefix is the NFS device convention.
|
||||
var matches = Regex.Matches(renderedYaml, @"device:\s*""?:(/[^""]+)""?", RegexOptions.IgnoreCase);
|
||||
var export = (nfsExport ?? string.Empty).Trim('/');
|
||||
var subFolder = (nfsExportFolder ?? string.Empty).Trim('/');
|
||||
var prefix = string.IsNullOrEmpty(subFolder) ? $"/{export}" : $"/{export}/{subFolder}";
|
||||
|
||||
var folders = new List<string>();
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
var devicePath = match.Groups[1].Value; // e.g. /mnt/DS-SwarmVolumes/Volumes/ots-cms-custom
|
||||
if (devicePath.StartsWith(prefix + "/"))
|
||||
{
|
||||
// Strip the export prefix to get the relative folder path
|
||||
var relative = devicePath[(prefix.Length + 1)..]; // e.g. ots-cms-custom or ots/cms-custom
|
||||
if (!string.IsNullOrEmpty(relative))
|
||||
folders.Add(relative);
|
||||
}
|
||||
}
|
||||
|
||||
return folders;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -134,6 +183,9 @@ public class ComposeRenderService
|
||||
CMS_PHP_MAX_EXECUTION_TIME: "{{PHP_MAX_EXECUTION_TIME}}"
|
||||
secrets:
|
||||
- {{ABBREV}}-cms-db-password
|
||||
- {{ABBREV}}-cms-db-user
|
||||
- global_mysql_host
|
||||
- global_mysql_port
|
||||
volumes:
|
||||
- {{ABBREV}}-cms-custom:/var/www/cms/custom
|
||||
- {{ABBREV}}-cms-backup:/var/www/backup
|
||||
@@ -199,37 +251,43 @@ public class ComposeRenderService
|
||||
{{ABBREV}}-cms-custom:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: cifs
|
||||
device: //{{CIFS_SERVER}}/{{CIFS_SHARE_NAME}}/{{ABBREV}}-cms-custom
|
||||
o: {{CIFS_OPTS}}
|
||||
type: nfs
|
||||
device: "{{NFS_DEVICE_PREFIX}}/{{ABBREV}}/cms-custom"
|
||||
o: "{{NFS_OPTS}}"
|
||||
{{ABBREV}}-cms-backup:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: cifs
|
||||
device: //{{CIFS_SERVER}}/{{CIFS_SHARE_NAME}}/{{ABBREV}}-cms-backup
|
||||
o: {{CIFS_OPTS}}
|
||||
type: nfs
|
||||
device: "{{NFS_DEVICE_PREFIX}}/{{ABBREV}}/cms-backup"
|
||||
o: "{{NFS_OPTS}}"
|
||||
{{ABBREV}}-cms-library:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: cifs
|
||||
device: //{{CIFS_SERVER}}/{{CIFS_SHARE_NAME}}/{{ABBREV}}-cms-library
|
||||
o: {{CIFS_OPTS}}
|
||||
type: nfs
|
||||
device: "{{NFS_DEVICE_PREFIX}}/{{ABBREV}}/cms-library"
|
||||
o: "{{NFS_OPTS}}"
|
||||
{{ABBREV}}-cms-userscripts:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: cifs
|
||||
device: //{{CIFS_SERVER}}/{{CIFS_SHARE_NAME}}/{{ABBREV}}-cms-userscripts
|
||||
o: {{CIFS_OPTS}}
|
||||
type: nfs
|
||||
device: "{{NFS_DEVICE_PREFIX}}/{{ABBREV}}/cms-userscripts"
|
||||
o: "{{NFS_OPTS}}"
|
||||
{{ABBREV}}-cms-ca-certs:
|
||||
driver: local
|
||||
driver_opts:
|
||||
type: cifs
|
||||
device: //{{CIFS_SERVER}}/{{CIFS_SHARE_NAME}}/{{ABBREV}}-cms-ca-certs
|
||||
o: {{CIFS_OPTS}}
|
||||
type: nfs
|
||||
device: "{{NFS_DEVICE_PREFIX}}/{{ABBREV}}/cms-ca-certs"
|
||||
o: "{{NFS_OPTS}}"
|
||||
|
||||
secrets:
|
||||
{{ABBREV}}-cms-db-password:
|
||||
external: true
|
||||
{{ABBREV}}-cms-db-user:
|
||||
external: true
|
||||
global_mysql_host:
|
||||
external: true
|
||||
global_mysql_port:
|
||||
external: true
|
||||
""";
|
||||
}
|
||||
|
||||
@@ -277,12 +335,12 @@ public class RenderContext
|
||||
public string? NewtId { get; set; }
|
||||
public string? NewtSecret { get; set; }
|
||||
|
||||
// CIFS volume settings
|
||||
public string? CifsServer { get; set; }
|
||||
public string? CifsShareName { get; set; }
|
||||
/// <summary>Optional subfolder within the share (e.g. "ots_cms"). Empty/null = share root.</summary>
|
||||
public string? CifsShareFolder { get; set; }
|
||||
public string? CifsUsername { get; set; }
|
||||
public string? CifsPassword { get; set; }
|
||||
public string? CifsExtraOptions { get; set; }
|
||||
// NFS volume settings
|
||||
public string? NfsServer { get; set; }
|
||||
/// <summary>NFS export path on the server (e.g. "/srv/nfs" or "/export/data").</summary>
|
||||
public string? NfsExport { get; set; }
|
||||
/// <summary>Optional subfolder within the export (e.g. "ots_cms"). Empty/null = export root.</summary>
|
||||
public string? NfsExportFolder { get; set; }
|
||||
/// <summary>Additional NFS mount options appended after the defaults (nfsvers=4,proto=tcp).</summary>
|
||||
public string? NfsExtraOptions { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OTSSignsOrchestrator.Core.Configuration;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace OTSSignsOrchestrator.Core.Services;
|
||||
@@ -91,6 +92,11 @@ public class ComposeValidationService
|
||||
|
||||
if (HasKey(root, "secrets") && root.Children[new YamlScalarNode("secrets")] is YamlMappingNode secretsNode)
|
||||
{
|
||||
var presentSecrets = secretsNode.Children.Keys
|
||||
.OfType<YamlScalarNode>()
|
||||
.Select(k => k.Value!)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var (key, value) in secretsNode.Children)
|
||||
{
|
||||
if (value is YamlMappingNode secretNode)
|
||||
@@ -99,6 +105,23 @@ public class ComposeValidationService
|
||||
warnings.Add($"Secret '{(key as YamlScalarNode)?.Value}' is not external.");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that all required MySQL secrets are declared
|
||||
if (!string.IsNullOrEmpty(customerAbbrev))
|
||||
{
|
||||
var requiredSecrets = new[]
|
||||
{
|
||||
AppConstants.CustomerMysqlPasswordSecretName(customerAbbrev),
|
||||
AppConstants.CustomerMysqlUserSecretName(customerAbbrev),
|
||||
AppConstants.GlobalMysqlHostSecretName,
|
||||
AppConstants.GlobalMysqlPortSecretName,
|
||||
};
|
||||
foreach (var required in requiredSecrets)
|
||||
{
|
||||
if (!presentSecrets.Contains(required))
|
||||
errors.Add($"Missing required secret: '{required}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ValidationResult { Errors = errors, Warnings = warnings };
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using MySqlConnector;
|
||||
using OTSSignsOrchestrator.Core.Models.DTOs;
|
||||
|
||||
namespace OTSSignsOrchestrator.Core.Services;
|
||||
@@ -17,25 +18,75 @@ public interface IDockerCliService
|
||||
Task<bool> EnsureDirectoryAsync(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the required folders exist on an SMB/CIFS share, creating any that are missing.
|
||||
/// If <paramref name="cifsShareFolder"/> is non-empty, creates it first as a subfolder of the share,
|
||||
/// Ensures the required folders exist on an NFS export, creating any that are missing.
|
||||
/// If <paramref name="nfsExportFolder"/> is non-empty, creates it first as a subfolder of the export,
|
||||
/// then creates the volume folders inside it.
|
||||
/// Uses smbclient on the remote host to interact with the share without requiring a mount.
|
||||
/// Temporarily mounts the NFS export on the Docker host to create the directories.
|
||||
/// </summary>
|
||||
Task<bool> EnsureSmbFoldersAsync(
|
||||
string cifsServer,
|
||||
string cifsShareName,
|
||||
string cifsUsername,
|
||||
string cifsPassword,
|
||||
Task<bool> EnsureNfsFoldersAsync(
|
||||
string nfsServer,
|
||||
string nfsExport,
|
||||
IEnumerable<string> folderNames,
|
||||
string? cifsShareFolder = null);
|
||||
string? nfsExportFolder = null);
|
||||
|
||||
/// <summary>
|
||||
/// Same as <see cref="EnsureNfsFoldersAsync"/> but returns the error message on failure
|
||||
/// so callers can surface actionable diagnostics.
|
||||
/// </summary>
|
||||
Task<(bool Success, string? Error)> EnsureNfsFoldersWithErrorAsync(
|
||||
string nfsServer,
|
||||
string nfsExport,
|
||||
IEnumerable<string> folderNames,
|
||||
string? nfsExportFolder = null);
|
||||
|
||||
/// <summary>
|
||||
/// Removes all Docker volumes whose names start with <paramref name="stackName"/>_.
|
||||
/// Volumes currently in use by running containers will be skipped.
|
||||
/// Safe for CIFS volumes since data lives on the remote share, not in the local volume.
|
||||
/// Safe for NFS volumes since data lives on the remote export, not in the local volume.
|
||||
/// </summary>
|
||||
Task<bool> RemoveStackVolumesAsync(string stackName);
|
||||
|
||||
/// <summary>
|
||||
/// Lists all nodes in the Docker Swarm cluster.
|
||||
/// Must be executed against a Swarm manager node.
|
||||
/// </summary>
|
||||
Task<List<NodeInfo>> ListNodesAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Force-updates a service so all its tasks are restarted and pick up any changed
|
||||
/// secrets or config (equivalent to docker service update --force).
|
||||
/// </summary>
|
||||
Task<bool> ForceUpdateServiceAsync(string serviceName);
|
||||
|
||||
/// <summary>
|
||||
/// Opens a <see cref="MySqlConnection"/> to a remote MySQL server through the
|
||||
/// implementation's transport (e.g. an SSH tunnel). The caller must dispose
|
||||
/// both the connection <b>and</b> the returned <c>tunnel</c> handle when finished.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A tuple of (connection, tunnel). <c>tunnel</c> is <see cref="IDisposable"/>
|
||||
/// and MUST be disposed after the connection is closed.
|
||||
/// </returns>
|
||||
Task<(MySqlConnection Connection, IDisposable Tunnel)> OpenMySqlConnectionAsync(
|
||||
string mysqlHost, int port,
|
||||
string adminUser, string adminPassword);
|
||||
|
||||
/// <summary>
|
||||
/// Executes <c>ALTER USER … IDENTIFIED BY …</c> on a remote MySQL server via
|
||||
/// <see cref="OpenMySqlConnectionAsync"/>.
|
||||
/// </summary>
|
||||
Task<(bool Success, string Error)> AlterMySqlUserPasswordAsync(
|
||||
string mysqlHost, int port,
|
||||
string adminUser, string adminPassword,
|
||||
string targetUser, string newPassword);
|
||||
|
||||
/// <summary>
|
||||
/// Atomically swaps one secret reference on a running service:
|
||||
/// removes <paramref name="oldSecretName"/> and adds <paramref name="newSecretName"/>,
|
||||
/// preserving the in-container path as <paramref name="targetAlias"/> (defaults to
|
||||
/// <paramref name="oldSecretName"/> when null, keeping the same /run/secrets/ filename).
|
||||
/// </summary>
|
||||
Task<bool> ServiceSwapSecretAsync(string serviceName, string oldSecretName, string newSecretName, string? targetAlias = null);
|
||||
}
|
||||
|
||||
public class StackInfo
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,7 @@ public class SettingsService
|
||||
public const string CatMySql = "MySql";
|
||||
public const string CatSmtp = "Smtp";
|
||||
public const string CatPangolin = "Pangolin";
|
||||
public const string CatCifs = "Cifs";
|
||||
public const string CatNfs = "Nfs";
|
||||
public const string CatDefaults = "Defaults";
|
||||
|
||||
// ── Key constants ──────────────────────────────────────────────────────
|
||||
@@ -49,13 +49,11 @@ public class SettingsService
|
||||
// Pangolin
|
||||
public const string PangolinEndpoint = "Pangolin.Endpoint";
|
||||
|
||||
// CIFS
|
||||
public const string CifsServer = "Cifs.Server";
|
||||
public const string CifsShareName = "Cifs.ShareName";
|
||||
public const string CifsShareFolder = "Cifs.ShareFolder";
|
||||
public const string CifsUsername = "Cifs.Username";
|
||||
public const string CifsPassword = "Cifs.Password";
|
||||
public const string CifsOptions = "Cifs.Options";
|
||||
// NFS
|
||||
public const string NfsServer = "Nfs.Server";
|
||||
public const string NfsExport = "Nfs.Export";
|
||||
public const string NfsExportFolder = "Nfs.ExportFolder";
|
||||
public const string NfsOptions = "Nfs.Options";
|
||||
|
||||
// Instance Defaults
|
||||
public const string DefaultCmsImage = "Defaults.CmsImage";
|
||||
|
||||
Reference in New Issue
Block a user