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:
@@ -31,6 +31,21 @@ public record MgmtDeployArtifactsCommand(int? SiteId = null);
|
||||
/// and needs the push anyway. Never overrides the Disabled skip.
|
||||
/// </param>
|
||||
public record MgmtDeploySiteCommand(int SiteId, bool Force = false);
|
||||
|
||||
/// <summary>
|
||||
/// Bulk-deploys every instance in one area SUBTREE — the area plus every area
|
||||
/// nested beneath it at any depth — with exactly the rules of
|
||||
/// <see cref="MgmtDeploySiteCommand"/>: Disabled instances are always skipped,
|
||||
/// up-to-date ones are skipped unless <see cref="Force"/> is set, and the batch is
|
||||
/// not all-or-nothing. Answers with the same site deployment summary. Deployer-only,
|
||||
/// site-scoped by the area's owning site (Gitea #40).
|
||||
/// </summary>
|
||||
/// <param name="AreaId">Database id of the subtree's root area.</param>
|
||||
/// <param name="Force">
|
||||
/// Redeploy instances whose config already matches the deployed revision. Never
|
||||
/// overrides the Disabled skip. Same meaning as <see cref="MgmtDeploySiteCommand.Force"/>.
|
||||
/// </param>
|
||||
public record MgmtDeployAreaCommand(int AreaId, bool Force = false);
|
||||
public record QueryDeploymentsCommand(int? InstanceId = null, string? Status = null, int Page = 1, int PageSize = 50);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -50,10 +50,10 @@ public static class ManagementEndpoints
|
||||
[
|
||||
typeof(ImportBundleCommand), typeof(PreviewBundleCommand), typeof(ExportBundleCommand),
|
||||
typeof(MgmtDeployArtifactsCommand), typeof(MgmtDeployInstanceCommand),
|
||||
// A bulk site deploy is by construction the longest command in the set —
|
||||
// A bulk site (or area-subtree) deploy is by construction the longest command in the set —
|
||||
// it is N instance deploys, so it must never inherit the ordinary Ask
|
||||
// timeout or the caller would 504 while the batch is still applying.
|
||||
typeof(MgmtDeploySiteCommand),
|
||||
typeof(MgmtDeploySiteCommand), typeof(MgmtDeployAreaCommand),
|
||||
// Flow Debugger (design 2026-09-05 §7.1): a Live run lasts up to the method timeout
|
||||
// (+ grace) or the value-change wait window — never the ordinary Ask timeout.
|
||||
typeof(FlowDebugRunMethodCommand), typeof(FlowDebugRunValueChangeCommand),
|
||||
@@ -73,7 +73,8 @@ public static class ManagementEndpoints
|
||||
public static TimeSpan ResolveAskTimeout(ManagementServiceOptions? options, Type commandType)
|
||||
=> LongRunningCommands.Contains(commandType)
|
||||
? (options is { LongRunningCommandTimeout.Ticks: > 0 } o ? o.LongRunningCommandTimeout : DefaultLongRunningTimeout)
|
||||
+ (commandType == typeof(MgmtDeploySiteCommand) ? TimeSpan.FromMinutes(1) : TimeSpan.Zero)
|
||||
+ (commandType == typeof(MgmtDeploySiteCommand) || commandType == typeof(MgmtDeployAreaCommand)
|
||||
? TimeSpan.FromMinutes(1) : TimeSpan.Zero)
|
||||
: ResolveAskTimeout(options);
|
||||
|
||||
/// <summary>Registers the <c>POST /management</c> endpoint on the given route builder.</summary>
|
||||
|
||||
Reference in New Issue
Block a user