summaryrefslogtreecommitdiff
path: root/src/marlin.web/Controllers
diff options
context:
space:
mode:
authorMelodi <62381002+Melodi17@users.noreply.github.com>2026-09-13 13:22:20 +1000
committerMelodi <62381002+Melodi17@users.noreply.github.com>2026-09-13 13:22:20 +1000
commitdf0737e55afeb5c01eb39909605970d1d3a73ffc (patch)
tree09823eaa310c60eefd2420dc305acae45c81900f /src/marlin.web/Controllers
parentee69ce25b2fba871920260028bb10c2d6b62b26f (diff)
refactor: cleanup; feat: connection to agent
Diffstat (limited to 'src/marlin.web/Controllers')
-rw-r--r--src/marlin.web/Controllers/AgentsController.cs89
-rw-r--r--src/marlin.web/Controllers/DispatchController.cs65
-rw-r--r--src/marlin.web/Controllers/JobsController.cs35
-rw-r--r--src/marlin.web/Controllers/VCSController.cs36
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)
{