Skip to content

Commit 4671706

Browse files
[cross-repo from workflow#481] TD-063: Cross-namespace service binding-kind contract drifts from runtime enum (#122)
1 parent 8673c40 commit 4671706

6 files changed

Lines changed: 93 additions & 10 deletions

File tree

app/Support/ServiceCallBoundary.php

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44

55
use App\Auth\Principal;
66
use Workflow\V2\Contracts\ServiceBoundaryPolicy;
7+
use Workflow\V2\Enums\ServiceCallBindingKind;
78
use Workflow\V2\Enums\ServiceCallOperationMode;
9+
use Workflow\V2\Enums\ServiceCallOutcome;
810
use Workflow\V2\Models\WorkflowServiceOperation;
911
use Workflow\V2\Support\DefaultServiceBoundaryPolicy;
1012
use Workflow\V2\Support\ServiceBoundaryAuditRecorder;
13+
use Workflow\V2\Support\ServiceBoundaryDecision;
1114
use Workflow\V2\Support\ServiceBoundaryRequest;
1215
use Workflow\V2\Support\ServiceCallPrincipal;
1316

@@ -81,6 +84,8 @@ public function admitFor(
8184
?array $cancellationPolicy = null,
8285
?array $retryPolicy = null,
8386
): ServiceCallAdmission {
87+
$resolvedBindingKind = self::resolvedBindingKind($operation);
88+
8489
$request = new ServiceBoundaryRequest(
8590
principal: self::principalFromAuth($principal),
8691
callerNamespace: $callerNamespace,
@@ -91,8 +96,8 @@ public function admitFor(
9196
operationMode: ServiceCallOperationMode::tryFromCatalog($operationModeOverride)
9297
?? ServiceCallOperationMode::tryFromCatalog($operation->operation_mode)
9398
?? ServiceCallOperationMode::Async,
94-
resolvedBindingKind: (string) $operation->handler_binding_kind,
95-
resolvedTargetReference: $operation->handler_target_reference,
99+
resolvedBindingKind: $resolvedBindingKind,
100+
resolvedTargetReference: $resolvedBindingKind === null ? null : $operation->handler_target_reference,
96101
callerWorkflowInstanceId: $callerWorkflowInstanceId,
97102
callerWorkflowRunId: $callerWorkflowRunId,
98103
linkedWorkflowInstanceId: $linkedWorkflowInstanceId,
@@ -108,6 +113,27 @@ public function admitFor(
108113
retryPolicy: $retryPolicy,
109114
);
110115

116+
if ($resolvedBindingKind === null) {
117+
$decision = new ServiceBoundaryDecision(
118+
outcome: ServiceCallOutcome::RejectedNotFound,
119+
reason: 'unknown_binding_kind',
120+
message: sprintf(
121+
'Service operation [%s] has unknown handler binding kind [%s].',
122+
$operation->operation_name,
123+
$operation->handler_binding_kind,
124+
),
125+
policyName: 'default',
126+
metadata: [
127+
'failure_reason' => 'resolution_failure',
128+
'resolution_failed_at' => 'handler_binding_kind',
129+
'handler_binding_kind' => (string) $operation->handler_binding_kind,
130+
],
131+
);
132+
$call = $this->recorder->record($request, $decision);
133+
134+
return new ServiceCallAdmission($decision, $call, $request);
135+
}
136+
111137
return $this->admit($request);
112138
}
113139

@@ -133,4 +159,23 @@ private static function principalFromAuth(Principal $principal): ServiceCallPrin
133159
claims: $principal->claims(),
134160
);
135161
}
162+
163+
private static function resolvedBindingKind(WorkflowServiceOperation $operation): ?string
164+
{
165+
return match (strtolower(trim((string) $operation->handler_binding_kind))) {
166+
ServiceCallBindingKind::WorkflowRun->value,
167+
'start_workflow',
168+
'workflow_class' => ServiceCallBindingKind::WorkflowRun->value,
169+
ServiceCallBindingKind::WorkflowUpdate->value,
170+
'update_workflow' => ServiceCallBindingKind::WorkflowUpdate->value,
171+
ServiceCallBindingKind::WorkflowSignal->value,
172+
'signal_workflow' => ServiceCallBindingKind::WorkflowSignal->value,
173+
ServiceCallBindingKind::WorkflowQuery->value,
174+
'query_workflow' => ServiceCallBindingKind::WorkflowQuery->value,
175+
ServiceCallBindingKind::ActivityExecution->value => ServiceCallBindingKind::ActivityExecution->value,
176+
ServiceCallBindingKind::InvocableCarrierRequest->value,
177+
'invocable_http' => ServiceCallBindingKind::InvocableCarrierRequest->value,
178+
default => null,
179+
};
180+
}
136181
}

