118 lines
4.6 KiB
Text
118 lines
4.6 KiB
Text
|
|
# schemas/lib/ops_contract.ncl — Ops contract (ADR-037)
|
||
|
|
# NATS JetStream subject namespaces, JWT signed command structure,
|
||
|
|
# stream configuration, and workspace ops contract definition.
|
||
|
|
|
||
|
|
let OpsType =
|
||
|
|
std.contract.custom (
|
||
|
|
fun label =>
|
||
|
|
fun value =>
|
||
|
|
let valid = ["deploy", "scale", "restart", "secret_update", "drain"] in
|
||
|
|
if std.array.any (fun x => x == value) valid then
|
||
|
|
'Ok value
|
||
|
|
else
|
||
|
|
'Error {
|
||
|
|
message = "Invalid op_type '%{value}'.\nValid values: deploy | scale | restart | secret_update | drain"
|
||
|
|
}
|
||
|
|
)
|
||
|
|
in
|
||
|
|
|
||
|
|
let _StreamRetention = [| 'WorkQueue, 'Limits, 'Interest |] in
|
||
|
|
|
||
|
|
let _ScopeEntry = {
|
||
|
|
op_type | OpsType,
|
||
|
|
target_pattern | String | doc "Glob pattern for allowed op targets (e.g., 'staging-*', 'vapora')",
|
||
|
|
} in
|
||
|
|
|
||
|
|
let _JwtClaims = {
|
||
|
|
iss | String | doc "Signer identity: keeper-vm-primary | operator-<id> | gh-actions-<id>",
|
||
|
|
sub | String | doc "Requesting principal: woodpecker-job-<id> | manual-<operator>",
|
||
|
|
aud | String | doc "Target workspace name",
|
||
|
|
scopes | Array _ScopeEntry | doc "Allowed (op_type, target_pattern) tuples scoped to this signer",
|
||
|
|
seq | Number | doc "Per-issuer monotonic counter — anti-replay",
|
||
|
|
jti | String | doc "UUIDv4 idempotency key",
|
||
|
|
expected_state_version | String | doc "Optimistic concurrency token — workspace state version this op read",
|
||
|
|
exp | Number | doc "Unix timestamp: token expiry",
|
||
|
|
nbf | Number | doc "Unix timestamp: token not-valid-before",
|
||
|
|
} in
|
||
|
|
|
||
|
|
let _StreamConfig = {
|
||
|
|
name | String,
|
||
|
|
subjects | Array String,
|
||
|
|
retention | _StreamRetention | doc "JetStream retention policy" | default = 'WorkQueue,
|
||
|
|
max_age_s | Number | doc "Message TTL in seconds",
|
||
|
|
replicas | Number | doc "JetStream stream replica count" | default = 1,
|
||
|
|
max_bytes | Number | doc "Max stream storage in bytes (-1 = unlimited)" | default = -1,
|
||
|
|
} in
|
||
|
|
|
||
|
|
let _OpsSubjects = {
|
||
|
|
pending | String | doc "ops.pending.<workspace>.> — unsigned proposals from emitters",
|
||
|
|
cmd | String | doc "ops.cmd.<workspace>.> — signed commands ready to apply",
|
||
|
|
ack | String | doc "ops.ack.<workspace>.> — application result from ops-controller",
|
||
|
|
audit | String | doc "ops.audit.<workspace> — immutable audit stream",
|
||
|
|
} in
|
||
|
|
|
||
|
|
let _OpsStreams = {
|
||
|
|
pending | _StreamConfig | doc "WorkQueue, 14d — buffers unsigned proposals",
|
||
|
|
cmd | _StreamConfig | doc "WorkQueue, 24h — signed commands awaiting application",
|
||
|
|
audit | _StreamConfig | doc "Limits, 90d, replicas=3 — immutable audit record",
|
||
|
|
} in
|
||
|
|
|
||
|
|
# Workspace-level ops contract — embed in workspace infra NCL as `ops_contract`
|
||
|
|
let _OpsWorkspaceConfig = {
|
||
|
|
workspace | String,
|
||
|
|
subjects | _OpsSubjects,
|
||
|
|
streams | _OpsStreams,
|
||
|
|
authorized_signers | Array String | doc "Signer identity keys allowed to sign for this workspace" | default = [],
|
||
|
|
} in
|
||
|
|
|
||
|
|
{
|
||
|
|
OpsType = OpsType,
|
||
|
|
StreamRetention = _StreamRetention,
|
||
|
|
ScopeEntry = _ScopeEntry,
|
||
|
|
JwtClaims = _JwtClaims,
|
||
|
|
StreamConfig = _StreamConfig,
|
||
|
|
OpsSubjects = _OpsSubjects,
|
||
|
|
OpsStreams = _OpsStreams,
|
||
|
|
OpsWorkspaceConfig = _OpsWorkspaceConfig,
|
||
|
|
|
||
|
|
# Constructs a full OpsWorkspaceConfig from a workspace name.
|
||
|
|
# Stream names follow ADR-037 convention: OPS_{STREAM}_{workspace}
|
||
|
|
# (workspace name is used verbatim; uppercase normalisation is ops-controller's concern).
|
||
|
|
make_ops_config | not_exported = fun workspace => {
|
||
|
|
workspace = workspace,
|
||
|
|
subjects = {
|
||
|
|
pending = "ops.pending.%{workspace}.>",
|
||
|
|
cmd = "ops.cmd.%{workspace}.>",
|
||
|
|
ack = "ops.ack.%{workspace}.>",
|
||
|
|
audit = "ops.audit.%{workspace}",
|
||
|
|
},
|
||
|
|
streams = {
|
||
|
|
pending = {
|
||
|
|
name = "OPS_PENDING_%{workspace}",
|
||
|
|
subjects = ["ops.pending.%{workspace}.>"],
|
||
|
|
retention = 'WorkQueue,
|
||
|
|
max_age_s = 1209600,
|
||
|
|
replicas = 1,
|
||
|
|
max_bytes = -1,
|
||
|
|
},
|
||
|
|
cmd = {
|
||
|
|
name = "OPS_CMD_%{workspace}",
|
||
|
|
subjects = ["ops.cmd.%{workspace}.>"],
|
||
|
|
retention = 'WorkQueue,
|
||
|
|
max_age_s = 86400,
|
||
|
|
replicas = 1,
|
||
|
|
max_bytes = -1,
|
||
|
|
},
|
||
|
|
audit = {
|
||
|
|
name = "OPS_AUDIT_%{workspace}",
|
||
|
|
subjects = ["ops.audit.%{workspace}"],
|
||
|
|
retention = 'Limits,
|
||
|
|
max_age_s = 7776000,
|
||
|
|
replicas = 3,
|
||
|
|
max_bytes = -1,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
authorized_signers = [],
|
||
|
|
},
|
||
|
|
}
|