Version the common and derived rail contract
This commit is contained in:
parent
fee395f0c4
commit
3ece1b55fb
8 changed files with 407 additions and 2 deletions
|
|
@ -9,7 +9,12 @@
|
|||
| Kind | ID | Status | Lane | Source |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| workplan | RAIL-K8S-WP-0001 | finished | — | workplans/RAIL-K8S-WP-0001-bootstrap-and-wave1-import.md |
|
||||
| workplan | RAIL-K8S-WP-0002 | finished | — | workplans/RAIL-K8S-WP-0002-versioned-common-and-derived-rail-contract.md |
|
||||
| task | RAIL-K8S-WP-0001-T01 | done | — | workplans/RAIL-K8S-WP-0001-bootstrap-and-wave1-import.md |
|
||||
| task | RAIL-K8S-WP-0001-T02 | done | — | workplans/RAIL-K8S-WP-0001-bootstrap-and-wave1-import.md |
|
||||
| task | RAIL-K8S-WP-0001-T03 | done | — | workplans/RAIL-K8S-WP-0001-bootstrap-and-wave1-import.md |
|
||||
| task | RAIL-K8S-WP-0001-T04 | done | — | workplans/RAIL-K8S-WP-0001-bootstrap-and-wave1-import.md |
|
||||
| task | RAIL-K8S-WP-0002-T01 | done | — | workplans/RAIL-K8S-WP-0002-versioned-common-and-derived-rail-contract.md |
|
||||
| task | RAIL-K8S-WP-0002-T02 | done | — | workplans/RAIL-K8S-WP-0002-versioned-common-and-derived-rail-contract.md |
|
||||
| task | RAIL-K8S-WP-0002-T03 | done | — | workplans/RAIL-K8S-WP-0002-versioned-common-and-derived-rail-contract.md |
|
||||
| task | RAIL-K8S-WP-0002-T04 | done | — | workplans/RAIL-K8S-WP-0002-versioned-common-and-derived-rail-contract.md |
|
||||
|
|
|
|||
|
|
@ -2,17 +2,22 @@ kind: execution-rail
|
|||
rail_id: rail-kubernetes
|
||||
repo: rail-kubernetes
|
||||
ownership_repo: railiance-cluster
|
||||
contract_version: 1.0.0
|
||||
composition_kind: base
|
||||
execution_architecture: kubernetes
|
||||
status: wave-1-bootstrap
|
||||
readiness_state: verified
|
||||
supported_rollout_modes:
|
||||
- stage1-run
|
||||
- stage2-isolated-canary
|
||||
- stage3-promote-rollback
|
||||
substrate_prerequisites:
|
||||
required_substrate_capabilities:
|
||||
- kubernetes-api
|
||||
- helm-apply-path
|
||||
- operator-driven-kubeconfig-access
|
||||
- namespace-creation-supported
|
||||
default_for:
|
||||
- platform-workloads
|
||||
- managed-applications
|
||||
compatibility_notes:
|
||||
- Weighted routing is optional and must not be assumed in wave 1.
|
||||
- Metrics availability is opportunistic and not a rollout success criterion.
|
||||
|
|
|
|||
30
examples/common-workload.json
Normal file
30
examples/common-workload.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"contract_version": "1.0.0",
|
||||
"workload_id": "example-platform-service",
|
||||
"ownership_repo": "railiance-platform",
|
||||
"criticality": "medium",
|
||||
"data_classification": "internal",
|
||||
"dependencies": ["kubernetes-api"],
|
||||
"identity": {
|
||||
"principal": "example-platform-service",
|
||||
"secret_references": []
|
||||
},
|
||||
"health": {
|
||||
"readiness": "HTTP readiness probe succeeds",
|
||||
"smoke": "Authenticated package smoke succeeds"
|
||||
},
|
||||
"recovery": {
|
||||
"rollback": "Restore the previous verified Kubernetes release"
|
||||
},
|
||||
"rail_bindings": [
|
||||
{
|
||||
"rail_id": "rail-kubernetes",
|
||||
"rail_contract": "1.0",
|
||||
"readiness_state": "declared",
|
||||
"required_substrate_capabilities": ["kubernetes-api"],
|
||||
"deployment_artifacts": ["railiance/app.toml"],
|
||||
"rollout": {"mode": "stage2-isolated-canary"},
|
||||
"rollback": {"mode": "previous-release"}
|
||||
}
|
||||
]
|
||||
}
|
||||
73
schemas/common-workload.schema.json
Normal file
73
schemas/common-workload.schema.json
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://railiance.dev/schemas/common-workload-v1.schema.json",
|
||||
"title": "Railiance common managed workload contract",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"contract_version",
|
||||
"workload_id",
|
||||
"ownership_repo",
|
||||
"criticality",
|
||||
"data_classification",
|
||||
"dependencies",
|
||||
"identity",
|
||||
"health",
|
||||
"recovery",
|
||||
"rail_bindings"
|
||||
],
|
||||
"properties": {
|
||||
"contract_version": {"const": "1.0.0"},
|
||||
"workload_id": {"type": "string", "pattern": "^[a-z][a-z0-9-]+$"},
|
||||
"ownership_repo": {"type": "string", "pattern": "^[a-z][a-z0-9-]+$"},
|
||||
"criticality": {
|
||||
"enum": ["low", "medium", "high", "critical"]
|
||||
},
|
||||
"data_classification": {
|
||||
"enum": ["public", "internal", "confidential", "restricted"]
|
||||
},
|
||||
"dependencies": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"identity": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["principal", "secret_references"],
|
||||
"properties": {
|
||||
"principal": {"type": "string", "minLength": 1},
|
||||
"tenant": {"type": "string", "minLength": 1},
|
||||
"secret_references": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"health": {
|
||||
"type": "object",
|
||||
"required": ["readiness", "smoke"],
|
||||
"properties": {
|
||||
"readiness": {"type": "string", "minLength": 1},
|
||||
"smoke": {"type": "string", "minLength": 1},
|
||||
"audit": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"recovery": {
|
||||
"type": "object",
|
||||
"required": ["rollback"],
|
||||
"properties": {
|
||||
"rollback": {"type": "string", "minLength": 1},
|
||||
"rto": {"type": "string"},
|
||||
"rpo": {"type": "string"}
|
||||
}
|
||||
},
|
||||
"rail_bindings": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {"$ref": "rail-binding-v1.schema.json"},
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
}
|
||||
54
schemas/derived-rail-v1.schema.json
Normal file
54
schemas/derived-rail-v1.schema.json
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://railiance.dev/schemas/derived-rail-v1.schema.json",
|
||||
"title": "Railiance derived rail declaration",
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"required": [
|
||||
"kind",
|
||||
"rail_id",
|
||||
"ownership_repo",
|
||||
"contract_version",
|
||||
"composition_kind",
|
||||
"base_rail",
|
||||
"base_rail_contract",
|
||||
"execution_architecture",
|
||||
"required_substrate_capabilities",
|
||||
"inherited_semantics",
|
||||
"overridden_semantics",
|
||||
"compatibility_constraints",
|
||||
"readiness_state"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {"const": "execution-rail"},
|
||||
"rail_id": {"type": "string", "pattern": "^rail-[a-z0-9-]+$"},
|
||||
"ownership_repo": {"type": "string"},
|
||||
"contract_version": {"type": "string"},
|
||||
"composition_kind": {"const": "derived"},
|
||||
"base_rail": {"const": "rail-kubernetes"},
|
||||
"base_rail_contract": {"const": "1.0.0"},
|
||||
"execution_architecture": {"type": "string"},
|
||||
"required_substrate_capabilities": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"inherited_semantics": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"overridden_semantics": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"compatibility_constraints": {"type": "array", "items": {"type": "string"}},
|
||||
"readiness_state": {
|
||||
"enum": ["declared", "installed", "verified", "production-approved", "deprecated"]
|
||||
}
|
||||
}
|
||||
}
|
||||
37
schemas/rail-binding-v1.schema.json
Normal file
37
schemas/rail-binding-v1.schema.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://railiance.dev/schemas/rail-binding-v1.schema.json",
|
||||
"title": "Railiance workload rail binding",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"rail_id",
|
||||
"rail_contract",
|
||||
"readiness_state",
|
||||
"required_substrate_capabilities",
|
||||
"deployment_artifacts",
|
||||
"rollout",
|
||||
"rollback"
|
||||
],
|
||||
"properties": {
|
||||
"rail_id": {"type": "string", "pattern": "^rail-[a-z0-9-]+$"},
|
||||
"rail_contract": {"type": "string", "pattern": "^[0-9]+\\.[0-9]+(\\.[0-9]+)?$"},
|
||||
"readiness_state": {
|
||||
"enum": ["declared", "installed", "verified", "production-approved", "deprecated"]
|
||||
},
|
||||
"required_substrate_capabilities": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"uniqueItems": true
|
||||
},
|
||||
"deployment_artifacts": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {"type": "string"}
|
||||
},
|
||||
"scaling": {"type": "object"},
|
||||
"routing": {"type": "object"},
|
||||
"rollout": {"type": "object"},
|
||||
"rollback": {"type": "object"}
|
||||
}
|
||||
}
|
||||
118
tools/validate_contracts.py
Normal file
118
tools/validate_contracts.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Validate Railiance rail and workload contracts without reading credentials."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from jsonschema import Draft202012Validator, RefResolver
|
||||
|
||||
|
||||
READINESS_STATES = {
|
||||
"declared",
|
||||
"installed",
|
||||
"verified",
|
||||
"production-approved",
|
||||
"deprecated",
|
||||
}
|
||||
|
||||
|
||||
def load(path: Path) -> Any:
|
||||
with path.open(encoding="utf-8") as handle:
|
||||
if path.suffix in {".yaml", ".yml"}:
|
||||
return yaml.safe_load(handle)
|
||||
return json.load(handle)
|
||||
|
||||
|
||||
def validate_schema(instance_path: Path, schema_path: Path) -> list[str]:
|
||||
instance = load(instance_path)
|
||||
schema = load(schema_path)
|
||||
store: dict[str, Any] = {}
|
||||
for candidate in schema_path.parent.glob("*.json"):
|
||||
candidate_schema = load(candidate)
|
||||
store[candidate.resolve().as_uri()] = candidate_schema
|
||||
if schema_id := candidate_schema.get("$id"):
|
||||
store[schema_id] = candidate_schema
|
||||
resolver = RefResolver(
|
||||
base_uri=schema_path.parent.resolve().as_uri() + "/",
|
||||
referrer=schema,
|
||||
store=store,
|
||||
)
|
||||
validator = Draft202012Validator(schema, resolver=resolver)
|
||||
return [
|
||||
f"{instance_path}: {'/'.join(str(part) for part in error.path) or '<root>'}: {error.message}"
|
||||
for error in sorted(validator.iter_errors(instance), key=lambda item: list(item.path))
|
||||
]
|
||||
|
||||
|
||||
def validate_rail(path: Path) -> list[str]:
|
||||
data = load(path)
|
||||
errors: list[str] = []
|
||||
required = {
|
||||
"rail_id",
|
||||
"ownership_repo",
|
||||
"contract_version",
|
||||
"composition_kind",
|
||||
"execution_architecture",
|
||||
"required_substrate_capabilities",
|
||||
"readiness_state",
|
||||
}
|
||||
for field in sorted(required - set(data or {})):
|
||||
errors.append(f"{path}: missing required field {field}")
|
||||
if not isinstance(data, dict):
|
||||
return [f"{path}: declaration must be an object"]
|
||||
if data.get("readiness_state") not in READINESS_STATES:
|
||||
errors.append(f"{path}: invalid readiness_state {data.get('readiness_state')!r}")
|
||||
kind = data.get("composition_kind")
|
||||
if kind not in {"base", "derived"}:
|
||||
errors.append(f"{path}: composition_kind must be base or derived")
|
||||
if kind == "derived":
|
||||
for field in (
|
||||
"base_rail",
|
||||
"base_rail_contract",
|
||||
"inherited_semantics",
|
||||
"overridden_semantics",
|
||||
"compatibility_constraints",
|
||||
):
|
||||
if field not in data:
|
||||
errors.append(f"{path}: derived rail missing {field}")
|
||||
if data.get("base_rail") == data.get("rail_id"):
|
||||
errors.append(f"{path}: derived rail cannot inherit from itself")
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--rail", action="append", default=[], type=Path)
|
||||
parser.add_argument("--workload", action="append", default=[], type=Path)
|
||||
parser.add_argument(
|
||||
"--schema-dir",
|
||||
type=Path,
|
||||
default=Path(__file__).resolve().parents[1] / "schemas",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
errors: list[str] = []
|
||||
for path in args.rail:
|
||||
errors.extend(validate_rail(path))
|
||||
for path in args.workload:
|
||||
errors.extend(
|
||||
validate_schema(path, args.schema_dir / "common-workload.schema.json")
|
||||
)
|
||||
if errors:
|
||||
print("\n".join(errors), file=sys.stderr)
|
||||
return 1
|
||||
print(
|
||||
f"validated {len(args.rail)} rail declaration(s) and "
|
||||
f"{len(args.workload)} workload declaration(s)"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
---
|
||||
id: RAIL-K8S-WP-0002
|
||||
type: workplan
|
||||
title: "Version the common workload and derived-rail contract"
|
||||
domain: financials
|
||||
repo: rail-kubernetes
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
created: "2026-07-26"
|
||||
updated: "2026-07-26"
|
||||
state_hub_workstream_id: "028b0456-3e54-4539-b563-0eec012c3ecb"
|
||||
---
|
||||
|
||||
# RAIL-K8S-WP-0002 - Version the common workload and derived-rail contract
|
||||
|
||||
## Goal
|
||||
|
||||
Make `rail-kubernetes` the explicit versioned base contract used by default for
|
||||
platform workloads and inherited by justified specializations such as
|
||||
`rail-knative`.
|
||||
|
||||
## T01 - Version the base rail declaration
|
||||
|
||||
```task
|
||||
id: RAIL-K8S-WP-0002-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "3a2c7e33-f1d4-42dd-b45a-08a5835a22cf"
|
||||
```
|
||||
|
||||
Add composition kind, contract version, required capabilities, and readiness
|
||||
state without changing current lifecycle behavior.
|
||||
|
||||
2026-07-26: `declarations/rail.yaml` now declares base composition, contract
|
||||
`1.0.0`, required substrate capabilities, verified readiness, and the default
|
||||
platform/managed-application scope.
|
||||
|
||||
## T02 - Add the rail-neutral workload and binding schemas
|
||||
|
||||
```task
|
||||
id: RAIL-K8S-WP-0002-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "61e92217-b7d9-4a53-ae28-1c60eb6a201e"
|
||||
```
|
||||
|
||||
Separate common workload identity, criticality, dependencies, security,
|
||||
health, evidence, and recovery requirements from rail-specific bindings.
|
||||
|
||||
2026-07-26: Added the common workload, rail-binding, and derived-rail JSON
|
||||
schemas plus a valid Kubernetes-default example.
|
||||
|
||||
## T03 - Add compatibility conformance
|
||||
|
||||
```task
|
||||
id: RAIL-K8S-WP-0002-T03
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "06e1aa1f-2111-4be5-9b4b-28fd440eb7c3"
|
||||
```
|
||||
|
||||
Provide an idempotent validator suitable for local, agent, CI, and scheduled
|
||||
execution. Reject incompatible base contracts, missing ownership, and
|
||||
unsupported substrate capabilities.
|
||||
|
||||
2026-07-26: Added `tools/validate_contracts.py`. It validates base/derived rail
|
||||
shape and common workload JSON Schema without accessing runtime credentials.
|
||||
|
||||
## T04 - Preserve the Kubernetes default
|
||||
|
||||
```task
|
||||
id: RAIL-K8S-WP-0002-T04
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "54793f28-a417-43c4-9a02-aa55199fca9f"
|
||||
```
|
||||
|
||||
Document and test that workloads without a justified specialization continue
|
||||
to bind to `rail-kubernetes`.
|
||||
|
||||
2026-07-26: The base declaration identifies Kubernetes as the default for
|
||||
platform workloads and managed applications; the example uses that binding.
|
||||
Loading…
Add table
Add a link
Reference in a new issue