Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ public static WorkflowServiceBindingIdentity ResolveBindingIdentity(
throw new InvalidOperationException(
"Workflow service must be reissued under the current tool catalog policy.");
}
if (plan.ExecutionMode == ExternalCapabilityExecutionMode.Unspecified ||
!Enum.IsDefined(plan.ExecutionMode) ||
plan.CapabilityAdmissionPlan == null ||
plan.ExecutionMode != plan.CapabilityAdmissionPlan.ExecutionMode)
if (plan.CapabilityAdmissionPlan == null ||
!WorkflowCapabilityAdmissionPlanIntegrity.IsExecutionModeCompatible(
plan.CapabilityAdmissionPlan.ExecutionMode,
plan.ExecutionMode))
{
throw new InvalidOperationException(
"Workflow service deployment execution mode must match the capability admission plan.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ public static PreparedServiceRevisionArtifact Build(
throw new InvalidOperationException("workflow capability admission execution mode is required.");
}

if (workflowSpec.ExpectedExecutionMode == ExternalCapabilityExecutionMode.Unspecified ||
!Enum.IsDefined(workflowSpec.ExpectedExecutionMode) ||
workflowSpec.ExpectedExecutionMode != capabilityAdmissionPlan.ExecutionMode)
if (!WorkflowCapabilityAdmissionPlanIntegrity.IsExecutionModeCompatible(
capabilityAdmissionPlan.ExecutionMode,
workflowSpec.ExpectedExecutionMode))
{
throw new InvalidOperationException(
"workflow expected execution mode must match the capability admission plan.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
using Aevatar.Studio.Application.Studio.Abstractions;
using Aevatar.Studio.Application.Studio.Contracts;
using Aevatar.Studio.Application.Studio.Services;
using Aevatar.Studio.Domain.Studio.Models;
using Aevatar.Workflow.Application.Abstractions.ExternalCapabilities;
using Aevatar.Workflow.Application.Abstractions.Observatory;
using Aevatar.Workflow.Application.Abstractions.Runs;
Expand Down Expand Up @@ -100,7 +99,7 @@ private static async Task<IResult> WriteDraft(HttpContext http, string? workflow
if (!AevatarScopeAccessGuard.TryGetCallerScopeId(http, out var scopeId)) return Results.Unauthorized();
try
{
var save = new SaveWorkflowDraftRequest(string.Empty, request.WorkflowName, request.FileName, request.Yaml, request.Layout);
var save = new SaveWorkflowDraftRequest(string.Empty, request.WorkflowName, request.FileName, request.Yaml, null);
var receipt = workflowId == null
? await drafts.CreateDraftAsync(scopeId, save, ct)
: await drafts.SaveKnownWorkflowDraftAsync(scopeId, workflowId, save, ct);
Expand Down Expand Up @@ -201,7 +200,7 @@ private static IReadOnlyList<string> SplitCsv(string? value) => string.IsNullOrW
? [] : value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
private static DateTimeOffset? ParseTimestamp(string? value) => DateTimeOffset.TryParse(value, out var parsed) ? parsed : null;

public sealed record WorkflowDraftWriteRequest(string WorkflowName, string Yaml, string? FileName = null, WorkflowLayoutDocument? Layout = null);
public sealed record WorkflowDraftWriteRequest(string WorkflowName, string Yaml, string? FileName = null);
public sealed record WorkflowDraftAcceptedResponse(string WorkflowId, string CommandId);
public sealed record WorkflowPublicationAcceptedResponse(string WorkflowId, string RevisionId, DateTimeOffset AcceptedAtUtc,
IReadOnlyList<WorkflowPublicationCommandHandle> CommandHandles);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@
ValidateBindingIdentity(workflowId, nameof(workflowId));
ValidateBindingIdentity(revisionId, nameof(revisionId));
if (!IsSupportedSchemaVersion(plan.SchemaVersion) ||
plan.ExternalCapabilities.Count != 0 ||

Check warning on line 148 in src/workflow/Aevatar.Workflow.Abstractions/WorkflowCapabilityAdmissionPlanIntegrity.cs

View workflow job for this annotation

GitHub Actions / host-composition-smoke

'WorkflowCapabilityAdmissionPlan.ExternalCapabilities' is obsolete

Check warning on line 148 in src/workflow/Aevatar.Workflow.Abstractions/WorkflowCapabilityAdmissionPlanIntegrity.cs

View workflow job for this annotation

GitHub Actions / runtime-main-flow-smoke

'WorkflowCapabilityAdmissionPlan.ExternalCapabilities' is obsolete
plan.ExecutionMode is not (ExternalCapabilityExecutionMode.Interactive or
ExternalCapabilityExecutionMode.Durable) ||
!FixedTimeEquals(plan.AdmissionDigest, ComputeAdmissionDigest(plan)))
Expand Down Expand Up @@ -244,7 +244,7 @@
workflowId,
revisionId).Result;

private static CompatibilityEvaluation EvaluateCompatibility(

Check warning on line 247 in src/workflow/Aevatar.Workflow.Abstractions/WorkflowCapabilityAdmissionPlanIntegrity.cs

View workflow job for this annotation

GitHub Actions / host-composition-smoke

'EvaluateCompatibility' has a cyclomatic complexity of '29'. Rewrite or refactor the code to decrease its complexity below '26'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1502)

Check warning on line 247 in src/workflow/Aevatar.Workflow.Abstractions/WorkflowCapabilityAdmissionPlanIntegrity.cs

View workflow job for this annotation

GitHub Actions / runtime-main-flow-smoke

'EvaluateCompatibility' has a cyclomatic complexity of '29'. Rewrite or refactor the code to decrease its complexity below '26'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1502)
WorkflowCapabilityAdmissionPlan plan,
string workflowYaml,
IReadOnlyDictionary<string, string>? inlineWorkflowYamls,
Expand All @@ -266,7 +266,7 @@
WorkflowCapabilityAdmissionCompatibilityFailure.SchemaMismatch,
new InvalidOperationException("Workflow capability admission schema version is invalid."));
}
if (plan.ExternalCapabilities.Count != 0)

Check warning on line 269 in src/workflow/Aevatar.Workflow.Abstractions/WorkflowCapabilityAdmissionPlanIntegrity.cs

View workflow job for this annotation

GitHub Actions / host-composition-smoke

'WorkflowCapabilityAdmissionPlan.ExternalCapabilities' is obsolete

Check warning on line 269 in src/workflow/Aevatar.Workflow.Abstractions/WorkflowCapabilityAdmissionPlanIntegrity.cs

View workflow job for this annotation

GitHub Actions / runtime-main-flow-smoke

'WorkflowCapabilityAdmissionPlan.ExternalCapabilities' is obsolete
{
return Failed(
WorkflowCapabilityAdmissionCompatibilityFailure.SchemaMismatch,
Expand All @@ -274,8 +274,7 @@
"Workflow capability admission cannot contain legacy external capabilities."));
}