tests/Feature/ClusterInfoTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,14 @@ public function test_it_publishes_service_execution_contract_manifest(): void
668668
->assertJsonPath('service_execution_contract.version', 1)
669669
->assertJsonPath('service_execution_contract.handler_binding_kinds.0', 'start_workflow')
670670
->assertJsonPath('service_execution_contract.handler_binding_kinds.5', 'invocable_http')
671+
->assertJsonPath(
672+
'service_execution_contract.resolved_target_binding_kinds.workflow_run.terminal_link_reference',
673+
'workflow_run_id',
674+
)
675+
->assertJsonPath(
676+
'service_execution_contract.resolved_target_binding_kinds.invocable_carrier_request.terminal_link_reference',
677+
'carrier_request_id',
678+
)
671679
->assertJsonPath(
672680
'service_execution_contract.durable_response_fields.0',
673681
'service_call_id',

tests/Feature/ServiceCallBoundaryTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Tests\Feature\Concerns\ServerTestHelpers;
1010
use Tests\TestCase;
1111
use Workflow\V2\Contracts\ServiceBoundaryPolicy;
12+
use Workflow\V2\Enums\ServiceCallBindingKind;
1213
use Workflow\V2\Enums\ServiceCallOutcome;
1314
use Workflow\V2\Models\WorkflowService;
1415
use Workflow\V2\Models\WorkflowServiceEndpoint;
@@ -55,6 +56,7 @@ public function test_admit_persists_the_audit_row_for_an_accepted_call(): void
5556
$this->assertSame('accepted', $admission->call->status);
5657
$this->assertSame('analytics', $admission->call->caller_namespace);
5758
$this->assertSame('finance', $admission->call->target_namespace);
59+
$this->assertSame(ServiceCallBindingKind::WorkflowUpdate->value, $admission->call->resolved_binding_kind);
5860
$this->assertSame('worker', $admission->call->caller_principal_roles[0]);
5961
$this->assertNotNull($admission->call->accepted_at);
6062
$this->assertNull($admission->call->failed_at);
@@ -67,6 +69,32 @@ public function test_admit_persists_the_audit_row_for_an_accepted_call(): void
6769
]);
6870
}
6971

72+
public function test_admit_fails_closed_when_handler_binding_kind_is_unknown(): void
73+
{
74+
$operation = $this->seedOperation();
75+
$operation->update(['handler_binding_kind' => 'unsupported_handler']);
76+
77+
/** @var ServiceCallBoundary $boundary */
78+
$boundary = $this->app->make(ServiceCallBoundary::class);
79+
80+
$admission = $boundary->admitFor(
81+
principal: Principal::role('worker', 'token'),
82+
callerNamespace: 'analytics',
83+
operation: $operation,
84+
endpointName: 'billing',
85+
serviceName: 'invoicing',
86+
);
87+
88+
$this->assertTrue($admission->rejected());
89+
$this->assertSame(ServiceCallOutcome::RejectedNotFound, $admission->decision->outcome);
90+
$this->assertSame('unknown_binding_kind', $admission->decision->reason);
91+
$this->assertSame('failed', $admission->call->status);
92+
$this->assertSame('unresolved', $admission->call->resolved_binding_kind);
93+
$this->assertNull($admission->call->resolved_target_reference);
94+
$this->assertSame('handler_binding_kind', $admission->call->outcome_metadata['resolution_failed_at']);
95+
$this->assertNull($admission->call->accepted_at);
96+
}
97+
7098
public function test_admit_blocks_call_when_caller_namespace_is_denied(): void
7199
{
72100
$this->bindStrictNamespacePolicy(['untrusted']);

tests/Feature/ServiceCatalogControllerTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ public function test_it_shows_a_durable_service_call_snapshot(): void
349349
'linked_workflow_update_id' => (string) Str::ulid(),
350350
'status' => 'running',
351351
'operation_mode' => 'async',
352-
'resolved_binding_kind' => 'update_workflow',
352+
'resolved_binding_kind' => 'workflow_update',
353353
'resolved_target_reference' => 'updates.invoice.submit',
354354
'payload_codec' => 'json',
355355
'input_payload_reference' => 'payloads/service-calls/input-1.json',
@@ -381,7 +381,7 @@ public function test_it_shows_a_durable_service_call_snapshot(): void
381381
->assertJsonPath('target_namespace', 'default')
382382
->assertJsonPath('status', 'running')
383383
->assertJsonPath('operation_mode', 'async')
384-
->assertJsonPath('resolved_binding_kind', 'update_workflow')
384+
->assertJsonPath('resolved_binding_kind', 'workflow_update')
385385
->assertJsonPath('resolved_target_reference', 'updates.invoice.submit')
386386
->assertJsonPath('payload_codec', 'json')
387387
->assertJsonPath('input_payload_reference', 'payloads/service-calls/input-1.json')
@@ -455,7 +455,7 @@ public function test_it_blocks_deleting_service_and_operation_resources_that_sti
455455
'target_namespace' => 'default',
456456
'status' => 'accepted',
457457
'operation_mode' => 'sync',
458-
'resolved_binding_kind' => 'start_workflow',
458+
'resolved_binding_kind' => 'workflow_run',
459459
'accepted_at' => now(),
460460
]);
461461

