feat(managementservice): MgmtDeployArea — Deployer-only, scoped by the area's owning site (fail-closed on an unknown area), long-running like MgmtDeploySite (Gitea #40)

This commit is contained in:
Joseph Doherty
2026-10-22 14:10:53 -04:00
parent eb1c9ea7f9
commit f72c72c52f
7 changed files with 154 additions and 5 deletions
@@ -343,7 +343,8 @@ public class ManagementActor : ReceiveActor
PreviewBundleCommand or ImportBundleCommand => AdminOnly,
// Deployer operations
CreateInstanceCommand or MgmtDeployInstanceCommand or MgmtDeploySiteCommand or MgmtEnableInstanceCommand
CreateInstanceCommand or MgmtDeployInstanceCommand or MgmtDeploySiteCommand or MgmtDeployAreaCommand
or MgmtEnableInstanceCommand
or MgmtDisableInstanceCommand or MgmtDeleteInstanceCommand
or SetConnectionBindingsCommand or SetInstanceOverridesCommand or SetInstanceAreaCommand
or SetInstanceAlarmOverrideCommand or DeleteInstanceAlarmOverrideCommand
@@ -431,6 +432,7 @@ public class ManagementActor : ReceiveActor
CreateInstanceCommand cmd => await HandleCreateInstance(sp, cmd, user),
MgmtDeployInstanceCommand cmd => await HandleDeployInstance(sp, cmd, user),
MgmtDeploySiteCommand cmd => await HandleDeploySite(sp, cmd, user),
MgmtDeployAreaCommand cmd => await HandleDeployArea(sp, cmd, user),
MgmtEnableInstanceCommand cmd => await HandleEnableInstance(sp, cmd, user),
MgmtDisableInstanceCommand cmd => await HandleDisableInstance(sp, cmd, user),
MgmtDeleteInstanceCommand cmd => await HandleDeleteInstance(sp, cmd, user),
@@ -1051,6 +1053,43 @@ public class ManagementActor : ReceiveActor
}
}
/// <summary>
/// Bulk-deploys every deployable instance in an area subtree (Gitea #40) —
/// <see cref="HandleDeploySite"/> narrowed to one area and its descendants.
///
/// <para>
/// Site scope is enforced on the area's OWNING site, the only site the batch
/// can touch. An area that does not resolve fails CLOSED for a site-scoped
/// caller (the same denial as an out-of-scope one, so the gate is not an
/// existence oracle — register row 98); a system-wide caller gets a plain
/// "not found" error. Same overall deadline and <c>BULK_DEPLOY_TIMEOUT</c>
/// mapping as the site deploy.
/// </para>
/// </summary>
private static async Task<object?> HandleDeployArea(IServiceProvider sp, MgmtDeployAreaCommand cmd, AuthenticatedUser user)
{
var area = await sp.GetRequiredService<ITemplateEngineRepository>().GetAreaByIdAsync(cmd.AreaId);
if (area is null)
{
DenyUnresolvedTargetIfScoped(user, $"area {cmd.AreaId}");
throw new ManagementCommandException($"Area with ID {cmd.AreaId} not found.");
}
EnforceSiteScope(user, area.SiteId);
var svc = sp.GetRequiredService<DeploymentService>();
var timeout = sp.GetService<Microsoft.Extensions.Options.IOptions<ManagementServiceOptions>>()?.Value.LongRunningCommandTimeout ?? TimeSpan.FromMinutes(5);
using var deadline = new CancellationTokenSource(timeout);
try
{
var result = await svc.DeployAreaAsync(cmd.AreaId, user.Username, cmd.Force, deadline.Token);
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
}
catch (OperationCanceledException) when (deadline.IsCancellationRequested)
{
throw new ManagementCommandException("Overall bulk deployment deadline exceeded before further apply could be scheduled.", "BULK_DEPLOY_TIMEOUT");
}
}
private static async Task<object?> HandleEnableInstance(IServiceProvider sp, MgmtEnableInstanceCommand cmd, AuthenticatedUser user)
{
await EnforceSiteScopeForInstance(sp, user, cmd.InstanceId);