if (executionMode == ExternalCapabilityExecutionMode.Unspecified ||
plan.ExecutionMode != executionMode)
if (!IsExecutionModeCompatible(plan.ExecutionMode, executionMode))
{
return Failed(
WorkflowCapabilityAdmissionCompatibilityFailure.ExecutionModeMismatch,
Expand Down Expand Up @@ -318,7 +317,9 @@
var actual = plan.InvocationAdmissions.ToArray();
try
{
ValidateInvocationAdmissions(actual, executionMode);
ValidateInvocationAdmissions(actual, plan.ExecutionMode);
if (plan.ExecutionMode != executionMode)
ValidateInvocationAdmissions(actual, executionMode);
}
catch (Exception exception) when (IsStructuralValidationException(exception))
{
Expand Down Expand Up @@ -405,25 +406,31 @@
.Select(static admission => admission.Capability)
.ToArray();

var requiresDurableAuthorizationCatalog = RequiresDurableAuthorizationCatalog(
var requiresStoredDurableAuthorizationCatalog = RequiresDurableAuthorizationCatalog(
plan.ExecutionMode,
expectedCapabilityArray);
var requiresRequestedDurableAuthorizationCatalog = RequiresDurableAuthorizationCatalog(
executionMode,
expectedCapabilityArray);
if (requiresDurableAuthorizationCatalog &&
if (requiresRequestedDurableAuthorizationCatalog &&
!HasDurableAuthorizationCatalogSource(plan.SourceStamps))
{
return Failed(
WorkflowCapabilityAdmissionCompatibilityFailure.RequiredSourceMissing,
new InvalidOperationException(
"Workflow capability admission durable authorization catalog source is required."));
}
if (!HasRequiredSourceEvidence(executionMode, expectedCapabilityArray, plan.SourceStamps))
var requiredSourceEvidenceMode = requiresStoredDurableAuthorizationCatalog
? plan.ExecutionMode
: executionMode;
if (!HasRequiredSourceEvidence(requiredSourceEvidenceMode, expectedCapabilityArray, plan.SourceStamps))
{
return Failed(
WorkflowCapabilityAdmissionCompatibilityFailure.RequiredSourceMissing,
new InvalidOperationException(
"Workflow capability admission required source evidence is invalid."));
}
if (requiresDurableAuthorizationCatalog)
if (requiresStoredDurableAuthorizationCatalog || requiresRequestedDurableAuthorizationCatalog)
{
if (!IsCanonicalDurableAuthorizationOwner(plan.DurableAuthorizationOwner))
{
Expand Down Expand Up @@ -454,6 +461,15 @@
null);
}

public static bool IsExecutionModeCompatible(
ExternalCapabilityExecutionMode planExecutionMode,
ExternalCapabilityExecutionMode expectedExecutionMode) =>
expectedExecutionMode != ExternalCapabilityExecutionMode.Unspecified &&
planExecutionMode is ExternalCapabilityExecutionMode.Interactive or ExternalCapabilityExecutionMode.Durable &&
(planExecutionMode == expectedExecutionMode ||
planExecutionMode == ExternalCapabilityExecutionMode.Durable &&
expectedExecutionMode == ExternalCapabilityExecutionMode.Interactive);
Comment thread
louis4li marked this conversation as resolved.
Resolved

public static IReadOnlyList<ExternalWorkflowCapabilityRef> DistinctCapabilities(
WorkflowCapabilityAdmissionPlan plan)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,29 +453,37 @@ public async Task<WorkflowCapabilityAdmissionPlan> RefreshPersistedAsync(
var confirmations = request.ExplicitRequestConfirmations.Count > 0
? request.ExplicitRequestConfirmations
: RestorePersistedExplicitRequestConfirmations(persisted, request.Access);
var admissionMode = ResolveRefreshAdmissionMode(persisted);
var liveRequest = persisted.WorkflowYamls is { Count: > 0 } workflowYamls
? WorkflowExternalCapabilityAdmissionRequest.FromWorkflowYamls(
request.Access,
workflowYamls,
persisted.SourceKind,
persisted.ExpectedExecutionMode,
admissionMode,
confirmations,
persisted.WorkflowId,
persisted.RevisionId,
persisted.ExpectedExecutionMode)
admissionMode)
: new WorkflowExternalCapabilityAdmissionRequest(
request.Access,
persisted.WorkflowYaml,
persisted.InlineWorkflowYamls,
persisted.SourceKind,
persisted.ExpectedExecutionMode,
admissionMode,
confirmations,
persisted.WorkflowId,
persisted.RevisionId,
persisted.ExpectedExecutionMode);
admissionMode);
return await AdmitAsync(liveRequest, cancellationToken);
}

private static ExternalCapabilityExecutionMode ResolveRefreshAdmissionMode(
PersistedWorkflowCapabilityAdmissionRequest persisted) =>
persisted.Plan.ExecutionMode == ExternalCapabilityExecutionMode.Durable &&
persisted.ExpectedExecutionMode == ExternalCapabilityExecutionMode.Interactive
? ExternalCapabilityExecutionMode.Durable
: persisted.ExpectedExecutionMode;

private async Task ValidatePersistedIntegrityAsync(
PersistedWorkflowCapabilityAdmissionRequest request,
CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,19 +127,37 @@ public void Build_WithUnspecifiedExecutionMode_ShouldFailClosed()
}

[Fact]
public void Build_WhenExpectedModeDiffersFromAdmissionPlan_ShouldFailClosed()
public void Build_WithDurableAdmissionPlanForInteractiveExpectedMode_ShouldSucceed()
{
var plan = new WorkflowCapabilityAdmissionPlan
{
ExecutionMode = ExternalCapabilityExecutionMode.Durable,
};

var action = () => BuildArtifactWithIdentity(
var artifact = BuildArtifactWithIdentity(
"wf-artifact-alpha",
"rev-artifact-alpha",
plan,
ExternalCapabilityExecutionMode.Interactive);

artifact.DeploymentPlan.WorkflowPlan.CapabilityAdmissionPlan.ExecutionMode.Should()
.Be(ExternalCapabilityExecutionMode.Durable);
}

[Fact]
public void Build_WithInteractiveAdmissionPlanForDurableExpectedMode_ShouldFailClosed()
{
var plan = new WorkflowCapabilityAdmissionPlan
{
ExecutionMode = ExternalCapabilityExecutionMode.Interactive,
};

var action = () => BuildArtifactWithIdentity(
"wf-artifact-alpha",
"rev-artifact-alpha",
plan,
ExternalCapabilityExecutionMode.Durable);

action.Should().Throw<InvalidOperationException>()
.WithMessage("*execution mode*match*");
}
Expand Down Expand Up @@ -171,11 +189,33 @@ public void Build_WithoutCurrentToolCatalogPolicy_ShouldFailClosed()
}

[Fact]
public void ResolveBindingIdentity_WhenDeploymentModeDiffersFromAdmissionPlan_ShouldFailClosed()
public void ResolveBindingIdentity_WithDurableAdmissionPlanForInteractiveDeploymentMode_ShouldSucceed()
{
var artifact = BuildArtifact();
artifact.DeploymentPlan.WorkflowPlan.ExecutionMode = ExternalCapabilityExecutionMode.Interactive;

var identity = WorkflowServiceDeploymentPlanIntegrity.ResolveBindingIdentity(
artifact,
"rev-artifact-alpha");

identity.WorkflowId.Should().Be("wf-artifact-alpha");
identity.RevisionId.Should().Be("rev-artifact-alpha");
}

[Fact]
public void ResolveBindingIdentity_WithInteractiveAdmissionPlanForDurableDeploymentMode_ShouldFailClosed()
{
var plan = new WorkflowCapabilityAdmissionPlan
{
ExecutionMode = ExternalCapabilityExecutionMode.Interactive,
};
var artifact = BuildArtifactWithIdentity(
"wf-artifact-alpha",
"rev-artifact-alpha",
plan,
ExternalCapabilityExecutionMode.Interactive);
artifact.DeploymentPlan.WorkflowPlan.ExecutionMode = ExternalCapabilityExecutionMode.Durable;

var action = () => WorkflowServiceDeploymentPlanIntegrity.ResolveBindingIdentity(
artifact,
"rev-artifact-alpha");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public sealed class WorkflowCapabilityAdmissionPlanIntegrityTests

[Theory]
[InlineData("schema", WorkflowCapabilityAdmissionCompatibilityFailure.SchemaMismatch)]
[InlineData("mode", WorkflowCapabilityAdmissionCompatibilityFailure.ExecutionModeMismatch)]
[InlineData("mode", WorkflowCapabilityAdmissionCompatibilityFailure.AdmissionDigestMismatch)]
[InlineData("definition", WorkflowCapabilityAdmissionCompatibilityFailure.DefinitionDigestMismatch)]
[InlineData("call-site-count", WorkflowCapabilityAdmissionCompatibilityFailure.InvocationMismatch)]
[InlineData("call-site-order", WorkflowCapabilityAdmissionCompatibilityFailure.InvocationOrderingInvalid)]
Expand Down Expand Up @@ -95,6 +95,25 @@ public void CheckCompatibility_WithMutatedPlan_ShouldReturnTypedFailure(
result.Failure.Should().Be(expected);
}

[Fact]
public void CheckCompatibility_WithInteractivePlanForDurableExpectedMode_ShouldRejectMode()
{
var fixture = ExplicitRequestFixture(ExternalCapabilityExecutionMode.Interactive);

var result = WorkflowCapabilityAdmissionPlanIntegrity.CheckCompatibility(
fixture.Plan,
fixture.WorkflowYaml,
fixture.InlineWorkflowYamls,
ExternalCapabilityExecutionMode.Durable,
fixture.ExpectedInvocations,
fixture.WorkflowId,
fixture.RevisionId);

result.Succeeded.Should().BeFalse();
result.Failure.Should().Be(
WorkflowCapabilityAdmissionCompatibilityFailure.ExecutionModeMismatch);
}

[Theory]
[InlineData(WorkflowCapabilityAdmissionPlanIntegrity.LegacySchemaVersion)]
[InlineData(WorkflowCapabilityAdmissionPlanIntegrity.OpenApiSchemaVersion)]
Expand Down
Loading
Loading