diff options
Diffstat (limited to 'src/marlin.web')
29 files changed, 2819 insertions, 121 deletions
diff --git a/src/marlin.web/Controllers/JobsController.cs b/src/marlin.web/Controllers/JobsController.cs index 1cfdadb..c86cb2d 100644 --- a/src/marlin.web/Controllers/JobsController.cs +++ b/src/marlin.web/Controllers/JobsController.cs @@ -30,10 +30,23 @@ public class JobsController(ApplicationDbContext dbContext) : Controller return this.View(results); } + public record JobSpawnDetails( + ICollection<VersionControlSource> Sources, + Guid? Vcs = null, + string? Ref = null, + string? Commit = null, + string? Recipe = null); + [HttpGet("spawn")] - public async Task<IActionResult> Spawn() + public async Task<IActionResult> Spawn(CancellationToken ct) { - return this.View(); + var sources = await dbContext.Sources + .Include(x => x.Branches) + .ThenInclude(x => x.Commit) + .ToListAsync(ct); + + var details = new JobSpawnDetails(sources); + return this.View(details); } public record JobSpawnRequest(Guid Vcs, string Ref, string Commit, string Recipe); @@ -62,4 +75,18 @@ public class JobsController(ApplicationDbContext dbContext) : Controller // cancel job // details job (recipe, status, agent, target, plan, log, artifacts) + [HttpGet("details/{id}")] + public async Task<IActionResult> Details(Guid id) + { + var job = await dbContext.Jobs + .Include(x => x.Agent) + .Include(x => x.Source) + .Include(x => x.StatusUpdates) + .FirstOrDefaultAsync(x => x.Id == id); + + if (job == null) + return this.NotFound(); + + return this.View(job); + } }
\ No newline at end of file diff --git a/src/marlin.web/Controllers/VCSController.cs b/src/marlin.web/Controllers/VCSController.cs index 86f655b..7370e0d 100644 --- a/src/marlin.web/Controllers/VCSController.cs +++ b/src/marlin.web/Controllers/VCSController.cs @@ -1,33 +1,47 @@ namespace marlin.web.Controllers; +using System.Text.Json; using core; using core.models; using Data; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Models; +using Services; [Route("vcs")] -public class VCSController(ApplicationDbContext dbContext) : Controller +public class VCSController(ApplicationDbContext dbContext, VersionControlSyncService syncService) : Controller { [HttpGet("list")] public async Task<IActionResult> List(CancellationToken ct) { var items = await dbContext.Sources + .Include(x => x.Branches) .ToListAsync(ct); return this.View(items); } + public record SecretOption(Guid Id, string Label); + [HttpGet("details/{id}")] public async Task<IActionResult> Details(Guid id, CancellationToken ct) { var item = await dbContext.Sources + .Include(x => x.Branches) + .ThenInclude(x => x.Commit) .FirstOrDefaultAsync(x => x.Id == id, ct); if (item == null) return this.NotFound(); - return this.View(item); + // Only Id/Label projected — a secret's Content never needs to leave the secrets pages. + var secretOptions = await dbContext.Secrets + .Where(x => x.SourceId == id) + .Select(x => new SecretOption(x.Id, x.Label)) + .ToListAsync(ct); + + return this.View((item, secretOptions)); } [HttpPost("edit/{id}")] @@ -43,8 +57,10 @@ public class VCSController(ApplicationDbContext dbContext) : Controller item.BranchFilter = vcs.BranchFilter; item.MainBranch = vcs.MainBranch; item.MinPollingInterval = vcs.MinPollingInterval; + item.AccessSecretId = vcs.AccessSecretId; await dbContext.SaveChangesAsync(ct); + await syncService.FetchSources(ct); return this.RedirectToAction("List"); } @@ -62,7 +78,79 @@ public class VCSController(ApplicationDbContext dbContext) : Controller await dbContext.AddAsync(vcs, ct); await dbContext.SaveChangesAsync(ct); + await syncService.FetchSources(ct); return this.RedirectToAction("Details", new { id = vcs.Id }); } + + public record SecretCreateRequest(string Label, string? Kind, bool MayReveal, string ContentJson); + + [HttpGet("details/{sourceId}/secrets/create")] + public async Task<IActionResult> CreateSecret(Guid sourceId, CancellationToken ct) + { + var source = await dbContext.Sources + .FirstOrDefaultAsync(x => x.Id == sourceId, ct); + + if (source == null) + return this.NotFound(); + + return this.View(source); + } + + [HttpPost("details/{sourceId}/secrets/create")] + public async Task<IActionResult> CreateSecretPost(Guid sourceId, SecretCreateRequest request, CancellationToken ct) + { + var source = await dbContext.Sources + .FirstOrDefaultAsync(x => x.Id == sourceId, ct); + + if (source == null) + return this.NotFound(); + + var content = JsonSerializer.Deserialize<Dictionary<string, string>>(request.ContentJson) ?? new(); + + var secret = new VersionControlSecret + { + SourceId = sourceId, + Label = request.Label, + Kind = request.Kind, + MayReveal = request.MayReveal, + Content = content + }; + + await dbContext.Secrets.AddAsync(secret, ct); + await dbContext.SaveChangesAsync(ct); + + return this.RedirectToAction("ListSecret", new { sourceId }); + } + + [HttpGet("details/{sourceId}/secrets/list")] + public async Task<IActionResult> ListSecret(Guid sourceId, CancellationToken ct) + { + var source = await dbContext.Sources + .FirstOrDefaultAsync(x => x.Id == sourceId, ct); + + if (source == null) + return this.NotFound(); + + var items = await dbContext.Secrets + .Where(x => x.SourceId == source.Id) + .ToListAsync<ISecret>(ct); + + return this.View((source, items)); + } + + [HttpPost("details/{sourceId}/secrets/delete/{id}")] + public async Task<IActionResult> DeleteSecret(Guid sourceId, Guid id, CancellationToken ct) + { + var secret = await dbContext.Secrets + .FirstOrDefaultAsync(x => x.SourceId == sourceId && x.Id == id, ct); + + if (secret == null) + return this.NotFound(); + + dbContext.Secrets.Remove(secret); + await dbContext.SaveChangesAsync(ct); + + return this.RedirectToAction("ListSecret", new { sourceId = sourceId }); + } }
\ No newline at end of file diff --git a/src/marlin.web/Data/ApplicationDbContext.cs b/src/marlin.web/Data/ApplicationDbContext.cs index 41ec4bc..2e05835 100644 --- a/src/marlin.web/Data/ApplicationDbContext.cs +++ b/src/marlin.web/Data/ApplicationDbContext.cs @@ -1,15 +1,31 @@ namespace marlin.web.Data; using System.Linq.Expressions; +using System.Text.Json; using core; using core.models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Models; + +// user-based session grant +// agent id/token grant +// agent job claim session grant +// service grant +public interface IActiveGrant +{ + bool CanAccessSource(Guid sourceId); +} public class ApplicationDbContext : DbContext { public DbSet<VersionControlSource> Sources => this.Set<VersionControlSource>(); + public DbSet<VersionControlledBranch> SourceBranches => this.Set<VersionControlledBranch>(); + public DbSet<VersionControlledCommit> SourceCommits => this.Set<VersionControlledCommit>(); + public DbSet<VersionControlledBlob> SourceBlobs => this.Set<VersionControlledBlob>(); + public DbSet<VersionControlSecret> Secrets => this.Set<VersionControlSecret>(); public DbSet<Job> Jobs => this.Set<Job>(); + public DbSet<Agent> Agents => this.Set<Agent>(); public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } @@ -18,7 +34,21 @@ public class ApplicationDbContext : DbContext { modelBuilder .Entity<Agent>() - .OwnsOne(p => p.Capabilities, b => b.ToJson()); + .Property(p => p.Capabilities) + .HasConversion( + v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null), + v => JsonSerializer.Deserialize<CapabilitySet>(v, (JsonSerializerOptions?)null)!); + + modelBuilder + .Entity<VersionControlledCommit>() + .HasAlternateKey(x => new { x.SourceId, x.Hash }); + + modelBuilder + .Entity<VersionControlSecret>() + .Property(p => p.Content) + .HasConversion( + v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null), + v => JsonSerializer.Deserialize<Dictionary<string, string>>(v, (JsonSerializerOptions?)null)!); base.OnModelCreating(modelBuilder); } diff --git a/src/marlin.web/Migrations/20260902214847_InitialCreate.Designer.cs b/src/marlin.web/Migrations/20260902214847_InitialCreate.Designer.cs deleted file mode 100644 index 21ab701..0000000 --- a/src/marlin.web/Migrations/20260902214847_InitialCreate.Designer.cs +++ /dev/null @@ -1,53 +0,0 @@ -// <auto-generated /> -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using marlin.web.Data; - -#nullable disable - -namespace marlin.web.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20260902214847_InitialCreate")] - partial class InitialCreate - { - /// <inheritdoc /> - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "10.0.11") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("marlin.core.VersionControlSource", b => - { - b.Property<Guid>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property<string>("BranchFilter") - .IsRequired() - .HasColumnType("text") - .HasColumnName("branch_filter"); - - b.Property<string>("UpstreamUrl") - .IsRequired() - .HasColumnType("text") - .HasColumnName("upstream_url"); - - b.HasKey("Id") - .HasName("pk_sources"); - - b.ToTable("sources", (string)null); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/marlin.web/Migrations/20260902214847_InitialCreate.cs b/src/marlin.web/Migrations/20260902214847_InitialCreate.cs deleted file mode 100644 index 6c91890..0000000 --- a/src/marlin.web/Migrations/20260902214847_InitialCreate.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace marlin.web.Migrations -{ - /// <inheritdoc /> - public partial class InitialCreate : Migration - { - /// <inheritdoc /> - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "sources", - columns: table => new - { - id = table.Column<Guid>(type: "uuid", nullable: false), - upstream_url = table.Column<string>(type: "text", nullable: false), - branch_filter = table.Column<string>(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_sources", x => x.id); - }); - } - - /// <inheritdoc /> - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "sources"); - } - } -} diff --git a/src/marlin.web/Migrations/20260903141622_InitialCreate.Designer.cs b/src/marlin.web/Migrations/20260903141622_InitialCreate.Designer.cs new file mode 100644 index 0000000..ad71457 --- /dev/null +++ b/src/marlin.web/Migrations/20260903141622_InitialCreate.Designer.cs @@ -0,0 +1,259 @@ +// <auto-generated /> +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using marlin.web.Data; + +#nullable disable + +namespace marlin.web.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260903141622_InitialCreate")] + partial class InitialCreate + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("marlin.core.models.Agent", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Capabilities") + .IsRequired() + .HasColumnType("text") + .HasColumnName("capabilities"); + + b.Property<DateTime>("LastSeen") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen"); + + b.Property<int>("MaxConcurrentJobs") + .HasColumnType("integer") + .HasColumnName("max_concurrent_jobs"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_agents"); + + b.ToTable("agents", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.Job", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid?>("AgentId") + .HasColumnType("uuid") + .HasColumnName("agent_id"); + + b.Property<string>("Commit") + .IsRequired() + .HasColumnType("text") + .HasColumnName("commit"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<string>("CreatedReason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_reason"); + + b.Property<string>("Recipe") + .IsRequired() + .HasColumnType("text") + .HasColumnName("recipe"); + + b.Property<string>("Ref") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ref"); + + b.Property<Guid>("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.Property<int>("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id") + .HasName("pk_jobs"); + + b.HasIndex("AgentId") + .HasDatabaseName("ix_jobs_agent_id"); + + b.HasIndex("SourceId") + .HasDatabaseName("ix_jobs_source_id"); + + b.ToTable("jobs", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.JobStatusUpdate", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("At") + .HasColumnType("timestamp with time zone") + .HasColumnName("at"); + + b.Property<Guid>("JobId") + .HasColumnType("uuid") + .HasColumnName("job_id"); + + b.Property<int>("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id") + .HasName("pk_job_status_update"); + + b.HasIndex("JobId") + .HasDatabaseName("ix_job_status_update_job_id"); + + b.ToTable("job_status_update", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlSource", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("BranchFilter") + .IsRequired() + .HasColumnType("text") + .HasColumnName("branch_filter"); + + b.Property<string>("MainBranch") + .IsRequired() + .HasColumnType("text") + .HasColumnName("main_branch"); + + b.Property<int>("MinPollingInterval") + .HasColumnType("integer") + .HasColumnName("min_polling_interval"); + + b.Property<string>("UpstreamUrl") + .IsRequired() + .HasColumnType("text") + .HasColumnName("upstream_url"); + + b.HasKey("Id") + .HasName("pk_sources"); + + b.ToTable("sources", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledBranch", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("HeadCommit") + .IsRequired() + .HasColumnType("text") + .HasColumnName("head_commit"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.HasKey("Id") + .HasName("pk_source_branches"); + + b.HasIndex("SourceId") + .HasDatabaseName("ix_source_branches_source_id"); + + b.ToTable("source_branches", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.Job", b => + { + b.HasOne("marlin.core.models.Agent", "Agent") + .WithMany() + .HasForeignKey("AgentId") + .HasConstraintName("fk_jobs_agents_agent_id"); + + b.HasOne("marlin.core.models.VersionControlSource", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_jobs_sources_source_id"); + + b.Navigation("Agent"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("marlin.core.models.JobStatusUpdate", b => + { + b.HasOne("marlin.core.models.Job", "Job") + .WithMany("StatusUpdates") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_job_status_update_jobs_job_id"); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledBranch", b => + { + b.HasOne("marlin.core.models.VersionControlSource", "Source") + .WithMany("Branches") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_source_branches_sources_source_id"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("marlin.core.models.Job", b => + { + b.Navigation("StatusUpdates"); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlSource", b => + { + b.Navigation("Branches"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/marlin.web/Migrations/20260903141622_InitialCreate.cs b/src/marlin.web/Migrations/20260903141622_InitialCreate.cs new file mode 100644 index 0000000..2ebcc67 --- /dev/null +++ b/src/marlin.web/Migrations/20260903141622_InitialCreate.cs @@ -0,0 +1,154 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace marlin.web.Migrations +{ + /// <inheritdoc /> + public partial class InitialCreate : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "agents", + columns: table => new + { + id = table.Column<Guid>(type: "uuid", nullable: false), + name = table.Column<string>(type: "text", nullable: false), + max_concurrent_jobs = table.Column<int>(type: "integer", nullable: false), + last_seen = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), + capabilities = table.Column<string>(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_agents", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "sources", + columns: table => new + { + id = table.Column<Guid>(type: "uuid", nullable: false), + upstream_url = table.Column<string>(type: "text", nullable: false), + main_branch = table.Column<string>(type: "text", nullable: false), + branch_filter = table.Column<string>(type: "text", nullable: false), + min_polling_interval = table.Column<int>(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_sources", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "jobs", + columns: table => new + { + id = table.Column<Guid>(type: "uuid", nullable: false), + source_id = table.Column<Guid>(type: "uuid", nullable: false), + @ref = table.Column<string>(name: "ref", type: "text", nullable: false), + commit = table.Column<string>(type: "text", nullable: false), + created_reason = table.Column<string>(type: "text", nullable: false), + created_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), + status = table.Column<int>(type: "integer", nullable: false), + agent_id = table.Column<Guid>(type: "uuid", nullable: true), + recipe = table.Column<string>(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_jobs", x => x.id); + table.ForeignKey( + name: "fk_jobs_agents_agent_id", + column: x => x.agent_id, + principalTable: "agents", + principalColumn: "id"); + table.ForeignKey( + name: "fk_jobs_sources_source_id", + column: x => x.source_id, + principalTable: "sources", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "source_branches", + columns: table => new + { + id = table.Column<Guid>(type: "uuid", nullable: false), + source_id = table.Column<Guid>(type: "uuid", nullable: false), + name = table.Column<string>(type: "text", nullable: false), + head_commit = table.Column<string>(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_source_branches", x => x.id); + table.ForeignKey( + name: "fk_source_branches_sources_source_id", + column: x => x.source_id, + principalTable: "sources", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "job_status_update", + columns: table => new + { + id = table.Column<Guid>(type: "uuid", nullable: false), + job_id = table.Column<Guid>(type: "uuid", nullable: false), + at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false), + status = table.Column<int>(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_job_status_update", x => x.id); + table.ForeignKey( + name: "fk_job_status_update_jobs_job_id", + column: x => x.job_id, + principalTable: "jobs", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_job_status_update_job_id", + table: "job_status_update", + column: "job_id"); + + migrationBuilder.CreateIndex( + name: "ix_jobs_agent_id", + table: "jobs", + column: "agent_id"); + + migrationBuilder.CreateIndex( + name: "ix_jobs_source_id", + table: "jobs", + column: "source_id"); + + migrationBuilder.CreateIndex( + name: "ix_source_branches_source_id", + table: "source_branches", + column: "source_id"); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "job_status_update"); + + migrationBuilder.DropTable( + name: "source_branches"); + + migrationBuilder.DropTable( + name: "jobs"); + + migrationBuilder.DropTable( + name: "agents"); + + migrationBuilder.DropTable( + name: "sources"); + } + } +} diff --git a/src/marlin.web/Migrations/20260905011100_AddCommitModel.Designer.cs b/src/marlin.web/Migrations/20260905011100_AddCommitModel.Designer.cs new file mode 100644 index 0000000..03f789e --- /dev/null +++ b/src/marlin.web/Migrations/20260905011100_AddCommitModel.Designer.cs @@ -0,0 +1,376 @@ +// <auto-generated /> +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using marlin.web.Data; + +#nullable disable + +namespace marlin.web.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260905011100_AddCommitModel")] + partial class AddCommitModel + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("marlin.core.models.Agent", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Capabilities") + .IsRequired() + .HasColumnType("text") + .HasColumnName("capabilities"); + + b.Property<DateTime>("LastSeen") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen"); + + b.Property<int>("MaxConcurrentJobs") + .HasColumnType("integer") + .HasColumnName("max_concurrent_jobs"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_agents"); + + b.ToTable("agents", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.Job", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid?>("AgentId") + .HasColumnType("uuid") + .HasColumnName("agent_id"); + + b.Property<string>("Commit") + .IsRequired() + .HasColumnType("text") + .HasColumnName("commit"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<string>("CreatedReason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_reason"); + + b.Property<string>("Recipe") + .IsRequired() + .HasColumnType("text") + .HasColumnName("recipe"); + + b.Property<string>("Ref") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ref"); + + b.Property<Guid>("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.Property<int>("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id") + .HasName("pk_jobs"); + + b.HasIndex("AgentId") + .HasDatabaseName("ix_jobs_agent_id"); + + b.HasIndex("SourceId") + .HasDatabaseName("ix_jobs_source_id"); + + b.ToTable("jobs", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.JobStatusUpdate", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("At") + .HasColumnType("timestamp with time zone") + .HasColumnName("at"); + + b.Property<Guid>("JobId") + .HasColumnType("uuid") + .HasColumnName("job_id"); + + b.Property<int>("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id") + .HasName("pk_job_status_update"); + + b.HasIndex("JobId") + .HasDatabaseName("ix_job_status_update_job_id"); + + b.ToTable("job_status_update", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlSource", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("BranchFilter") + .IsRequired() + .HasColumnType("text") + .HasColumnName("branch_filter"); + + b.Property<string>("MainBranch") + .IsRequired() + .HasColumnType("text") + .HasColumnName("main_branch"); + + b.Property<int>("MinPollingInterval") + .HasColumnType("integer") + .HasColumnName("min_polling_interval"); + + b.Property<string>("UpstreamUrl") + .IsRequired() + .HasColumnType("text") + .HasColumnName("upstream_url"); + + b.HasKey("Id") + .HasName("pk_sources"); + + b.ToTable("sources", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledBlob", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid>("CommitId") + .HasColumnType("uuid") + .HasColumnName("commit_id"); + + b.Property<string>("Content") + .IsRequired() + .HasColumnType("text") + .HasColumnName("content"); + + b.Property<string>("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property<Guid>("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.HasKey("Id") + .HasName("pk_source_blobs"); + + b.HasIndex("CommitId") + .HasDatabaseName("ix_source_blobs_commit_id"); + + b.HasIndex("SourceId") + .HasDatabaseName("ix_source_blobs_source_id"); + + b.ToTable("source_blobs", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledBranch", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid>("CommitId") + .HasColumnType("uuid") + .HasColumnName("commit_id"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.HasKey("Id") + .HasName("pk_source_branches"); + + b.HasIndex("CommitId") + .HasDatabaseName("ix_source_branches_commit_id"); + + b.HasIndex("SourceId") + .HasDatabaseName("ix_source_branches_source_id"); + + b.ToTable("source_branches", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledCommit", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<bool>("BlobsFetched") + .HasColumnType("boolean") + .HasColumnName("blobs_fetched"); + + b.Property<string>("Hash") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hash"); + + b.Property<Guid>("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.HasKey("Id") + .HasName("pk_source_commits"); + + b.HasAlternateKey("SourceId", "Hash") + .HasName("ak_source_commits_source_id_hash"); + + b.ToTable("source_commits", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.Job", b => + { + b.HasOne("marlin.core.models.Agent", "Agent") + .WithMany() + .HasForeignKey("AgentId") + .HasConstraintName("fk_jobs_agents_agent_id"); + + b.HasOne("marlin.core.models.VersionControlSource", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_jobs_sources_source_id"); + + b.Navigation("Agent"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("marlin.core.models.JobStatusUpdate", b => + { + b.HasOne("marlin.core.models.Job", "Job") + .WithMany("StatusUpdates") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_job_status_update_jobs_job_id"); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledBlob", b => + { + b.HasOne("marlin.core.models.VersionControlledCommit", "Commit") + .WithMany("Blobs") + .HasForeignKey("CommitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_source_blobs_source_commits_commit_id"); + + b.HasOne("marlin.core.models.VersionControlSource", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_source_blobs_sources_source_id"); + + b.Navigation("Commit"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledBranch", b => + { + b.HasOne("marlin.core.models.VersionControlledCommit", "Commit") + .WithMany("Branches") + .HasForeignKey("CommitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_source_branches_source_commits_commit_id"); + + b.HasOne("marlin.core.models.VersionControlSource", "Source") + .WithMany("Branches") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_source_branches_sources_source_id"); + + b.Navigation("Commit"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledCommit", b => + { + b.HasOne("marlin.core.models.VersionControlSource", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_source_commits_sources_source_id"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("marlin.core.models.Job", b => + { + b.Navigation("StatusUpdates"); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlSource", b => + { + b.Navigation("Branches"); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledCommit", b => + { + b.Navigation("Blobs"); + + b.Navigation("Branches"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/marlin.web/Migrations/20260905011100_AddCommitModel.cs b/src/marlin.web/Migrations/20260905011100_AddCommitModel.cs new file mode 100644 index 0000000..78a23d0 --- /dev/null +++ b/src/marlin.web/Migrations/20260905011100_AddCommitModel.cs @@ -0,0 +1,126 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace marlin.web.Migrations +{ + /// <inheritdoc /> + public partial class AddCommitModel : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "head_commit", + table: "source_branches"); + + migrationBuilder.AddColumn<Guid>( + name: "commit_id", + table: "source_branches", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.CreateTable( + name: "source_commits", + columns: table => new + { + id = table.Column<Guid>(type: "uuid", nullable: false), + source_id = table.Column<Guid>(type: "uuid", nullable: false), + blobs_fetched = table.Column<bool>(type: "boolean", nullable: false), + hash = table.Column<string>(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_source_commits", x => x.id); + table.UniqueConstraint("ak_source_commits_source_id_hash", x => new { x.source_id, x.hash }); + table.ForeignKey( + name: "fk_source_commits_sources_source_id", + column: x => x.source_id, + principalTable: "sources", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "source_blobs", + columns: table => new + { + id = table.Column<Guid>(type: "uuid", nullable: false), + source_id = table.Column<Guid>(type: "uuid", nullable: false), + commit_id = table.Column<Guid>(type: "uuid", nullable: false), + path = table.Column<string>(type: "text", nullable: false), + content = table.Column<string>(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_source_blobs", x => x.id); + table.ForeignKey( + name: "fk_source_blobs_source_commits_commit_id", + column: x => x.commit_id, + principalTable: "source_commits", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_source_blobs_sources_source_id", + column: x => x.source_id, + principalTable: "sources", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_source_branches_commit_id", + table: "source_branches", + column: "commit_id"); + + migrationBuilder.CreateIndex( + name: "ix_source_blobs_commit_id", + table: "source_blobs", + column: "commit_id"); + + migrationBuilder.CreateIndex( + name: "ix_source_blobs_source_id", + table: "source_blobs", + column: "source_id"); + + migrationBuilder.AddForeignKey( + name: "fk_source_branches_source_commits_commit_id", + table: "source_branches", + column: "commit_id", + principalTable: "source_commits", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_source_branches_source_commits_commit_id", + table: "source_branches"); + + migrationBuilder.DropTable( + name: "source_blobs"); + + migrationBuilder.DropTable( + name: "source_commits"); + + migrationBuilder.DropIndex( + name: "ix_source_branches_commit_id", + table: "source_branches"); + + migrationBuilder.DropColumn( + name: "commit_id", + table: "source_branches"); + + migrationBuilder.AddColumn<string>( + name: "head_commit", + table: "source_branches", + type: "text", + nullable: false, + defaultValue: ""); + } + } +} diff --git a/src/marlin.web/Migrations/20260905102259_AddSecretModel.Designer.cs b/src/marlin.web/Migrations/20260905102259_AddSecretModel.Designer.cs new file mode 100644 index 0000000..142703a --- /dev/null +++ b/src/marlin.web/Migrations/20260905102259_AddSecretModel.Designer.cs @@ -0,0 +1,430 @@ +// <auto-generated /> +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using marlin.web.Data; + +#nullable disable + +namespace marlin.web.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260905102259_AddSecretModel")] + partial class AddSecretModel + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("marlin.core.models.Agent", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Capabilities") + .IsRequired() + .HasColumnType("text") + .HasColumnName("capabilities"); + + b.Property<DateTime>("LastSeen") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen"); + + b.Property<int>("MaxConcurrentJobs") + .HasColumnType("integer") + .HasColumnName("max_concurrent_jobs"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_agents"); + + b.ToTable("agents", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.Job", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid?>("AgentId") + .HasColumnType("uuid") + .HasColumnName("agent_id"); + + b.Property<string>("Commit") + .IsRequired() + .HasColumnType("text") + .HasColumnName("commit"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<string>("CreatedReason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_reason"); + + b.Property<string>("Recipe") + .IsRequired() + .HasColumnType("text") + .HasColumnName("recipe"); + + b.Property<string>("Ref") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ref"); + + b.Property<Guid>("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.Property<int>("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id") + .HasName("pk_jobs"); + + b.HasIndex("AgentId") + .HasDatabaseName("ix_jobs_agent_id"); + + b.HasIndex("SourceId") + .HasDatabaseName("ix_jobs_source_id"); + + b.ToTable("jobs", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.JobStatusUpdate", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("At") + .HasColumnType("timestamp with time zone") + .HasColumnName("at"); + + b.Property<Guid>("JobId") + .HasColumnType("uuid") + .HasColumnName("job_id"); + + b.Property<int>("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id") + .HasName("pk_job_status_update"); + + b.HasIndex("JobId") + .HasDatabaseName("ix_job_status_update_job_id"); + + b.ToTable("job_status_update", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlSource", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid?>("AccessSecretId") + .HasColumnType("uuid") + .HasColumnName("access_secret_id"); + + b.Property<string>("BranchFilter") + .IsRequired() + .HasColumnType("text") + .HasColumnName("branch_filter"); + + b.Property<string>("MainBranch") + .IsRequired() + .HasColumnType("text") + .HasColumnName("main_branch"); + + b.Property<int>("MinPollingInterval") + .HasColumnType("integer") + .HasColumnName("min_polling_interval"); + + b.Property<string>("UpstreamUrl") + .IsRequired() + .HasColumnType("text") + .HasColumnName("upstream_url"); + + b.HasKey("Id") + .HasName("pk_sources"); + + b.ToTable("sources", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledBlob", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid>("CommitId") + .HasColumnType("uuid") + .HasColumnName("commit_id"); + + b.Property<string>("Content") + .IsRequired() + .HasColumnType("text") + .HasColumnName("content"); + + b.Property<string>("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property<Guid>("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.HasKey("Id") + .HasName("pk_source_blobs"); + + b.HasIndex("CommitId") + .HasDatabaseName("ix_source_blobs_commit_id"); + + b.HasIndex("SourceId") + .HasDatabaseName("ix_source_blobs_source_id"); + + b.ToTable("source_blobs", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledBranch", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid>("CommitId") + .HasColumnType("uuid") + .HasColumnName("commit_id"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.HasKey("Id") + .HasName("pk_source_branches"); + + b.HasIndex("CommitId") + .HasDatabaseName("ix_source_branches_commit_id"); + + b.HasIndex("SourceId") + .HasDatabaseName("ix_source_branches_source_id"); + + b.ToTable("source_branches", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledCommit", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<bool>("BlobsFetched") + .HasColumnType("boolean") + .HasColumnName("blobs_fetched"); + + b.Property<string>("Hash") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hash"); + + b.Property<Guid>("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.HasKey("Id") + .HasName("pk_source_commits"); + + b.HasAlternateKey("SourceId", "Hash") + .HasName("ak_source_commits_source_id_hash"); + + b.ToTable("source_commits", (string)null); + }); + + modelBuilder.Entity("marlin.web.Models.VersionControlSecret", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Content") + .IsRequired() + .HasColumnType("text") + .HasColumnName("content"); + + b.Property<string>("Kind") + .HasColumnType("text") + .HasColumnName("kind"); + + b.Property<string>("Label") + .IsRequired() + .HasColumnType("text") + .HasColumnName("label"); + + b.Property<bool>("MayReveal") + .HasColumnType("boolean") + .HasColumnName("may_reveal"); + + b.Property<Guid>("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.HasKey("Id") + .HasName("pk_secrets"); + + b.HasIndex("SourceId") + .HasDatabaseName("ix_secrets_source_id"); + + b.ToTable("secrets", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.Job", b => + { + b.HasOne("marlin.core.models.Agent", "Agent") + .WithMany() + .HasForeignKey("AgentId") + .HasConstraintName("fk_jobs_agents_agent_id"); + + b.HasOne("marlin.core.models.VersionControlSource", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_jobs_sources_source_id"); + + b.Navigation("Agent"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("marlin.core.models.JobStatusUpdate", b => + { + b.HasOne("marlin.core.models.Job", "Job") + .WithMany("StatusUpdates") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_job_status_update_jobs_job_id"); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledBlob", b => + { + b.HasOne("marlin.core.models.VersionControlledCommit", "Commit") + .WithMany("Blobs") + .HasForeignKey("CommitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_source_blobs_source_commits_commit_id"); + + b.HasOne("marlin.core.models.VersionControlSource", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_source_blobs_sources_source_id"); + + b.Navigation("Commit"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledBranch", b => + { + b.HasOne("marlin.core.models.VersionControlledCommit", "Commit") + .WithMany("Branches") + .HasForeignKey("CommitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_source_branches_source_commits_commit_id"); + + b.HasOne("marlin.core.models.VersionControlSource", "Source") + .WithMany("Branches") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_source_branches_sources_source_id"); + + b.Navigation("Commit"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledCommit", b => + { + b.HasOne("marlin.core.models.VersionControlSource", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_source_commits_sources_source_id"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("marlin.web.Models.VersionControlSecret", b => + { + b.HasOne("marlin.core.models.VersionControlSource", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_secrets_sources_source_id"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("marlin.core.models.Job", b => + { + b.Navigation("StatusUpdates"); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlSource", b => + { + b.Navigation("Branches"); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledCommit", b => + { + b.Navigation("Blobs"); + + b.Navigation("Branches"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/marlin.web/Migrations/20260905102259_AddSecretModel.cs b/src/marlin.web/Migrations/20260905102259_AddSecretModel.cs new file mode 100644 index 0000000..8b09e89 --- /dev/null +++ b/src/marlin.web/Migrations/20260905102259_AddSecretModel.cs @@ -0,0 +1,59 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace marlin.web.Migrations +{ + /// <inheritdoc /> + public partial class AddSecretModel : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn<Guid>( + name: "access_secret_id", + table: "sources", + type: "uuid", + nullable: true); + + migrationBuilder.CreateTable( + name: "secrets", + columns: table => new + { + id = table.Column<Guid>(type: "uuid", nullable: false), + source_id = table.Column<Guid>(type: "uuid", nullable: false), + may_reveal = table.Column<bool>(type: "boolean", nullable: false), + kind = table.Column<string>(type: "text", nullable: true), + label = table.Column<string>(type: "text", nullable: false), + content = table.Column<string>(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_secrets", x => x.id); + table.ForeignKey( + name: "fk_secrets_sources_source_id", + column: x => x.source_id, + principalTable: "sources", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_secrets_source_id", + table: "secrets", + column: "source_id"); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "secrets"); + + migrationBuilder.DropColumn( + name: "access_secret_id", + table: "sources"); + } + } +} diff --git a/src/marlin.web/Migrations/20260905104308_BlobNowBinary.Designer.cs b/src/marlin.web/Migrations/20260905104308_BlobNowBinary.Designer.cs new file mode 100644 index 0000000..6061a9d --- /dev/null +++ b/src/marlin.web/Migrations/20260905104308_BlobNowBinary.Designer.cs @@ -0,0 +1,430 @@ +// <auto-generated /> +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using marlin.web.Data; + +#nullable disable + +namespace marlin.web.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260905104308_BlobNowBinary")] + partial class BlobNowBinary + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("marlin.core.models.Agent", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Capabilities") + .IsRequired() + .HasColumnType("text") + .HasColumnName("capabilities"); + + b.Property<DateTime>("LastSeen") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen"); + + b.Property<int>("MaxConcurrentJobs") + .HasColumnType("integer") + .HasColumnName("max_concurrent_jobs"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_agents"); + + b.ToTable("agents", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.Job", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid?>("AgentId") + .HasColumnType("uuid") + .HasColumnName("agent_id"); + + b.Property<string>("Commit") + .IsRequired() + .HasColumnType("text") + .HasColumnName("commit"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<string>("CreatedReason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_reason"); + + b.Property<string>("Recipe") + .IsRequired() + .HasColumnType("text") + .HasColumnName("recipe"); + + b.Property<string>("Ref") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ref"); + + b.Property<Guid>("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.Property<int>("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id") + .HasName("pk_jobs"); + + b.HasIndex("AgentId") + .HasDatabaseName("ix_jobs_agent_id"); + + b.HasIndex("SourceId") + .HasDatabaseName("ix_jobs_source_id"); + + b.ToTable("jobs", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.JobStatusUpdate", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("At") + .HasColumnType("timestamp with time zone") + .HasColumnName("at"); + + b.Property<Guid>("JobId") + .HasColumnType("uuid") + .HasColumnName("job_id"); + + b.Property<int>("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id") + .HasName("pk_job_status_update"); + + b.HasIndex("JobId") + .HasDatabaseName("ix_job_status_update_job_id"); + + b.ToTable("job_status_update", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlSource", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid?>("AccessSecretId") + .HasColumnType("uuid") + .HasColumnName("access_secret_id"); + + b.Property<string>("BranchFilter") + .IsRequired() + .HasColumnType("text") + .HasColumnName("branch_filter"); + + b.Property<string>("MainBranch") + .IsRequired() + .HasColumnType("text") + .HasColumnName("main_branch"); + + b.Property<int>("MinPollingInterval") + .HasColumnType("integer") + .HasColumnName("min_polling_interval"); + + b.Property<string>("UpstreamUrl") + .IsRequired() + .HasColumnType("text") + .HasColumnName("upstream_url"); + + b.HasKey("Id") + .HasName("pk_sources"); + + b.ToTable("sources", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledBlob", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid>("CommitId") + .HasColumnType("uuid") + .HasColumnName("commit_id"); + + b.Property<byte[]>("Content") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("content"); + + b.Property<string>("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property<Guid>("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.HasKey("Id") + .HasName("pk_source_blobs"); + + b.HasIndex("CommitId") + .HasDatabaseName("ix_source_blobs_commit_id"); + + b.HasIndex("SourceId") + .HasDatabaseName("ix_source_blobs_source_id"); + + b.ToTable("source_blobs", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledBranch", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid>("CommitId") + .HasColumnType("uuid") + .HasColumnName("commit_id"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.HasKey("Id") + .HasName("pk_source_branches"); + + b.HasIndex("CommitId") + .HasDatabaseName("ix_source_branches_commit_id"); + + b.HasIndex("SourceId") + .HasDatabaseName("ix_source_branches_source_id"); + + b.ToTable("source_branches", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledCommit", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<bool>("BlobsFetched") + .HasColumnType("boolean") + .HasColumnName("blobs_fetched"); + + b.Property<string>("Hash") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hash"); + + b.Property<Guid>("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.HasKey("Id") + .HasName("pk_source_commits"); + + b.HasAlternateKey("SourceId", "Hash") + .HasName("ak_source_commits_source_id_hash"); + + b.ToTable("source_commits", (string)null); + }); + + modelBuilder.Entity("marlin.web.Models.VersionControlSecret", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Content") + .IsRequired() + .HasColumnType("text") + .HasColumnName("content"); + + b.Property<string>("Kind") + .HasColumnType("text") + .HasColumnName("kind"); + + b.Property<string>("Label") + .IsRequired() + .HasColumnType("text") + .HasColumnName("label"); + + b.Property<bool>("MayReveal") + .HasColumnType("boolean") + .HasColumnName("may_reveal"); + + b.Property<Guid>("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.HasKey("Id") + .HasName("pk_secrets"); + + b.HasIndex("SourceId") + .HasDatabaseName("ix_secrets_source_id"); + + b.ToTable("secrets", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.Job", b => + { + b.HasOne("marlin.core.models.Agent", "Agent") + .WithMany() + .HasForeignKey("AgentId") + .HasConstraintName("fk_jobs_agents_agent_id"); + + b.HasOne("marlin.core.models.VersionControlSource", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_jobs_sources_source_id"); + + b.Navigation("Agent"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("marlin.core.models.JobStatusUpdate", b => + { + b.HasOne("marlin.core.models.Job", "Job") + .WithMany("StatusUpdates") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_job_status_update_jobs_job_id"); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledBlob", b => + { + b.HasOne("marlin.core.models.VersionControlledCommit", "Commit") + .WithMany("Blobs") + .HasForeignKey("CommitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_source_blobs_source_commits_commit_id"); + + b.HasOne("marlin.core.models.VersionControlSource", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_source_blobs_sources_source_id"); + + b.Navigation("Commit"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledBranch", b => + { + b.HasOne("marlin.core.models.VersionControlledCommit", "Commit") + .WithMany("Branches") + .HasForeignKey("CommitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_source_branches_source_commits_commit_id"); + + b.HasOne("marlin.core.models.VersionControlSource", "Source") + .WithMany("Branches") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_source_branches_sources_source_id"); + + b.Navigation("Commit"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledCommit", b => + { + b.HasOne("marlin.core.models.VersionControlSource", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_source_commits_sources_source_id"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("marlin.web.Models.VersionControlSecret", b => + { + b.HasOne("marlin.core.models.VersionControlSource", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_secrets_sources_source_id"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("marlin.core.models.Job", b => + { + b.Navigation("StatusUpdates"); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlSource", b => + { + b.Navigation("Branches"); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledCommit", b => + { + b.Navigation("Blobs"); + + b.Navigation("Branches"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/marlin.web/Migrations/20260905104308_BlobNowBinary.cs b/src/marlin.web/Migrations/20260905104308_BlobNowBinary.cs new file mode 100644 index 0000000..493e698 --- /dev/null +++ b/src/marlin.web/Migrations/20260905104308_BlobNowBinary.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace marlin.web.Migrations +{ + /// <inheritdoc /> + public partial class BlobNowBinary : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "content", + table: "source_blobs"); + + migrationBuilder.AddColumn<byte[]>( + name: "content", + table: "source_blobs", + type: "bytea", + nullable: false, + defaultValue: new byte[0]); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "content", + table: "source_blobs"); + + migrationBuilder.AddColumn<string>( + name: "content", + table: "source_blobs", + type: "text", + nullable: false, + defaultValue: ""); + } + } +} diff --git a/src/marlin.web/Migrations/ApplicationDbContextModelSnapshot.cs b/src/marlin.web/Migrations/ApplicationDbContextModelSnapshot.cs index 4c026e5..8de35de 100644 --- a/src/marlin.web/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/marlin.web/Migrations/ApplicationDbContextModelSnapshot.cs @@ -22,18 +22,145 @@ namespace marlin.web.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("marlin.core.VersionControlSource", b => + modelBuilder.Entity("marlin.core.models.Agent", b => { b.Property<Guid>("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid") .HasColumnName("id"); + b.Property<string>("Capabilities") + .IsRequired() + .HasColumnType("text") + .HasColumnName("capabilities"); + + b.Property<DateTime>("LastSeen") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen"); + + b.Property<int>("MaxConcurrentJobs") + .HasColumnType("integer") + .HasColumnName("max_concurrent_jobs"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_agents"); + + b.ToTable("agents", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.Job", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid?>("AgentId") + .HasColumnType("uuid") + .HasColumnName("agent_id"); + + b.Property<string>("Commit") + .IsRequired() + .HasColumnType("text") + .HasColumnName("commit"); + + b.Property<DateTime>("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property<string>("CreatedReason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("created_reason"); + + b.Property<string>("Recipe") + .IsRequired() + .HasColumnType("text") + .HasColumnName("recipe"); + + b.Property<string>("Ref") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ref"); + + b.Property<Guid>("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.Property<int>("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id") + .HasName("pk_jobs"); + + b.HasIndex("AgentId") + .HasDatabaseName("ix_jobs_agent_id"); + + b.HasIndex("SourceId") + .HasDatabaseName("ix_jobs_source_id"); + + b.ToTable("jobs", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.JobStatusUpdate", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<DateTime>("At") + .HasColumnType("timestamp with time zone") + .HasColumnName("at"); + + b.Property<Guid>("JobId") + .HasColumnType("uuid") + .HasColumnName("job_id"); + + b.Property<int>("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("Id") + .HasName("pk_job_status_update"); + + b.HasIndex("JobId") + .HasDatabaseName("ix_job_status_update_job_id"); + + b.ToTable("job_status_update", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlSource", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid?>("AccessSecretId") + .HasColumnType("uuid") + .HasColumnName("access_secret_id"); + b.Property<string>("BranchFilter") .IsRequired() .HasColumnType("text") .HasColumnName("branch_filter"); + b.Property<string>("MainBranch") + .IsRequired() + .HasColumnType("text") + .HasColumnName("main_branch"); + + b.Property<int>("MinPollingInterval") + .HasColumnType("integer") + .HasColumnName("min_polling_interval"); + b.Property<string>("UpstreamUrl") .IsRequired() .HasColumnType("text") @@ -44,6 +171,256 @@ namespace marlin.web.Migrations b.ToTable("sources", (string)null); }); + + modelBuilder.Entity("marlin.core.models.VersionControlledBlob", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid>("CommitId") + .HasColumnType("uuid") + .HasColumnName("commit_id"); + + b.Property<byte[]>("Content") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("content"); + + b.Property<string>("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property<Guid>("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.HasKey("Id") + .HasName("pk_source_blobs"); + + b.HasIndex("CommitId") + .HasDatabaseName("ix_source_blobs_commit_id"); + + b.HasIndex("SourceId") + .HasDatabaseName("ix_source_blobs_source_id"); + + b.ToTable("source_blobs", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledBranch", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<Guid>("CommitId") + .HasColumnType("uuid") + .HasColumnName("commit_id"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property<Guid>("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.HasKey("Id") + .HasName("pk_source_branches"); + + b.HasIndex("CommitId") + .HasDatabaseName("ix_source_branches_commit_id"); + + b.HasIndex("SourceId") + .HasDatabaseName("ix_source_branches_source_id"); + + b.ToTable("source_branches", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledCommit", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<bool>("BlobsFetched") + .HasColumnType("boolean") + .HasColumnName("blobs_fetched"); + + b.Property<string>("Hash") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hash"); + + b.Property<Guid>("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.HasKey("Id") + .HasName("pk_source_commits"); + + b.HasAlternateKey("SourceId", "Hash") + .HasName("ak_source_commits_source_id_hash"); + + b.ToTable("source_commits", (string)null); + }); + + modelBuilder.Entity("marlin.web.Models.VersionControlSecret", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property<string>("Content") + .IsRequired() + .HasColumnType("text") + .HasColumnName("content"); + + b.Property<string>("Kind") + .HasColumnType("text") + .HasColumnName("kind"); + + b.Property<string>("Label") + .IsRequired() + .HasColumnType("text") + .HasColumnName("label"); + + b.Property<bool>("MayReveal") + .HasColumnType("boolean") + .HasColumnName("may_reveal"); + + b.Property<Guid>("SourceId") + .HasColumnType("uuid") + .HasColumnName("source_id"); + + b.HasKey("Id") + .HasName("pk_secrets"); + + b.HasIndex("SourceId") + .HasDatabaseName("ix_secrets_source_id"); + + b.ToTable("secrets", (string)null); + }); + + modelBuilder.Entity("marlin.core.models.Job", b => + { + b.HasOne("marlin.core.models.Agent", "Agent") + .WithMany() + .HasForeignKey("AgentId") + .HasConstraintName("fk_jobs_agents_agent_id"); + + b.HasOne("marlin.core.models.VersionControlSource", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_jobs_sources_source_id"); + + b.Navigation("Agent"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("marlin.core.models.JobStatusUpdate", b => + { + b.HasOne("marlin.core.models.Job", "Job") + .WithMany("StatusUpdates") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_job_status_update_jobs_job_id"); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledBlob", b => + { + b.HasOne("marlin.core.models.VersionControlledCommit", "Commit") + .WithMany("Blobs") + .HasForeignKey("CommitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_source_blobs_source_commits_commit_id"); + + b.HasOne("marlin.core.models.VersionControlSource", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_source_blobs_sources_source_id"); + + b.Navigation("Commit"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledBranch", b => + { + b.HasOne("marlin.core.models.VersionControlledCommit", "Commit") + .WithMany("Branches") + .HasForeignKey("CommitId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_source_branches_source_commits_commit_id"); + + b.HasOne("marlin.core.models.VersionControlSource", "Source") + .WithMany("Branches") + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_source_branches_sources_source_id"); + + b.Navigation("Commit"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledCommit", b => + { + b.HasOne("marlin.core.models.VersionControlSource", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_source_commits_sources_source_id"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("marlin.web.Models.VersionControlSecret", b => + { + b.HasOne("marlin.core.models.VersionControlSource", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_secrets_sources_source_id"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("marlin.core.models.Job", b => + { + b.Navigation("StatusUpdates"); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlSource", b => + { + b.Navigation("Branches"); + }); + + modelBuilder.Entity("marlin.core.models.VersionControlledCommit", b => + { + b.Navigation("Blobs"); + + b.Navigation("Branches"); + }); #pragma warning restore 612, 618 } } diff --git a/src/marlin.web/Models/DatabaseSourceSecretStore.cs b/src/marlin.web/Models/DatabaseSourceSecretStore.cs new file mode 100644 index 0000000..b4ca61f --- /dev/null +++ b/src/marlin.web/Models/DatabaseSourceSecretStore.cs @@ -0,0 +1,27 @@ +namespace marlin.web.Models; + +using core.models; +using Data; +using Microsoft.EntityFrameworkCore; + +public class DatabaseSourceSecretStore(ApplicationDbContext dbContext) : ISourceSecretStore +{ + public async Task<ISecret?> GetAsync(Guid id, CancellationToken ct) + { + return await dbContext.Secrets + .FirstOrDefaultAsync(x => x.Id == id, cancellationToken: ct); + } +} + +public class VersionControlSecret : ISecret +{ + public Guid Id { get; set; } + + public Guid SourceId { get; set; } + public VersionControlSource Source { get; set; } + + public bool MayReveal { get; set; } + public string? Kind { get; set; } + public string Label { get; set; } + public Dictionary<string, string> Content { get; set; } +}
\ No newline at end of file diff --git a/src/marlin.web/Program.cs b/src/marlin.web/Program.cs index cf330b2..f079a3c 100644 --- a/src/marlin.web/Program.cs +++ b/src/marlin.web/Program.cs @@ -1,4 +1,5 @@ using marlin.web.Data; +using marlin.web.Models; using marlin.web.Services; using Microsoft.EntityFrameworkCore; @@ -10,8 +11,13 @@ builder.Services.AddDbContext<ApplicationDbContext>(options => options .UseNpgsql(builder.Configuration.GetConnectionString("Master")) .UseSnakeCaseNamingConvention()); +builder.Services.AddScoped<ISourceSecretStore, DatabaseSourceSecretStore>(); + builder.Services.AddHostedService<DatabaseUpgrader>(); -builder.Services.AddHostedService<VersionControlSyncService>(); + +builder.Services.AddSingleton<VersionControlSyncService>(); +builder.Services.AddHostedService<VersionControlSyncService>(sp => sp + .GetRequiredService<VersionControlSyncService>()); // Add services to the container. builder.Services.AddControllersWithViews(); diff --git a/src/marlin.web/Services/VersionControlSyncService.cs b/src/marlin.web/Services/VersionControlSyncService.cs index ba40662..bf189b0 100644 --- a/src/marlin.web/Services/VersionControlSyncService.cs +++ b/src/marlin.web/Services/VersionControlSyncService.cs @@ -4,8 +4,10 @@ using System.Diagnostics; using System.Text; using core.functional; using core.models; +using core.util; using Data; using Microsoft.EntityFrameworkCore; +using Models; public class VersionControlSyncService : BackgroundService { @@ -13,38 +15,213 @@ public class VersionControlSyncService : BackgroundService private readonly IServiceProvider _services; private ICollection<VersionControlSource> _sources; - private Dictionary<Guid, DateTime> _nextPoll; + private Dictionary<Guid, DateTime> _lastPoll; public VersionControlSyncService(ILogger<VersionControlSyncService> logger, IServiceProvider services) { this._logger = logger; this._services = services; this._sources = new List<VersionControlSource>(); - this._nextPoll = new(); + this._lastPoll = new(); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await this.FetchSources(stoppingToken); + + while (!stoppingToken.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); + + try + { + await Tick(stoppingToken); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + this._logger.LogError(ex, "VCS sync tick failed"); + } + } } - public async Task FetchSources() + private async Task Tick(CancellationToken ct) { - await using var scoped = this._services.CreateAsyncScope(); - var dbContext = scoped.ServiceProvider.GetRequiredService<ApplicationDbContext>(); - this._sources = await dbContext.Sources.ToListAsync(); + var now = DateTime.UtcNow; + var due = this + ._sources + .Where(x => this._lastPoll.TryGetValue(x.Id, out var lastTime) ? now >= lastTime.AddSeconds(x.MinPollingInterval) : true) + .ToArray(); + + if (due.Length > 0) + { + await using var scoped = this._services.CreateAsyncScope(); + var dbContext = scoped.ServiceProvider.GetRequiredService<ApplicationDbContext>(); + var sourceSecretStore = scoped.ServiceProvider.GetRequiredService<ISourceSecretStore>(); + + foreach (var source in due) + { + var trackedSource = await dbContext.Sources + .Include(x => x.Branches) + .ThenInclude(x => x.Commit) + .FirstAsync(x => x.Id == source.Id, ct); + + try + { + await this.SyncRefs(dbContext, sourceSecretStore, trackedSource, ct); + await this.SyncBlobs(dbContext, sourceSecretStore, trackedSource, ct); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + this._logger.LogError(ex, "Failed to sync vcs source '{name}'", source.UpstreamUrl); + } + finally + { + this._lastPoll[source.Id] = DateTime.UtcNow; + } + } + + if (dbContext.ChangeTracker.HasChanges()) + { + await this.Sweep(dbContext, ct); + + await dbContext.SaveChangesAsync(ct); + this._logger.LogInformation("Synced {count} vcs roots", due.Length); + } + } } - private async Task SyncSource(VersionControlSource source, ApplicationDbContext db) + private async Task<IGitClient> GetClient(VersionControlSource source, ISourceSecretStore store, CancellationToken ct) + { + IGitCredential? cred = null; + if (source.AccessSecretId != null) + { + ISecret? secret = await store.GetAsync(source.AccessSecretId.Value, ct); + if (secret == null) + throw new InvalidOperationException($"Could not find access secret for '{source.UpstreamUrl}'"); + + if (secret.Kind == SecretKind.SSHKey && secret.Has("public_key", "private_key")) + cred = new SshKeyGitCredential(secret.Content["private_key"]); + + else if (secret.Kind == SecretKind.HTTPToken && secret.Has("identity", "token")) + cred = new TokenGitCredential(secret.Content["token"], secret.Content["identity"]); + + else + throw new InvalidOperationException($"Could not resolve access secret content to credentials for '{source.UpstreamUrl}'"); + } + + IGitClient client = new GitBinaryClient(cred); + return client; + } + + private async Task<string> GetLocalPath(IGitClient client, VersionControlSource source) + { + var dir = Path.Join(Environment.CurrentDirectory, "run", "sources", source.Id.ToString("N")); + if (!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + await client.Init(dir); + } + + return dir; + } + + public async Task FetchSources(CancellationToken ct) { - IGitClient client = new GitBinaryClient(); - var refs = await client.GetRemoteRefs(source.UpstreamUrl); + await using var scoped = this._services.CreateAsyncScope(); + var dbContext = scoped.ServiceProvider.GetRequiredService<ApplicationDbContext>(); + this._sources = await dbContext.Sources + .Include(x => x.Branches) + .ToListAsync(ct); + await this.Sweep(dbContext, ct); } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + private async Task Sweep(ApplicationDbContext dbContext, CancellationToken ct) { - await this.FetchSources(); + await dbContext.SourceCommits + .Include(x => x.Branches) + .Where(x => !x.Branches.Any()) + .ExecuteDeleteAsync(ct); + } + + private async Task SyncRefs(ApplicationDbContext dbContext, ISourceSecretStore secretStore, VersionControlSource source, CancellationToken ct) + { + IGitClient client = await this.GetClient(source, secretStore, ct); + var newRefs = await client.GetRemoteRefs(source.UpstreamUrl); + + Dictionary<string, string> currentRefs = source.Branches + .ToDictionary(x => x.Name, x => x.Commit.Hash); + Dictionary<string, VersionControlledBranch> branchesByName = source.Branches + .ToDictionary(x => x.Name); + + var added = newRefs.Keys.Except(currentRefs.Keys).ToArray(); + var removed = currentRefs.Keys.Except(newRefs.Keys).ToArray(); + var changed = currentRefs.Keys.Intersect(newRefs.Keys) + .Where(name => currentRefs[name] != newRefs[name]) + .ToArray(); + + foreach (var name in removed) + source.Branches.Remove(branchesByName[name]); + + // Pre-load the commits these refs now point to, so a hash shared across + // multiple branches (or seen in an earlier sync) resolves to one row + // instead of racing duplicate inserts. + var neededHashes = added.Concat(changed) + .Select(name => newRefs[name]) + .Distinct() + .ToHashSet(); + + var commitsByHash = await dbContext.SourceCommits + .Where(c => c.SourceId == source.Id && neededHashes.Contains(c.Hash)) + .ToDictionaryAsync(c => c.Hash, ct); + + VersionControlledCommit GetOrCreateCommit(string hash) + { + if (!commitsByHash.TryGetValue(hash, out var commit)) + { + commit = new VersionControlledCommit { SourceId = source.Id, Hash = hash, BlobsFetched = false }; + commitsByHash[hash] = commit; + } + + return commit; + } + + foreach (var name in added) + source.Branches.Add(new VersionControlledBranch { Name = name, Commit = GetOrCreateCommit(newRefs[name]) }); + + foreach (var name in changed) + branchesByName[name].Commit = GetOrCreateCommit(newRefs[name]); + } + + private async Task SyncBlobs(ApplicationDbContext dbContext, ISourceSecretStore secretStore, VersionControlSource source, CancellationToken ct) + { + var refs = source.Branches + .Where(x => !x.Commit.BlobsFetched) + .DistinctBy(x => x.CommitId) + .ToArray(); + + IGitClient client = await this.GetClient(source, secretStore, ct); + var localPath = await GetLocalPath(client, source); + await client.FetchTrees(localPath, source.UpstreamUrl, refs.Select(x => x.Name)); - while (!stoppingToken.IsCancellationRequested) + foreach (VersionControlledCommit commit in refs.Select(x => x.Commit)) { + var blobNames = await client.ListTree(localPath, commit.Hash, "."); + + foreach (string blobName in blobNames) + { + var content = await client.CatFile(localPath, commit.Hash, blobName); + dbContext.SourceBlobs.Add(new VersionControlledBlob + { + Source = source, + Commit = commit, + Path = blobName, + Content = content + }); + } + commit.BlobsFetched = true; } } }
\ No newline at end of file diff --git a/src/marlin.web/Views/Jobs/Details.cshtml b/src/marlin.web/Views/Jobs/Details.cshtml new file mode 100644 index 0000000..e1fd184 --- /dev/null +++ b/src/marlin.web/Views/Jobs/Details.cshtml @@ -0,0 +1,5 @@ +@model marlin.core.models.Job + +@Model.Id + +<a asp-action="List">Back</a>
\ No newline at end of file diff --git a/src/marlin.web/Views/Jobs/List.cshtml b/src/marlin.web/Views/Jobs/List.cshtml index 2c10e46..6ae1d96 100644 --- a/src/marlin.web/Views/Jobs/List.cshtml +++ b/src/marlin.web/Views/Jobs/List.cshtml @@ -1,5 +1,7 @@ @model ICollection<marlin.core.models.Job> +<a asp-action="Spawn">Spawn new...</a> + <table> <thead> <tr> @@ -10,6 +12,7 @@ <th>vcs</th> <th>branch</th> <th>agent</th> + <th>created</th> </tr> </thead> <tbody> @@ -23,6 +26,7 @@ <td>@source.Source.UpstreamUrl</td> <td>@source.Ref</td> <td>@(source.Agent?.Name ?? "no agent assigned")</td> + <td>@(source.CreatedReason)</td> </tr> } </tbody> diff --git a/src/marlin.web/Views/Jobs/Spawn.cshtml b/src/marlin.web/Views/Jobs/Spawn.cshtml new file mode 100644 index 0000000..8f7ee43 --- /dev/null +++ b/src/marlin.web/Views/Jobs/Spawn.cshtml @@ -0,0 +1,56 @@ +@using System.Linq +@using System.Text.Json +@using marlin.web.Controllers +@model marlin.web.Controllers.JobsController.JobSpawnDetails + +@{ + var sourcesJson = JsonSerializer.Serialize(Model.Sources.Select(s => new + { + id = s.Id, + label = s.UpstreamUrl, + mainBranch = s.MainBranch, + branches = s.Branches.Select(b => new { name = b.Name, commit = b.Commit.Hash }) + })); +} + +<form method="post" asp-action="Spawn"> + VCS: <select id="vcs" name="@nameof(JobsController.JobSpawnRequest.Vcs)"></select><br/> + Ref: <select id="ref" name="@nameof(JobsController.JobSpawnRequest.Ref)"></select><br/> + Commit: <input type="text" id="commit" name="@nameof(JobsController.JobSpawnRequest.Commit)" readonly/><br/> + Recipe: <input type="text" name="@nameof(JobsController.JobSpawnRequest.Recipe)" value="@Model.Recipe"/><br/> + <button type="submit">Save</button> +</form> + +<script> + const sources = @Html.Raw(sourcesJson); + const vcsSelect = document.getElementById('vcs'); + const refSelect = document.getElementById('ref'); + const commitInput = document.getElementById('commit'); + + for (const s of sources) { + vcsSelect.add(new Option(s.label, s.id)); + } + + function currentSource() { + return sources.find(s => s.id === vcsSelect.value); + } + + function populateRefs() { + refSelect.innerHTML = ''; + for (const b of currentSource()?.branches ?? []) { + refSelect.add(new Option(b.name, b.name)); + } + refSelect.value = currentSource()?.mainBranch ?? ''; + populateCommit(); + } + + function populateCommit() { + const branch = currentSource()?.branches.find(b => b.name === refSelect.value); + commitInput.value = branch?.commit ?? ''; + } + + vcsSelect.addEventListener('change', populateRefs); + refSelect.addEventListener('change', populateCommit); + + populateRefs(); +</script> diff --git a/src/marlin.web/Views/Shared/_Layout.cshtml b/src/marlin.web/Views/Shared/_Layout.cshtml index f060fcf..e8be2af 100644 --- a/src/marlin.web/Views/Shared/_Layout.cshtml +++ b/src/marlin.web/Views/Shared/_Layout.cshtml @@ -24,7 +24,10 @@ <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a> </li> <li class="nav-item"> - <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a> + <a class="nav-link text-dark" asp-area="" asp-controller="VCS" asp-action="List">VCS Configuration</a> + </li> + <li class="nav-item"> + <a class="nav-link text-dark" asp-area="" asp-controller="Jobs" asp-action="List">Jobs</a> </li> </ul> </div> diff --git a/src/marlin.web/Views/VCS/Create.cshtml b/src/marlin.web/Views/VCS/Create.cshtml index adaa913..e04592d 100644 --- a/src/marlin.web/Views/VCS/Create.cshtml +++ b/src/marlin.web/Views/VCS/Create.cshtml @@ -2,10 +2,10 @@ @using marlin.core.models <form method="post" asp-action="Create"> - URL: <input type="text" name="@nameof(VersionControlSource.UpstreamUrl)" /> - Filter: <input type="text" name="@nameof(VersionControlSource.BranchFilter)" /> - Main Branch: <input type="text" name="@nameof(VersionControlSource.MainBranch)" /> - Polling Interval: <input type="number" name="@nameof(VersionControlSource.MinPollingInterval)" /> + URL: <input type="text" name="@nameof(VersionControlSource.UpstreamUrl)" /><br/> + Filter: <input type="text" name="@nameof(VersionControlSource.BranchFilter)" /><br/> + Main Branch: <input type="text" name="@nameof(VersionControlSource.MainBranch)" /><br/> + Polling Interval: <input type="number" name="@nameof(VersionControlSource.MinPollingInterval)" /><br/> <button type="submit">Create</button> </form> diff --git a/src/marlin.web/Views/VCS/CreateSecret.cshtml b/src/marlin.web/Views/VCS/CreateSecret.cshtml new file mode 100644 index 0000000..6453056 --- /dev/null +++ b/src/marlin.web/Views/VCS/CreateSecret.cshtml @@ -0,0 +1,12 @@ +@using marlin.core.models +@model VersionControlSource + +<form method="post" asp-action="CreateSecret" asp-route-sourceId="@Model.Id"> + Label: <input type="text" name="Label"/><br/> + Kind: <input type="text" name="Kind"/><br/> + May Reveal: <input type="checkbox" name="MayReveal" value="true"/><br/> + Content (JSON): <textarea name="ContentJson">{}</textarea><br/> + <button type="submit">Create</button> +</form> + +<a asp-action="ListSecret" asp-route-sourceId="@Model.Id">Back</a> diff --git a/src/marlin.web/Views/VCS/Details.cshtml b/src/marlin.web/Views/VCS/Details.cshtml index 8d25472..45c1a8f 100644 --- a/src/marlin.web/Views/VCS/Details.cshtml +++ b/src/marlin.web/Views/VCS/Details.cshtml @@ -1,13 +1,43 @@ @using marlin.core @using marlin.core.models -@model marlin.core.models.VersionControlSource +@using marlin.web.Controllers +@model (VersionControlSource Source, List<VCSController.SecretOption> SecretOptions) -<form method="post" asp-route-id="@Model.Id" asp-action="Edit"> - URL: <input type="text" name="@nameof(VersionControlSource.UpstreamUrl)" value="@Model.UpstreamUrl"/> - Filter: <input type="text" name="@nameof(VersionControlSource.BranchFilter)" value="@Model.BranchFilter"/> - Main Branch: <input type="text" name="@nameof(VersionControlSource.MainBranch)" value="@Model.MainBranch"/> - Polling Interval: <input type="number" name="@nameof(VersionControlSource.MinPollingInterval)" value="@Model.MinPollingInterval"/> +<form method="post" asp-route-id="@Model.Source.Id" asp-action="Edit"> + URL: <input type="text" name="@nameof(VersionControlSource.UpstreamUrl)" value="@Model.Source.UpstreamUrl"/><br/> + Filter: <input type="text" name="@nameof(VersionControlSource.BranchFilter)" value="@Model.Source.BranchFilter"/><br/> + Main Branch: <input type="text" name="@nameof(VersionControlSource.MainBranch)" value="@Model.Source.MainBranch"/><br/> + Polling Interval: <input type="number" name="@nameof(VersionControlSource.MinPollingInterval)" value="@Model.Source.MinPollingInterval"/><br/> + Access Secret: + <select name="@nameof(VersionControlSource.AccessSecretId)"> + <option value="">(none)</option> + @foreach (var secret in Model.SecretOptions) + { + <option value="@secret.Id" selected="@(secret.Id == Model.Source.AccessSecretId)">@secret.Label</option> + } + </select><br/> <button type="submit">Save</button> </form> -<a asp-action="List">Back</a>
\ No newline at end of file +<table> + <thead> + <tr> + <th></th> + <th>branch</th> + <th>commit</th> + </tr> + </thead> + <tbody> + @foreach (var branch in Model.Source.Branches) + { + <tr> + <td>@(branch.Name == Model.Source.MainBranch ? "@" : "")</td> + <td>@branch.Name</td> + <td>@branch.Commit.Hash</td> + </tr> + } + </tbody> +</table> + +<a asp-action="ListSecret" asp-route-sourceId="@Model.Source.Id">Secrets</a> +<a asp-action="List">Back</a> diff --git a/src/marlin.web/Views/VCS/List.cshtml b/src/marlin.web/Views/VCS/List.cshtml index 90c3f48..4c248e4 100644 --- a/src/marlin.web/Views/VCS/List.cshtml +++ b/src/marlin.web/Views/VCS/List.cshtml @@ -1,4 +1,4 @@ -@model IReadOnlyList<marlin.core.models.VersionControlSource> +@model ICollection<marlin.core.models.VersionControlSource> <a asp-action="Create">New</a> diff --git a/src/marlin.web/Views/VCS/ListSecret.cshtml b/src/marlin.web/Views/VCS/ListSecret.cshtml new file mode 100644 index 0000000..aa5f15c --- /dev/null +++ b/src/marlin.web/Views/VCS/ListSecret.cshtml @@ -0,0 +1,45 @@ +@model (marlin.core.models.VersionControlSource Source, List<ISecret> Secrets) + +<a asp-action="CreateSecret" asp-route-sourceId="@Model.Source.Id">New</a> + +<table> + <thead> + <tr> + <th>id</th> + <th>label</th> + <th>kind</th> + <th>value</th> + <th>actions</th> + </tr> + </thead> + <tbody> + @foreach (var secret in Model.Secrets) + { + <tr> + <td>@secret.Id</td> + <td>@secret.Label</td> + <td>@secret.Kind</td> + <td> + @if (secret.MayReveal) + { + @foreach (var pair in secret.Content) + { + <span>@pair.Key = @pair.Value</span> + } + } + else + { + <span>secret cannot be revealed</span> + } + </td> + <td> + <form asp-action="DeleteSecret" method="post" asp-route-sourceId="@Model.Source.Id" asp-route-id="@secret.Id"> + <button type="submit">delete</button> + </form> + </td> + </tr> + } + </tbody> +</table> + +<a asp-action="Details", asp-route-id="@Model.Source.Id">back</a> diff --git a/src/marlin.web/appsettings.json b/src/marlin.web/appsettings.json index 10f68b8..793d7e4 100644 --- a/src/marlin.web/appsettings.json +++ b/src/marlin.web/appsettings.json @@ -2,7 +2,8 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.AspNetCore": "Warning" + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore.Database.Command": "Warning" } }, "AllowedHosts": "*" diff --git a/src/marlin.web/marlin.web.csproj b/src/marlin.web/marlin.web.csproj index 3160c98..26c8ea3 100644 --- a/src/marlin.web/marlin.web.csproj +++ b/src/marlin.web/marlin.web.csproj @@ -21,4 +21,8 @@ <ProjectReference Include="..\marlin.core\marlin.core.csproj" /> </ItemGroup> + <ItemGroup> + <Folder Include="Migrations\" /> + </ItemGroup> + </Project> diff --git a/src/marlin.web/wwwroot/css/site.css b/src/marlin.web/wwwroot/css/site.css index 819f612..c6ecfb7 100644 --- a/src/marlin.web/wwwroot/css/site.css +++ b/src/marlin.web/wwwroot/css/site.css @@ -28,4 +28,24 @@ body { .form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder { text-align: start; +} + +table { + border-collapse: collapse; + width: 100%; +} + +th, td { + border: 1px solid #ccc; + text-align: left; +} + +th { + background-color: #f0f0f0; + font-weight: bold; +} + +th a { + text-decoration: none; + color: inherit; }
\ No newline at end of file |
