diff options
| author | Melodi <62381002+Melodi17@users.noreply.github.com> | 2026-09-13 13:22:20 +1000 |
|---|---|---|
| committer | Melodi <62381002+Melodi17@users.noreply.github.com> | 2026-09-13 13:22:20 +1000 |
| commit | df0737e55afeb5c01eb39909605970d1d3a73ffc (patch) | |
| tree | 09823eaa310c60eefd2420dc305acae45c81900f /src/marlin.web/Controllers | |
| parent | ee69ce25b2fba871920260028bb10c2d6b62b26f (diff) | |
refactor: cleanup; feat: connection to agent
Diffstat (limited to 'src/marlin.web/Controllers')
| -rw-r--r-- | src/marlin.web/Controllers/AgentsController.cs | 89 | ||||
| -rw-r--r-- | src/marlin.web/Controllers/DispatchController.cs | 65 | ||||
| -rw-r--r-- | src/marlin.web/Controllers/JobsController.cs | 35 | ||||
| -rw-r--r-- | src/marlin.web/Controllers/VCSController.cs | 36 |
4 files changed, 210 insertions, 15 deletions
diff --git a/src/marlin.web/Controllers/AgentsController.cs b/src/marlin.web/Controllers/AgentsController.cs new file mode 100644 index 0000000..199acf9 --- /dev/null +++ b/src/marlin.web/Controllers/AgentsController.cs @@ -0,0 +1,89 @@ +namespace marlin.web.Controllers; + +using core.models; +using core.util; +using Data; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Models; + +[Route("agents")] +public class AgentsController(ApplicationDbContext dbContext) : Controller +{ + [HttpGet("list")] + public async Task<IActionResult> List(CancellationToken ct) + { + var items = await dbContext + .Agents + .Include(x => x.Jobs.Where(j => j.Status == JobStatus.Active)) + .ToListAsync(ct); + + return this.View(items); + } + + [HttpGet("details/{id}")] + public async Task<IActionResult> Details(Guid id, CancellationToken ct) + { + var item = await dbContext.Agents + .Include(x => x.Jobs.Where(j => j.Status == JobStatus.Active)) + .FirstOrDefaultAsync(x => x.Id == id, ct); + + if (item == null) + return this.NotFound(); + + return this.View(item); + } + + [HttpPost("edit/{id}")] + public async Task<IActionResult> Edit(Guid id, Agent agent, CancellationToken ct) + { + var item = await dbContext.Agents + .FirstOrDefaultAsync(x => x.Id == id, ct); + + if (item == null) + return this.NotFound(); + + item.Name = agent.Name; + item.MaxConcurrentJobs = agent.MaxConcurrentJobs; + + await dbContext.SaveChangesAsync(ct); + + return this.RedirectToAction("Details", new { id }); + } + + [HttpPost("details/{id}/regenerate-token")] + public async Task<IActionResult> RegenerateToken(Guid id, CancellationToken ct) + { + var item = await dbContext.Agents + .FirstOrDefaultAsync(x => x.Id == id, ct); + + if (item == null) + return this.NotFound(); + + item.AuthToken = SecureTokenGenerator.CreateAgentToken(); + + await dbContext.SaveChangesAsync(ct); + + return this.RedirectToAction("Details", new { id }); + } + + [HttpGet("create")] + public IActionResult Create() + { + return this.View(); + } + + [HttpPost("create")] + public async Task<IActionResult> CreatePost(Agent agent, CancellationToken ct) + { + agent.Id = Guid.Empty; + agent.AuthToken = SecureTokenGenerator.CreateAgentToken(); + agent.LastSeen = DateTime.UtcNow; + agent.Capabilities = new CapabilitySet(); + + await dbContext.Agents.AddAsync(agent, ct); + await dbContext.SaveChangesAsync(ct); + + return this.RedirectToAction("Details", new { id = agent.Id }); + } +}
\ No newline at end of file diff --git a/src/marlin.web/Controllers/DispatchController.cs b/src/marlin.web/Controllers/DispatchController.cs index 6b2285c..a37d5fa 100644 --- a/src/marlin.web/Controllers/DispatchController.cs +++ b/src/marlin.web/Controllers/DispatchController.cs @@ -1,10 +1,71 @@ namespace marlin.web.Controllers; +using core.models; +using core.models.meta; +using core.util; +using Data; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Models; +using Services; +using Util; [Route("api/dispatch")] [ApiController] -public class DispatchController : ControllerBase +[Authorize(Policy = "Agent")] +public class DispatchController(ApplicationDbContext dbContext) : ControllerBase { + [HttpPost("handshake")] + public async Task<ActionResult<AgentConfiguration>> Handshake(AgentStatus status, CancellationToken ct) + { + var agentId = User.RequireAgentId(); + + var agent = await dbContext.Agents + .FirstAsync(x => x.Id == agentId, ct); + + foreach (var capability in status.CapabilitySet.Items) + agent.Capabilities.Items[capability.Key] = capability.Value; + + await dbContext.SaveChangesAsync(ct); + + return new AgentConfiguration( + Name: agent.Name, + MaxConcurrentJobs: agent.MaxConcurrentJobs + ); + } -}
\ No newline at end of file + [HttpGet("queue")] + public async Task<ActionResult<DispatchJob?>> Queue(CancellationToken ct) + { + var agentId = User.RequireAgentId(); + + var nextJob = await dbContext + .Jobs + .Include(x => x.Source) + .Include(x => x.Branch) + .Include(x => x.Commit) + .Where(x => x.Status == JobStatus.Pending && x.AgentId == null) + .OrderBy(x => x.CreatedAt) + .FirstOrDefaultAsync(ct); + + if (nextJob == null) + return (DispatchJob?) null; + + string claimToken = SecureTokenGenerator.CreateClaimToken(); + nextJob.ClaimToken = claimToken; + nextJob.AgentId = agentId; + + await dbContext.SaveChangesAsync(ct); + + return new DispatchJob( + Id: nextJob.Id, + Token: claimToken, + Vcs: nextJob.Source.UpstreamUrl, + Ref: nextJob.Branch.Name, + Commit: nextJob.Commit.Hash, + Recipe: nextJob.Recipe + ); + } +} + diff --git a/src/marlin.web/Controllers/JobsController.cs b/src/marlin.web/Controllers/JobsController.cs index c86cb2d..552b56c 100644 --- a/src/marlin.web/Controllers/JobsController.cs +++ b/src/marlin.web/Controllers/JobsController.cs @@ -4,25 +4,29 @@ using core.models; using Data; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Models; using Util; [Route("jobs")] public class JobsController(ApplicationDbContext dbContext) : Controller { - // list jobs (pending, active, cancelled, success, failed), filter to vcs, filter to recipe - public record JobListFilter(JobStatus? Status = null, Guid? Vcs = null, string? Ref = null, string? Recipe = null); - + public record JobListFilter(JobStatus? Status = null, Guid? Vcs = null, Guid? Branch = null, string? Recipe = null, Guid? Agent = null); + [HttpGet("list")] public async Task<IActionResult> List([FromQuery] JobListFilter filter, CancellationToken ct) { - var results = await dbContext.Jobs + var results = await dbContext + .Jobs .WhereIf(filter.Status != null, x => x.Status == filter.Status) .WhereIf(filter.Vcs != null, x => x.SourceId == filter.Vcs) - .WhereIf(filter.Ref != null, x => x.Ref == filter.Ref) + .WhereIf(filter.Branch != null, x => x.BranchId == filter.Branch) .WhereIf(filter.Recipe != null, x => x.Recipe == filter.Recipe) + .WhereIf(filter.Agent != null, x => x.AgentId == filter.Agent) .OrderByDescending(x => x.CreatedAt) - + + .Include(x => x.Commit) + .Include(x => x.Branch) .Include(x => x.Source) .Include(x => x.Agent) .ToListAsync(ct); @@ -40,25 +44,34 @@ public class JobsController(ApplicationDbContext dbContext) : Controller [HttpGet("spawn")] public async Task<IActionResult> Spawn(CancellationToken ct) { - var sources = await dbContext.Sources + var sources = await dbContext + .Sources .Include(x => x.Branches) .ThenInclude(x => x.Commit) + .ThenInclude(x => x.WorkflowSnapshot) .ToListAsync(ct); var details = new JobSpawnDetails(sources); return this.View(details); } - public record JobSpawnRequest(Guid Vcs, string Ref, string Commit, string Recipe); + public record JobSpawnRequest(Guid Vcs, Guid Branch, string Recipe); [HttpPost("spawn")] public async Task<IActionResult> SpawnPost(JobSpawnRequest request, CancellationToken ct) { + var branch = await dbContext + .SourceBranches + .FirstOrDefaultAsync(x => x.Id == request.Branch, ct); + + if (branch == null) + return this.NotFound(); + Job job = new() { - SourceId = request.Vcs, - Ref = request.Ref, - Commit = request.Commit, + SourceId = branch.SourceId, + BranchId = branch.Id, + CommitId = branch.CommitId, Recipe = request.Recipe, CreatedAt = DateTime.UtcNow, diff --git a/src/marlin.web/Controllers/VCSController.cs b/src/marlin.web/Controllers/VCSController.cs index 7370e0d..8086e99 100644 --- a/src/marlin.web/Controllers/VCSController.cs +++ b/src/marlin.web/Controllers/VCSController.cs @@ -27,15 +27,16 @@ public class VCSController(ApplicationDbContext dbContext, VersionControlSyncSer [HttpGet("details/{id}")] public async Task<IActionResult> Details(Guid id, CancellationToken ct) { - var item = await dbContext.Sources + var item = await dbContext + .Sources .Include(x => x.Branches) .ThenInclude(x => x.Commit) + .ThenInclude(x => x.WorkflowSnapshot) .FirstOrDefaultAsync(x => x.Id == id, ct); if (item == null) return this.NotFound(); - // 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)) @@ -44,6 +45,37 @@ public class VCSController(ApplicationDbContext dbContext, VersionControlSyncSer return this.View((item, secretOptions)); } + [HttpGet("details/{sourceId}/{commitId}/blobs")] + public async Task<IActionResult> Blobs(Guid sourceId, Guid commitId) + { + var blobs = await dbContext + .SourceBlobs + .Include(x => x.Commit) + .ThenInclude(x => x.Source) + .Where(x => x.CommitId == commitId && x.SourceId == sourceId) + .ToListAsync(); + + if (!blobs.Any()) + return this.NotFound(); + + return this.View(blobs); + } + + [HttpGet("details/{sourceId}/{commitId}/workflow")] + public async Task<IActionResult> Workflow(Guid sourceId, Guid commitId) + { + var snapshot = await dbContext + .WorkflowSnapshots + .Include(x => x.Commit) + .ThenInclude(x => x.Source) + .FirstOrDefaultAsync(x => x.CommitId == commitId && x.Commit.SourceId == sourceId); + + if (snapshot == null) + return this.NotFound(); + + return this.View(snapshot); + } + [HttpPost("edit/{id}")] public async Task<IActionResult> Edit(Guid id, VersionControlSource vcs, CancellationToken ct) { |