tests/Feature/ServiceExecutionRoutesTest.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public function execute(string $endpointName, string $serviceName, string $opera
6060
'service_name' => $serviceName,
6161
'operation_name' => $operationName,
6262
'operation_mode' => 'async',
63-
'resolved_binding_kind' => 'start_workflow',
63+
'resolved_binding_kind' => 'workflow_run',
6464
'resolved_target_reference' => 'workflows.invoice.create',
6565
'status' => 'accepted',
6666
'linked_workflow_instance_id' => 'invoice-1',
@@ -122,7 +122,7 @@ public function cancelCall(string $serviceCallId, array $options = []): array
122122
->assertJsonPath('accepted', true)
123123
->assertJsonPath('service_call_id', '01JEXECUTECALL000000000000')
124124
->assertJsonPath('linked_workflow_instance_id', 'invoice-1')
125-
->assertJsonPath('resolved_binding_kind', 'start_workflow');
125+
->assertJsonPath('resolved_binding_kind', 'workflow_run');
126126

127127
$this->assertNotNull($stub->captured);
128128
$this->assertSame('billing', $stub->captured['endpoint']);
@@ -208,7 +208,7 @@ public function test_cancel_route_routes_through_service_control_plane(): void
208208
'operation_name' => 'createinvoice',
209209
'status' => 'accepted',
210210
'operation_mode' => 'async',
211-
'resolved_binding_kind' => 'start_workflow',
211+
'resolved_binding_kind' => 'workflow_run',
212212
'resolved_target_reference' => 'workflows.invoice.create',
213213
]);
214214

@@ -282,7 +282,7 @@ public function test_service_call_show_routes_through_service_control_plane_desc
282282
'operation_name' => 'createinvoice',
283283
'status' => 'started',
284284
'operation_mode' => 'async',
285-
'resolved_binding_kind' => 'start_workflow',
285+
'resolved_binding_kind' => 'workflow_run',
286286
'resolved_target_reference' => '01JSVCCALLRUN00000000004',
287287
'linked_workflow_instance_id' => 'invoice-4',
288288
'linked_workflow_run_id' => '01JSVCCALLRUN00000000004',
@@ -313,7 +313,7 @@ public function describeCall(string $serviceCallId, array $options = []): array
313313
'linked_workflow_instance_id' => 'invoice-4',
314314
'linked_workflow_run_id' => '01JSVCCALLRUN00000000004',
315315
'linked_workflow_update_id' => null,
316-
'resolved_binding_kind' => 'start_workflow',
316+
'resolved_binding_kind' => 'workflow_run',
317317
'resolved_target_reference' => '01JSVCCALLRUN00000000004',
318318
'reason' => null,
319319
];

tests/Unit/Support/WorkflowPackageApiFloorTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ public function test_service_execution_contract_manifest_is_public_static(): voi
115115
$this->assertSame('durable-workflow.v2.service-execution.contract', $manifest['schema'] ?? null);
116116
$this->assertContains('start_workflow', $manifest['handler_binding_kinds'] ?? []);
117117
$this->assertContains('invocable_http', $manifest['handler_binding_kinds'] ?? []);
118+
$this->assertArrayHasKey('workflow_run', $manifest['resolved_target_binding_kinds'] ?? []);
119+
$this->assertArrayHasKey('invocable_carrier_request', $manifest['resolved_target_binding_kinds'] ?? []);
118120
}
119121

120122
public function test_worker_session_protocol_contract_is_public_static(): void

0 commit comments

Comments
 (0)