feat(deploy): ship the canon classification vocabulary with the release
The API validates repo classification against the-custodian canon allowed values. A container has no such checkout, so every classification write failed with a 500 and classification could only ever be written from a workstation. Mounts the vocabulary as a ConfigMap and points REPO_CLASSIFICATION_ALLOWED_PATH at it. The copy is the risk, so it is owned rather than trusted: scripts/sync_classification_allowed.py regenerates it from canon and --check fails on drift. make check-classification-allowed and make railiance-state-hub-render both refuse to proceed when the copy diverges, so a release cannot silently validate against a stale vocabulary. The container volumeMounts and env blocks are merged rather than appended — a second pair would have produced duplicate YAML keys as soon as sweep was re-enabled. Refs CUST-WP-0067-T09 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 2583210@bnt-lap001 Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
This commit is contained in:
parent
05dda8d276
commit
ac21accd7a
6 changed files with 270 additions and 2 deletions
12
Makefile
12
Makefile
|
|
@ -71,7 +71,17 @@ dashboard:
|
|||
check:
|
||||
curl -sf http://127.0.0.1:8000/state/health | python3 -m json.tool
|
||||
|
||||
railiance-state-hub-render:
|
||||
# CUST-WP-0067-T09. The chart ships a copy of the-custodian canon allowed-values
|
||||
# because a container has no checkout to read it from. Regenerate after any
|
||||
# canon vocabulary change; the -check target fails on drift so a release cannot
|
||||
# validate classification against a stale vocabulary.
|
||||
sync-classification-allowed:
|
||||
python3 scripts/sync_classification_allowed.py
|
||||
|
||||
check-classification-allowed:
|
||||
python3 scripts/sync_classification_allowed.py --check
|
||||
|
||||
railiance-state-hub-render: check-classification-allowed
|
||||
$(HELM) template $(RAILIANCE_STATE_HUB_RELEASE) $(RAILIANCE_STATE_HUB_CHART) \
|
||||
--namespace $(RAILIANCE_STATE_HUB_NAMESPACE) \
|
||||
-f $(RAILIANCE_STATE_HUB_VALUES) \
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
# GENERATED — do not edit.
|
||||
# Synced from the-custodian/canon/standards/repo-classification.allowed.yaml
|
||||
# by scripts/sync_classification_allowed.py (CUST-WP-0067-T09).
|
||||
# Canon is authoritative; this copy exists only so the container has one.
|
||||
# Machine-readable allowed-values for the Repo Classification Standard.
|
||||
#
|
||||
# Single source of truth for the standard's controlled vocabularies, derived
|
||||
# from canon/standards/repo-classification-standard_v1.0.md. Consumed by:
|
||||
# - the per-repo .repo-classification.yaml linter (tools/validate_repo_classification.py)
|
||||
# - the State Hub registration validator (CUST-WP-0050 T04)
|
||||
#
|
||||
# When the standard's vocabularies change, update this file and bump `version`
|
||||
# to match the standard version. CUST-WP-0050 T01.
|
||||
|
||||
standard: "Repo Classification Standard"
|
||||
version: "1.0"
|
||||
canon_id: "canon-repo-classification"
|
||||
|
||||
# category — exactly 1 required (§5)
|
||||
categories:
|
||||
- experimental
|
||||
- research
|
||||
- project
|
||||
- tooling
|
||||
- product
|
||||
- business
|
||||
|
||||
# domain / secondary_domains — primary exactly 1; secondaries 0..n (§6)
|
||||
domains:
|
||||
- infotech
|
||||
- financials
|
||||
- communication
|
||||
- consumer
|
||||
- health
|
||||
- industrials
|
||||
- energy
|
||||
- utilities
|
||||
- materials
|
||||
- realestate
|
||||
- crypto
|
||||
- agents
|
||||
- space
|
||||
- government
|
||||
|
||||
# business_stake — 0..n; 2..6 recommended (§8)
|
||||
business_stake:
|
||||
- execution
|
||||
- intelligence
|
||||
- finance
|
||||
- legal
|
||||
- sales
|
||||
- experience
|
||||
- technology
|
||||
- operations
|
||||
- product
|
||||
- people
|
||||
- procurement
|
||||
- sustainability
|
||||
- automation
|
||||
|
||||
# business_mechanics — 0..n, optional (§9)
|
||||
business_mechanics:
|
||||
- intention
|
||||
- control
|
||||
- coordination
|
||||
- operation
|
||||
- adaptation
|
||||
|
||||
# capability_tags are intentionally OPEN-ENDED (§7): lowercase kebab-case, not
|
||||
# restricted to this set. The families below are the standard's recommended
|
||||
# canonical tags — used to warn on likely synonyms/typos, never to reject.
|
||||
capability_families:
|
||||
identity_and_access:
|
||||
- identity
|
||||
- authentication
|
||||
- authorization
|
||||
- access-control
|
||||
- user-management
|
||||
- tenancy
|
||||
knowledge_and_evidence:
|
||||
- knowledge
|
||||
- citations
|
||||
- evidence
|
||||
- source-management
|
||||
- traceability
|
||||
- documentation
|
||||
- decision-support
|
||||
platform_and_operations:
|
||||
- platform
|
||||
- deployment
|
||||
- operations
|
||||
- observability
|
||||
- feature-control
|
||||
- configuration
|
||||
- orchestration
|
||||
market_and_coordination:
|
||||
- marketplace
|
||||
- pricing
|
||||
- monetization
|
||||
- reputation
|
||||
- challenges
|
||||
- bounties
|
||||
- collaboration
|
||||
- coordination
|
||||
product_and_lifecycle:
|
||||
- product-development
|
||||
- lifecycle
|
||||
governance_and_control:
|
||||
- governance
|
||||
- policy
|
||||
- compliance
|
||||
- risk
|
||||
- audit
|
||||
- control
|
||||
games_and_entertainment:
|
||||
- game
|
||||
- entertainment
|
||||
|
||||
# Validation guidance (advisory bounds the linter applies as warnings)
|
||||
guidance:
|
||||
secondary_domains_max: 3
|
||||
business_stake_recommended_min: 2
|
||||
business_stake_recommended_max: 6
|
||||
capability_tag_pattern: "^[a-z0-9]+(-[a-z0-9]+)*$"
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{{- if .Values.classificationAllowed.enabled }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ .Values.classificationAllowed.name | quote }}
|
||||
labels: {{- include "statehub.labels" . | nindent 4 }}
|
||||
data:
|
||||
# Generated copy of the-custodian canon allowed-values. Kept in sync by
|
||||
# scripts/sync_classification_allowed.py; `--check` fails the release on drift.
|
||||
repo-classification.allowed.yaml: |
|
||||
{{ .Files.Get "files/repo-classification.allowed.yaml" | indent 4 }}
|
||||
{{- end }}
|
||||
|
|
@ -22,8 +22,9 @@ spec:
|
|||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets: {{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if .Values.sweep.enabled }}
|
||||
{{- if or .Values.sweep.enabled .Values.classificationAllowed.enabled }}
|
||||
volumes:
|
||||
{{- if .Values.sweep.enabled }}
|
||||
- name: sweep-repos
|
||||
hostPath:
|
||||
path: {{ .Values.sweep.hostPath | quote }}
|
||||
|
|
@ -32,6 +33,12 @@ spec:
|
|||
hostPath:
|
||||
path: {{ .Values.sweep.sshHostPath | quote }}
|
||||
type: DirectoryOrCreate
|
||||
{{- end }}
|
||||
{{- if .Values.classificationAllowed.enabled }}
|
||||
- name: classification-allowed
|
||||
configMap:
|
||||
name: {{ .Values.classificationAllowed.name | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: state-hub
|
||||
|
|
@ -50,17 +57,32 @@ spec:
|
|||
- /bin/sh
|
||||
- -c
|
||||
- git config --global --add safe.directory '*'
|
||||
{{- end }}
|
||||
{{- if or .Values.sweep.enabled .Values.classificationAllowed.enabled }}
|
||||
volumeMounts:
|
||||
{{- if .Values.sweep.enabled }}
|
||||
- name: sweep-repos
|
||||
mountPath: {{ .Values.sweep.hostPath | quote }}
|
||||
- name: sweep-ssh
|
||||
mountPath: /root/.ssh
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
{{- if .Values.classificationAllowed.enabled }}
|
||||
- name: classification-allowed
|
||||
mountPath: {{ .Values.classificationAllowed.mountPath | quote }}
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
env:
|
||||
{{- if .Values.sweep.enabled }}
|
||||
- name: STATE_HUB_SWEEP_HOSTNAME
|
||||
value: {{ .Values.sweep.hostname | quote }}
|
||||
- name: GIT_SSH_COMMAND
|
||||
value: "ssh -o StrictHostKeyChecking=accept-new -F /root/.ssh/config"
|
||||
{{- end }}
|
||||
{{- if .Values.classificationAllowed.enabled }}
|
||||
- name: REPO_CLASSIFICATION_ALLOWED_PATH
|
||||
value: {{ printf "%s/repo-classification.allowed.yaml" .Values.classificationAllowed.mountPath | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
envFrom:
|
||||
{{- if .Values.config.enabled }}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,15 @@ ingress:
|
|||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
|
||||
# Classification allowed-values (CUST-WP-0067-T09). The API validates repo
|
||||
# classification against the-custodian canon; a container has no such checkout,
|
||||
# so the file travels with the release as a ConfigMap. Without it every
|
||||
# classification write fails with a 500.
|
||||
classificationAllowed:
|
||||
enabled: true
|
||||
name: state-hub-classification-allowed
|
||||
mountPath: /etc/state-hub/classification
|
||||
|
||||
# MCP layer (CUST-WP-0067-T08). Runs from the same image with a different
|
||||
# command, as a stateless HTTP client over the API service. ClusterIP only —
|
||||
# it proxies an unauthenticated API, so it must never gain an Ingress.
|
||||
|
|
|
|||
91
scripts/sync_classification_allowed.py
Executable file
91
scripts/sync_classification_allowed.py
Executable file
|
|
@ -0,0 +1,91 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Sync the canon classification allowed-values into the chart (CUST-WP-0067-T09).
|
||||
|
||||
The State Hub validates repository classification against
|
||||
``the-custodian/canon/standards/repo-classification.allowed.yaml``. That file is
|
||||
canon and must stay authoritative, but a container has no the-custodian
|
||||
checkout, so a copy has to travel with the release.
|
||||
|
||||
A copy that nobody checks is a copy that silently drifts. This script owns the
|
||||
copy: ``--check`` fails when it diverges from canon, so the release can refuse
|
||||
to ship a stale vocabulary rather than validate against yesterday's rules.
|
||||
|
||||
Usage:
|
||||
python scripts/sync_classification_allowed.py # write the copy
|
||||
python scripts/sync_classification_allowed.py --check # verify, exit 1 on drift
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import difflib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
CHART_COPY = (
|
||||
REPO_ROOT
|
||||
/ "deploy/railiance/apps/charts/state-hub/files/repo-classification.allowed.yaml"
|
||||
)
|
||||
CANON_CANDIDATES = (
|
||||
Path("/home/worsch/the-custodian/canon/standards/repo-classification.allowed.yaml"),
|
||||
Path("/home/tegwick/the-custodian/canon/standards/repo-classification.allowed.yaml"),
|
||||
REPO_ROOT.parent / "the-custodian/canon/standards/repo-classification.allowed.yaml",
|
||||
)
|
||||
|
||||
HEADER = (
|
||||
"# GENERATED — do not edit.\n"
|
||||
"# Synced from the-custodian/canon/standards/repo-classification.allowed.yaml\n"
|
||||
"# by scripts/sync_classification_allowed.py (CUST-WP-0067-T09).\n"
|
||||
"# Canon is authoritative; this copy exists only so the container has one.\n"
|
||||
)
|
||||
|
||||
|
||||
def find_canon() -> Path:
|
||||
for candidate in CANON_CANDIDATES:
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
raise SystemExit(
|
||||
"ERROR: canon allowed-values not found. Looked in:\n "
|
||||
+ "\n ".join(str(c) for c in CANON_CANDIDATES)
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--check",
|
||||
action="store_true",
|
||||
help="Verify the chart copy matches canon; exit 1 on drift",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
canon = find_canon()
|
||||
desired = HEADER + canon.read_text(encoding="utf-8")
|
||||
|
||||
if args.check:
|
||||
if not CHART_COPY.is_file():
|
||||
print(f"DRIFT: {CHART_COPY} is missing; run without --check to create it")
|
||||
return 1
|
||||
current = CHART_COPY.read_text(encoding="utf-8")
|
||||
if current != desired:
|
||||
print(f"DRIFT: chart copy differs from canon ({canon})")
|
||||
sys.stdout.writelines(
|
||||
difflib.unified_diff(
|
||||
current.splitlines(keepends=True),
|
||||
desired.splitlines(keepends=True),
|
||||
fromfile="chart copy",
|
||||
tofile="canon",
|
||||
)
|
||||
)
|
||||
return 1
|
||||
print(f"OK: chart copy matches canon ({canon})")
|
||||
return 0
|
||||
|
||||
CHART_COPY.parent.mkdir(parents=True, exist_ok=True)
|
||||
CHART_COPY.write_text(desired, encoding="utf-8")
|
||||
print(f"Synced {canon} -> {CHART_COPY}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue