Add authenticated internal worker API
This commit is contained in:
parent
9d96c4d563
commit
ebf818d23c
1 changed files with 2745 additions and 43 deletions
|
|
@ -1,5 +1,12 @@
|
|||
use std::{collections::BTreeSet, fs};
|
||||
|
||||
use remora_contracts::{
|
||||
ActivationResponseDto, AttemptActivateRequestDto, AttemptCheckpointRequestDto,
|
||||
AttemptOutcomeRequestDto, AttemptProgressRequestDto, AttemptRenewRequestDto,
|
||||
AttemptResolvedSourceRequestDto, ClaimRequestDto, ClaimResponseDto, JobPhaseProjectionDto,
|
||||
LeaseProjectionDto,
|
||||
};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use serde_json::Value;
|
||||
|
||||
#[test]
|
||||
|
|
@ -48,6 +55,273 @@ fn api_contract_documents_parse_and_local_references_resolve() {
|
|||
validate_openapi_corpus(&component_schema, &corpus, "JobFailure", "job_failures");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn worker_routes_and_dtos_conform_to_the_mutable_openapi_contract() {
|
||||
let openapi: Value =
|
||||
serde_yaml_ng::from_str(&fs::read_to_string("../../docs/api/openapi.yaml").unwrap())
|
||||
.unwrap();
|
||||
let component_schema = openapi_component_schema(&openapi);
|
||||
let mutation = serde_json::json!({
|
||||
"operation_id": "operation_0001",
|
||||
"fence": 1,
|
||||
"worker_incarnation": "worker_0001"
|
||||
});
|
||||
let descriptor = serde_json::json!({
|
||||
"id": "descriptor_0001",
|
||||
"kind": "resolved_source",
|
||||
"schema_version": 1,
|
||||
"byte_length": 64,
|
||||
"item_count": 1,
|
||||
"checksum": format!("sha256:{}", "22".repeat(32))
|
||||
});
|
||||
let execution = serde_json::json!({
|
||||
"id": "grant_0001",
|
||||
"job_id": "job_00000001",
|
||||
"attempt_id": "attempt_0001",
|
||||
"fence": 1,
|
||||
"worker_incarnation": "worker_0001",
|
||||
"source": {
|
||||
"kind": "http",
|
||||
"display_url": "https://example.invalid/artifact",
|
||||
"result_path": "artifact.bin"
|
||||
},
|
||||
"sensitive_source_ref": "source_00001",
|
||||
"credential_version": null,
|
||||
"resolved_source_ref": null,
|
||||
"target": {
|
||||
"store": "primary",
|
||||
"bucket": "remora-results",
|
||||
"prefix": "users/alice"
|
||||
},
|
||||
"staging_target": {
|
||||
"store": "primary",
|
||||
"bucket": "remora-results",
|
||||
"prefix": "_remora/a/attempt_0001/"
|
||||
},
|
||||
"execution_grant_ref": "grant_0001",
|
||||
"checkpoint_ref": null
|
||||
});
|
||||
let authority = serde_json::json!({
|
||||
"attempt_id": "attempt_0001",
|
||||
"fence": 1,
|
||||
"worker_incarnation": "worker_0001"
|
||||
});
|
||||
let lease = serde_json::json!({
|
||||
"expires_at": "2026-07-17T00:01:00Z",
|
||||
"renew_after": "2026-07-17T00:00:30Z"
|
||||
});
|
||||
|
||||
let claim_request = serde_json::json!({
|
||||
"operation_id": "operation_0001",
|
||||
"worker_incarnation": "worker_0001"
|
||||
});
|
||||
assert_dto_and_schema::<ClaimRequestDto>(&component_schema, "ClaimRequest", claim_request);
|
||||
let claim_response = serde_json::json!({
|
||||
"authority": authority,
|
||||
"execution": execution
|
||||
});
|
||||
assert_dto_and_schema::<ClaimResponseDto>(
|
||||
&component_schema,
|
||||
"ClaimResponse",
|
||||
claim_response.clone(),
|
||||
);
|
||||
assert_dto_and_schema::<AttemptRenewRequestDto>(
|
||||
&component_schema,
|
||||
"AttemptRenewRequest",
|
||||
mutation.clone(),
|
||||
);
|
||||
assert_dto_and_schema::<AttemptActivateRequestDto>(
|
||||
&component_schema,
|
||||
"AttemptActivateRequest",
|
||||
mutation.clone(),
|
||||
);
|
||||
assert_dto_and_schema::<ActivationResponseDto>(
|
||||
&component_schema,
|
||||
"ActivationResponse",
|
||||
serde_json::json!({
|
||||
"authority": claim_response["authority"],
|
||||
"execution": claim_response["execution"],
|
||||
"lease": lease.clone()
|
||||
}),
|
||||
);
|
||||
assert_dto_and_schema::<LeaseProjectionDto>(
|
||||
&component_schema,
|
||||
"LeaseProjection",
|
||||
lease.clone(),
|
||||
);
|
||||
assert_dto_and_schema::<AttemptResolvedSourceRequestDto>(
|
||||
&component_schema,
|
||||
"AttemptResolvedSourceRequest",
|
||||
merged(
|
||||
&mutation,
|
||||
serde_json::json!({ "resolved_source_ref": descriptor.clone() }),
|
||||
),
|
||||
);
|
||||
let mut checkpoint_descriptor = descriptor.clone();
|
||||
checkpoint_descriptor["kind"] = Value::String("checkpoint".into());
|
||||
assert_dto_and_schema::<AttemptCheckpointRequestDto>(
|
||||
&component_schema,
|
||||
"AttemptCheckpointRequest",
|
||||
merged(
|
||||
&mutation,
|
||||
serde_json::json!({
|
||||
"checkpoint_ref": checkpoint_descriptor,
|
||||
"resolved_source_id": "descriptor_0001",
|
||||
"validated_bytes": 1024,
|
||||
"validated_members": 1
|
||||
}),
|
||||
),
|
||||
);
|
||||
assert_dto_and_schema::<AttemptProgressRequestDto>(
|
||||
&component_schema,
|
||||
"AttemptProgressRequest",
|
||||
merged(
|
||||
&mutation,
|
||||
serde_json::json!({
|
||||
"progress": {
|
||||
"completed_bytes": 1024,
|
||||
"total_bytes": 4096,
|
||||
"completed_members": 1,
|
||||
"total_members": 1
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
assert_dto_and_schema::<AttemptOutcomeRequestDto>(
|
||||
&component_schema,
|
||||
"AttemptOutcomeRequest",
|
||||
merged(
|
||||
&mutation,
|
||||
serde_json::json!({
|
||||
"outcome": {
|
||||
"kind": "failure",
|
||||
"failure": {
|
||||
"class": "transient_source",
|
||||
"message": "temporarily unavailable"
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
assert_dto_and_schema::<JobPhaseProjectionDto>(
|
||||
&component_schema,
|
||||
"JobPhaseProjection",
|
||||
serde_json::json!({ "phase": "RetryWait" }),
|
||||
);
|
||||
|
||||
let routes = [
|
||||
(
|
||||
"/internal/v1/jobs/{job_id}/claim",
|
||||
"ClaimRequest",
|
||||
"200",
|
||||
Some("ClaimResponse"),
|
||||
),
|
||||
(
|
||||
"/internal/v1/attempts/{attempt_id}/renew",
|
||||
"AttemptRenewRequest",
|
||||
"200",
|
||||
Some("LeaseProjection"),
|
||||
),
|
||||
(
|
||||
"/internal/v1/attempts/{attempt_id}/activate",
|
||||
"AttemptActivateRequest",
|
||||
"200",
|
||||
Some("ActivationResponse"),
|
||||
),
|
||||
(
|
||||
"/internal/v1/attempts/{attempt_id}/resolved-source",
|
||||
"AttemptResolvedSourceRequest",
|
||||
"204",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"/internal/v1/attempts/{attempt_id}/checkpoint",
|
||||
"AttemptCheckpointRequest",
|
||||
"204",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"/internal/v1/attempts/{attempt_id}/progress",
|
||||
"AttemptProgressRequest",
|
||||
"204",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"/internal/v1/attempts/{attempt_id}/outcome",
|
||||
"AttemptOutcomeRequest",
|
||||
"200",
|
||||
Some("JobPhaseProjection"),
|
||||
),
|
||||
];
|
||||
for (path, request, status, response) in routes {
|
||||
assert_worker_route(&openapi, path, request, status, response);
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_dto_and_schema<T>(component_schema: &Value, definition: &str, value: Value)
|
||||
where
|
||||
T: DeserializeOwned + Serialize,
|
||||
{
|
||||
let dto: T = serde_json::from_value(value.clone()).unwrap();
|
||||
assert_eq!(serde_json::to_value(dto).unwrap(), value);
|
||||
validate_openapi_value(component_schema, definition, &value);
|
||||
}
|
||||
|
||||
fn validate_openapi_value(component_schema: &Value, definition: &str, value: &Value) {
|
||||
let mut schema = component_schema.clone();
|
||||
schema.as_object_mut().unwrap().insert(
|
||||
"$ref".to_owned(),
|
||||
Value::String(format!("#/$defs/{definition}")),
|
||||
);
|
||||
jsonschema::draft202012::options()
|
||||
.should_validate_formats(true)
|
||||
.build(&schema)
|
||||
.unwrap()
|
||||
.validate(value)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn merged(base: &Value, extension: Value) -> Value {
|
||||
let mut value = base.clone();
|
||||
value
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.extend(extension.as_object().unwrap().clone());
|
||||
value
|
||||
}
|
||||
|
||||
fn assert_worker_route(
|
||||
openapi: &Value,
|
||||
path: &str,
|
||||
request_schema: &str,
|
||||
success_status: &str,
|
||||
response_schema: Option<&str>,
|
||||
) {
|
||||
let operation = &openapi["paths"][path]["post"];
|
||||
assert_eq!(
|
||||
operation.pointer("/security/0/serviceAuth"),
|
||||
Some(&serde_json::json!([]))
|
||||
);
|
||||
assert_eq!(
|
||||
operation.pointer("/requestBody/content/application~1json/schema/$ref"),
|
||||
Some(&Value::String(format!(
|
||||
"#/components/schemas/{request_schema}"
|
||||
)))
|
||||
);
|
||||
assert!(operation["responses"].get("409").is_some());
|
||||
assert!(operation["responses"].get("default").is_some());
|
||||
let success = &operation["responses"][success_status];
|
||||
match response_schema {
|
||||
Some(response_schema) => assert_eq!(
|
||||
success.pointer("/content/application~1json/schema/$ref"),
|
||||
Some(&Value::String(format!(
|
||||
"#/components/schemas/{response_schema}"
|
||||
)))
|
||||
),
|
||||
None => assert!(success.get("content").is_none()),
|
||||
}
|
||||
}
|
||||
|
||||
fn openapi_component_schema(openapi: &Value) -> Value {
|
||||
let mut definitions = openapi
|
||||
.pointer("/components/schemas")
|
||||
|
|
|
|||
Loading…
Reference in a new issue