Prove pinned OpenBao ArgoCD adoption with deterministic native checks
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
This commit is contained in:
parent
5a1a7ee5c1
commit
725d126c77
19 changed files with 1130 additions and 2 deletions
34
docs/evidence/2026-10-11-openbao-adoption-proof/README.md
Normal file
34
docs/evidence/2026-10-11-openbao-adoption-proof/README.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Verified OpenBao Argo adoption proof
|
||||
|
||||
Run from the workstation:
|
||||
|
||||
```sh
|
||||
python3 scripts/argocd_adoption_proof.py --live --source-root . --observation observation.json --receipt proof.json
|
||||
python3 -m unittest discover -s tests -p 'test_argocd_adoption_proof.py'
|
||||
```
|
||||
|
||||
The helper reads nodes, the exact Application, ClusterSecretStore, ExternalSecret,
|
||||
Argo configuration and controller version. It never reads a Secret. Raw command
|
||||
output and errors are captured and discarded. Git archive renders the exact
|
||||
pin in a disposable directory; only a public ClusterSecretStore is admitted.
|
||||
Server API dry-runs and native diffs make no persistent native change.
|
||||
|
||||
The final proof is verified: both native diffs return zero and both same-version
|
||||
API dry-runs have no changed field paths after exact Argo tracking injection.
|
||||
The observed 3.5.3 controller and configuration select annotation mode. Tracking
|
||||
uses the Application destination namespace even for a cluster-scoped object;
|
||||
no instance label is injected. This follows the upstream [v3.5.3 resource tracking
|
||||
implementation](https://github.com/argoproj/argo-cd/blob/v3.5.3/util/argo/resource_tracking.go)
|
||||
and the [3.0 default change](https://argo-cd.readthedocs.io/en/release-3.5/operator-manual/upgrading/2.14-3.0/).
|
||||
Only the tracking annotation is injected; live fields are never copied into the
|
||||
desired source. Raw source diffs remain diagnostic. Structured JSON pointers
|
||||
identify only tracking/last-applied differences. Unknown or substantive fields
|
||||
refuse proof; neither parser ambiguity nor a manual ignore can grant success.
|
||||
|
||||
Check `status: verified`, never receipt presence. Five-minute stale and future
|
||||
observations are refused. The successful observation is a point-in-time proof,
|
||||
not authority or evidence of subsequent automation. Earlier failed receipts are
|
||||
retained. Thirteen fixture tests include exact identity/pin/destination mismatch,
|
||||
tracking/render semantics, proving age, readiness changes, extra tracked objects,
|
||||
unknown/substantive/native diff failures and protected-error-output canaries.
|
||||
Fixtures are synthetic; native acceptance is the separate final proof.
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"status": "verified-diagnostic",
|
||||
"normalized_client_dry_run_changed_paths": [],
|
||||
"same_version_read": true,
|
||||
"diffs": {
|
||||
"client": 0,
|
||||
"server": 0,
|
||||
"argo_manager_server": 0,
|
||||
"argo_manager_client": 0
|
||||
},
|
||||
"manager_kubectl_client_side_apply": false,
|
||||
"manager_argocd_controller": false,
|
||||
"manager_argocd_application_controller": false
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"spec_equals_pin": true,
|
||||
"desired_labels_present": true,
|
||||
"tracking_empty_namespace": false,
|
||||
"tracking_default_namespace": false,
|
||||
"tracking_argocd_namespace": false,
|
||||
"tracking_identity_prefix_matches": true,
|
||||
"tracking_name_matches": true
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"status": "verified-diagnostic",
|
||||
"client_dry_run_changed_paths": [
|
||||
"/apiVersion",
|
||||
"/metadata/annotations/argocd.argoproj.io~1tracking-id",
|
||||
"/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration"
|
||||
],
|
||||
"server_dry_run_changed_paths": [
|
||||
"/apiVersion",
|
||||
"/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
{
|
||||
"application": {
|
||||
"automated_present": false,
|
||||
"condition_types": [
|
||||
"OrphanedResourceWarning"
|
||||
],
|
||||
"destination": {
|
||||
"namespace": "external-secrets",
|
||||
"server": "https://kubernetes.default.svc"
|
||||
},
|
||||
"health": "Healthy",
|
||||
"name": "openbao-secretstore",
|
||||
"operation": {
|
||||
"finishedAt": "2026-09-21T17:09:18Z",
|
||||
"phase": "Succeeded",
|
||||
"startedAt": "2026-09-21T17:09:16Z"
|
||||
},
|
||||
"operation_revision": "d2dbc19c254247652c49fda8721c80d53bca206a",
|
||||
"pending_operation": false,
|
||||
"project": "railiance-platform-addons",
|
||||
"resources": [
|
||||
{
|
||||
"group": "external-secrets.io",
|
||||
"kind": "ClusterSecretStore",
|
||||
"name": "openbao",
|
||||
"namespace": null,
|
||||
"status": "Synced"
|
||||
}
|
||||
],
|
||||
"revision": "d2dbc19c254247652c49fda8721c80d53bca206a",
|
||||
"source": {
|
||||
"path": "argocd/platform-addons/openbao-secretstore",
|
||||
"repoURL": "https://forgejo.coulomb.social/coulomb/railiance-platform.git",
|
||||
"targetRevision": "d2dbc19c254247652c49fda8721c80d53bca206a"
|
||||
},
|
||||
"sync": "Synced",
|
||||
"sync_prune": false,
|
||||
"sync_strategy_apply": true,
|
||||
"uid": "f2932505-e649-4b99-b505-4789ed21301f"
|
||||
},
|
||||
"captured_at": "2026-10-11T19:22:22.892821+00:00",
|
||||
"estate_ip": "92.205.62.239",
|
||||
"ready_checks": [
|
||||
{
|
||||
"kind": "ClusterSecretStore",
|
||||
"name": "openbao",
|
||||
"namespace": "",
|
||||
"ready": [
|
||||
{
|
||||
"lastTransitionTime": "2026-08-19T19:49:59Z",
|
||||
"status": "True"
|
||||
}
|
||||
],
|
||||
"tracking_matches": true,
|
||||
"uid": "99563b77-f30f-451b-ac86-16b1b162c6a9"
|
||||
},
|
||||
{
|
||||
"kind": "ExternalSecret",
|
||||
"name": "issue-core-runtime",
|
||||
"namespace": "issue-core",
|
||||
"ready": [
|
||||
{
|
||||
"lastTransitionTime": "2026-08-19T19:54:20Z",
|
||||
"status": "True"
|
||||
}
|
||||
],
|
||||
"tracking_matches": null,
|
||||
"uid": "490dca37-6364-480a-a757-c3a266d32e47"
|
||||
}
|
||||
],
|
||||
"tracking_settings": {
|
||||
"controller_version": "v3.5.3",
|
||||
"installation_id_absent": true,
|
||||
"instance_label_key": "app.kubernetes.io/instance",
|
||||
"method": "annotation"
|
||||
},
|
||||
"pinned_render_diff": {
|
||||
"client_side_exit_code": 0,
|
||||
"exit_code": 0,
|
||||
"normalization": "argocd-v3-annotation-tracking-destination-namespace",
|
||||
"normalized_client_changed_paths": [],
|
||||
"normalized_render_sha256": "94e014dd61b81e09c8b137acae1b797f8976b8607fcaa46f54674d4d407b9843",
|
||||
"normalized_server_changed_paths": [],
|
||||
"output_suppressed": true,
|
||||
"path": "argocd/platform-addons/openbao-secretstore",
|
||||
"raw_client_changed_paths": [
|
||||
"/metadata/annotations/argocd.argoproj.io~1tracking-id",
|
||||
"/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration"
|
||||
],
|
||||
"raw_client_exit_code": 1,
|
||||
"raw_server_changed_paths": [
|
||||
"/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration"
|
||||
],
|
||||
"raw_server_exit_code": 1,
|
||||
"render_resource_count": 1,
|
||||
"render_sha256": "35585d1b005272ec75e55cb4a68386bc515032693c2c70f6e76f68cad4e29fb3",
|
||||
"revision": "d2dbc19c254247652c49fda8721c80d53bca206a",
|
||||
"same_api_version": true,
|
||||
"tracking_settings": {
|
||||
"controller_version": "v3.5.3",
|
||||
"installation_id_absent": true,
|
||||
"instance_label_key": "app.kubernetes.io/instance",
|
||||
"method": "annotation"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
{
|
||||
"application": {
|
||||
"automated_present": false,
|
||||
"condition_types": [
|
||||
"OrphanedResourceWarning"
|
||||
],
|
||||
"destination": {
|
||||
"namespace": "external-secrets",
|
||||
"server": "https://kubernetes.default.svc"
|
||||
},
|
||||
"health": "Healthy",
|
||||
"name": "openbao-secretstore",
|
||||
"operation": {
|
||||
"finishedAt": "2026-09-21T17:09:18Z",
|
||||
"phase": "Succeeded",
|
||||
"startedAt": "2026-09-21T17:09:16Z"
|
||||
},
|
||||
"operation_revision": "d2dbc19c254247652c49fda8721c80d53bca206a",
|
||||
"pending_operation": false,
|
||||
"project": "railiance-platform-addons",
|
||||
"resources": [
|
||||
{
|
||||
"group": "external-secrets.io",
|
||||
"kind": "ClusterSecretStore",
|
||||
"name": "openbao",
|
||||
"namespace": null,
|
||||
"status": "Synced"
|
||||
}
|
||||
],
|
||||
"revision": "d2dbc19c254247652c49fda8721c80d53bca206a",
|
||||
"source": {
|
||||
"path": "argocd/platform-addons/openbao-secretstore",
|
||||
"repoURL": "https://forgejo.coulomb.social/coulomb/railiance-platform.git",
|
||||
"targetRevision": "d2dbc19c254247652c49fda8721c80d53bca206a"
|
||||
},
|
||||
"sync": "Synced",
|
||||
"sync_prune": false,
|
||||
"sync_strategy_apply": true,
|
||||
"uid": "f2932505-e649-4b99-b505-4789ed21301f"
|
||||
},
|
||||
"captured_at": "2026-10-11T19:16:28.318163+00:00",
|
||||
"estate_ip": "92.205.62.239",
|
||||
"ready_checks": [
|
||||
{
|
||||
"kind": "ClusterSecretStore",
|
||||
"name": "openbao",
|
||||
"namespace": "",
|
||||
"ready": [
|
||||
{
|
||||
"lastTransitionTime": "2026-08-19T19:49:59Z",
|
||||
"status": "True"
|
||||
}
|
||||
],
|
||||
"tracking_matches": true,
|
||||
"uid": "99563b77-f30f-451b-ac86-16b1b162c6a9"
|
||||
},
|
||||
{
|
||||
"kind": "ExternalSecret",
|
||||
"name": "issue-core-runtime",
|
||||
"namespace": "issue-core",
|
||||
"ready": [
|
||||
{
|
||||
"lastTransitionTime": "2026-08-19T19:54:20Z",
|
||||
"status": "True"
|
||||
}
|
||||
],
|
||||
"tracking_matches": null,
|
||||
"uid": "490dca37-6364-480a-a757-c3a266d32e47"
|
||||
}
|
||||
],
|
||||
"pinned_render_diff": {
|
||||
"revision": "d2dbc19c254247652c49fda8721c80d53bca206a",
|
||||
"path": "argocd/platform-addons/openbao-secretstore",
|
||||
"render_sha256": "35585d1b005272ec75e55cb4a68386bc515032693c2c70f6e76f68cad4e29fb3",
|
||||
"render_resource_count": 1,
|
||||
"exit_code": 1,
|
||||
"output_suppressed": true,
|
||||
"changed_top_fields": [
|
||||
"unknown"
|
||||
],
|
||||
"client_side_exit_code": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
{
|
||||
"application": {
|
||||
"automated_present": false,
|
||||
"condition_types": [
|
||||
"OrphanedResourceWarning"
|
||||
],
|
||||
"destination": {
|
||||
"namespace": "external-secrets",
|
||||
"server": "https://kubernetes.default.svc"
|
||||
},
|
||||
"health": "Healthy",
|
||||
"name": "openbao-secretstore",
|
||||
"operation": {
|
||||
"finishedAt": "2026-09-21T17:09:18Z",
|
||||
"phase": "Succeeded",
|
||||
"startedAt": "2026-09-21T17:09:16Z"
|
||||
},
|
||||
"operation_revision": "d2dbc19c254247652c49fda8721c80d53bca206a",
|
||||
"pending_operation": false,
|
||||
"project": "railiance-platform-addons",
|
||||
"resources": [
|
||||
{
|
||||
"group": "external-secrets.io",
|
||||
"kind": "ClusterSecretStore",
|
||||
"name": "openbao",
|
||||
"namespace": null,
|
||||
"status": "Synced"
|
||||
}
|
||||
],
|
||||
"revision": "d2dbc19c254247652c49fda8721c80d53bca206a",
|
||||
"source": {
|
||||
"path": "argocd/platform-addons/openbao-secretstore",
|
||||
"repoURL": "https://forgejo.coulomb.social/coulomb/railiance-platform.git",
|
||||
"targetRevision": "d2dbc19c254247652c49fda8721c80d53bca206a"
|
||||
},
|
||||
"sync": "Synced",
|
||||
"sync_prune": false,
|
||||
"sync_strategy_apply": true,
|
||||
"uid": "f2932505-e649-4b99-b505-4789ed21301f"
|
||||
},
|
||||
"captured_at": "2026-10-11T19:15:47.973008+00:00",
|
||||
"estate_ip": "92.205.62.239",
|
||||
"ready_checks": [
|
||||
{
|
||||
"kind": "ClusterSecretStore",
|
||||
"name": "openbao",
|
||||
"namespace": "",
|
||||
"ready": [
|
||||
{
|
||||
"lastTransitionTime": "2026-08-19T19:49:59Z",
|
||||
"status": "True"
|
||||
}
|
||||
],
|
||||
"tracking_matches": true,
|
||||
"uid": "99563b77-f30f-451b-ac86-16b1b162c6a9"
|
||||
},
|
||||
{
|
||||
"kind": "ExternalSecret",
|
||||
"name": "issue-core-runtime",
|
||||
"namespace": "issue-core",
|
||||
"ready": [
|
||||
{
|
||||
"lastTransitionTime": "2026-08-19T19:54:20Z",
|
||||
"status": "True"
|
||||
}
|
||||
],
|
||||
"tracking_matches": null,
|
||||
"uid": "490dca37-6364-480a-a757-c3a266d32e47"
|
||||
}
|
||||
],
|
||||
"pinned_render_diff": {
|
||||
"revision": "d2dbc19c254247652c49fda8721c80d53bca206a",
|
||||
"path": "argocd/platform-addons/openbao-secretstore",
|
||||
"render_sha256": "35585d1b005272ec75e55cb4a68386bc515032693c2c70f6e76f68cad4e29fb3",
|
||||
"render_resource_count": 1,
|
||||
"exit_code": 1,
|
||||
"output_suppressed": true,
|
||||
"changed_top_fields": [
|
||||
"unknown"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
{
|
||||
"application": {
|
||||
"automated_present": false,
|
||||
"condition_types": [
|
||||
"OrphanedResourceWarning"
|
||||
],
|
||||
"destination": {
|
||||
"namespace": "external-secrets",
|
||||
"server": "https://kubernetes.default.svc"
|
||||
},
|
||||
"health": "Healthy",
|
||||
"name": "openbao-secretstore",
|
||||
"operation": {
|
||||
"finishedAt": "2026-09-21T17:09:18Z",
|
||||
"phase": "Succeeded",
|
||||
"startedAt": "2026-09-21T17:09:16Z"
|
||||
},
|
||||
"operation_revision": "d2dbc19c254247652c49fda8721c80d53bca206a",
|
||||
"pending_operation": false,
|
||||
"project": "railiance-platform-addons",
|
||||
"resources": [
|
||||
{
|
||||
"group": "external-secrets.io",
|
||||
"kind": "ClusterSecretStore",
|
||||
"name": "openbao",
|
||||
"namespace": null,
|
||||
"status": "Synced"
|
||||
}
|
||||
],
|
||||
"revision": "d2dbc19c254247652c49fda8721c80d53bca206a",
|
||||
"source": {
|
||||
"path": "argocd/platform-addons/openbao-secretstore",
|
||||
"repoURL": "https://forgejo.coulomb.social/coulomb/railiance-platform.git",
|
||||
"targetRevision": "d2dbc19c254247652c49fda8721c80d53bca206a"
|
||||
},
|
||||
"sync": "Synced",
|
||||
"sync_prune": false,
|
||||
"sync_strategy_apply": true,
|
||||
"uid": "f2932505-e649-4b99-b505-4789ed21301f"
|
||||
},
|
||||
"captured_at": "2026-10-11T19:15:05.030899+00:00",
|
||||
"estate_ip": "92.205.62.239",
|
||||
"ready_checks": [
|
||||
{
|
||||
"kind": "ClusterSecretStore",
|
||||
"name": "openbao",
|
||||
"namespace": "",
|
||||
"ready": [
|
||||
{
|
||||
"lastTransitionTime": "2026-08-19T19:49:59Z",
|
||||
"status": "True"
|
||||
}
|
||||
],
|
||||
"tracking_matches": true,
|
||||
"uid": "99563b77-f30f-451b-ac86-16b1b162c6a9"
|
||||
},
|
||||
{
|
||||
"kind": "ExternalSecret",
|
||||
"name": "issue-core-runtime",
|
||||
"namespace": "issue-core",
|
||||
"ready": [
|
||||
{
|
||||
"lastTransitionTime": "2026-08-19T19:54:20Z",
|
||||
"status": "True"
|
||||
}
|
||||
],
|
||||
"tracking_matches": null,
|
||||
"uid": "490dca37-6364-480a-a757-c3a266d32e47"
|
||||
}
|
||||
],
|
||||
"pinned_render_diff": {
|
||||
"revision": "d2dbc19c254247652c49fda8721c80d53bca206a",
|
||||
"path": "argocd/platform-addons/openbao-secretstore",
|
||||
"render_sha256": "35585d1b005272ec75e55cb4a68386bc515032693c2c70f6e76f68cad4e29fb3",
|
||||
"render_resource_count": 1,
|
||||
"exit_code": 1,
|
||||
"output_suppressed": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
{
|
||||
"application": {
|
||||
"automated_present": false,
|
||||
"condition_types": [
|
||||
"OrphanedResourceWarning"
|
||||
],
|
||||
"health": "Healthy",
|
||||
"name": "openbao-secretstore",
|
||||
"operation": {
|
||||
"finishedAt": "2026-09-21T17:09:18Z",
|
||||
"phase": "Succeeded",
|
||||
"startedAt": "2026-09-21T17:09:16Z"
|
||||
},
|
||||
"operation_revision": "d2dbc19c254247652c49fda8721c80d53bca206a",
|
||||
"pending_operation": false,
|
||||
"project": "railiance-platform-addons",
|
||||
"resources": [
|
||||
{
|
||||
"group": "external-secrets.io",
|
||||
"kind": "ClusterSecretStore",
|
||||
"name": "openbao",
|
||||
"namespace": null,
|
||||
"status": "Synced"
|
||||
}
|
||||
],
|
||||
"revision": "d2dbc19c254247652c49fda8721c80d53bca206a",
|
||||
"source": {
|
||||
"path": "argocd/platform-addons/openbao-secretstore",
|
||||
"repoURL": "https://forgejo.coulomb.social/coulomb/railiance-platform.git",
|
||||
"targetRevision": "d2dbc19c254247652c49fda8721c80d53bca206a"
|
||||
},
|
||||
"sync": "Synced",
|
||||
"sync_prune": false,
|
||||
"sync_strategy_apply": true,
|
||||
"uid": "f2932505-e649-4b99-b505-4789ed21301f"
|
||||
},
|
||||
"captured_at": "2026-10-11T19:12:20.205677+00:00",
|
||||
"estate_ip": "92.205.62.239",
|
||||
"ready_checks": [
|
||||
{
|
||||
"kind": "ClusterSecretStore",
|
||||
"name": "openbao",
|
||||
"namespace": "",
|
||||
"ready": [
|
||||
{
|
||||
"lastTransitionTime": "2026-08-19T19:49:59Z",
|
||||
"status": "True"
|
||||
}
|
||||
],
|
||||
"tracking_matches": false,
|
||||
"uid": "99563b77-f30f-451b-ac86-16b1b162c6a9"
|
||||
},
|
||||
{
|
||||
"kind": "ExternalSecret",
|
||||
"name": "issue-core-runtime",
|
||||
"namespace": "issue-core",
|
||||
"ready": [
|
||||
{
|
||||
"lastTransitionTime": "2026-08-19T19:54:20Z",
|
||||
"status": "True"
|
||||
}
|
||||
],
|
||||
"tracking_matches": null,
|
||||
"uid": "490dca37-6364-480a-a757-c3a266d32e47"
|
||||
}
|
||||
],
|
||||
"pinned_render_diff": {
|
||||
"revision": "d2dbc19c254247652c49fda8721c80d53bca206a",
|
||||
"path": "argocd/platform-addons/openbao-secretstore",
|
||||
"render_sha256": "35585d1b005272ec75e55cb4a68386bc515032693c2c70f6e76f68cad4e29fb3",
|
||||
"render_resource_count": 1,
|
||||
"exit_code": 1,
|
||||
"output_suppressed": true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"schema_version": "argocd-adoption-proof/v1",
|
||||
"status": "verified",
|
||||
"application": "openbao-secretstore",
|
||||
"revision": "d2dbc19c254247652c49fda8721c80d53bca206a",
|
||||
"captured_at": "2026-10-11T19:22:22.892821+00:00",
|
||||
"verified_at": "2026-10-11T19:23:40.325446+00:00",
|
||||
"proving_seconds": 1735984.892821,
|
||||
"resource_count": 1,
|
||||
"ready_count": 2,
|
||||
"manual_apply_prune_off": true,
|
||||
"warnings": [
|
||||
"OrphanedResourceWarning"
|
||||
],
|
||||
"native_mutation": false,
|
||||
"verifier_sha256": "c058c0ea646bcc3410e0d26db6b42a5471a8fa45a1df62b9ec85f2bf7d03f660",
|
||||
"contract_sha256": "427084e4f40d34778534397a99379789e355a3b3573b97a61762da8e43abc31e",
|
||||
"observation_sha256": "01e0850e496fe9234e91d883f1b4c7e7a574dd14bed60ac4585520bf1ddb79b2"
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"schema_version": "argocd-adoption-proof/v1",
|
||||
"status": "verified",
|
||||
"application": "openbao-secretstore",
|
||||
"revision": "d2dbc19c254247652c49fda8721c80d53bca206a",
|
||||
"captured_at": "2026-10-11T19:22:22.892821+00:00",
|
||||
"verified_at": "2026-10-11T19:22:27.223821+00:00",
|
||||
"proving_seconds": 1735984.892821,
|
||||
"resource_count": 1,
|
||||
"ready_count": 2,
|
||||
"manual_apply_prune_off": true,
|
||||
"warnings": [
|
||||
"OrphanedResourceWarning"
|
||||
],
|
||||
"native_mutation": false,
|
||||
"contract_sha256": "427084e4f40d34778534397a99379789e355a3b3573b97a61762da8e43abc31e",
|
||||
"observation_sha256": "01e0850e496fe9234e91d883f1b4c7e7a574dd14bed60ac4585520bf1ddb79b2"
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"schema_version": "argocd-adoption-proof/v1",
|
||||
"status": "unverified",
|
||||
"error": "adoption proof incomplete; raw output suppressed",
|
||||
"native_mutation": false
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"schema_version": "argocd-adoption-proof/v1",
|
||||
"status": "unverified",
|
||||
"error": "adoption proof incomplete; raw output suppressed",
|
||||
"native_mutation": false
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"schema_version": "argocd-adoption-proof/v1",
|
||||
"status": "unverified",
|
||||
"error": "adoption proof incomplete; raw output suppressed",
|
||||
"native_mutation": false
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"schema_version": "argocd-adoption-proof/v1",
|
||||
"status": "unverified",
|
||||
"error": "adoption proof incomplete; raw output suppressed",
|
||||
"native_mutation": false
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"schema_version": "argocd-adoption-validation/v1",
|
||||
"workplan": "RPF-WP-0044",
|
||||
"task": "RPF-WP-0044-T03",
|
||||
"native_mutation": false,
|
||||
"task_closed": true,
|
||||
"tests": {
|
||||
"passed": 13,
|
||||
"failed": 0
|
||||
},
|
||||
"compilation": "passed",
|
||||
"native_proof": "verified",
|
||||
"proof_ref": "proof-argo-normalized-final.json",
|
||||
"observation_ref": "observation-argo-normalized.json",
|
||||
"native_diff": {
|
||||
"canonical_server_side": 0,
|
||||
"canonical_client_side": 0,
|
||||
"canonical_client_changed_paths": [],
|
||||
"canonical_server_changed_paths": [],
|
||||
"raw_source_server_side": 1,
|
||||
"raw_source_client_side": 1
|
||||
},
|
||||
"normalization": "Argo v3 annotation tracking with Application destination namespace fallback; no live fields copied to desired source",
|
||||
"upstream_refs": [
|
||||
"https://github.com/argoproj/argo-cd/blob/v3.5.3/util/argo/resource_tracking.go",
|
||||
"https://argo-cd.readthedocs.io/en/release-3.5/operator-manual/upgrading/2.14-3.0/"
|
||||
],
|
||||
"observed_runtime": {
|
||||
"controller_version": "v3.5.3",
|
||||
"installation_id_absent": true,
|
||||
"instance_label_key": "app.kubernetes.io/instance",
|
||||
"method": "annotation"
|
||||
},
|
||||
"verifier_sha256": "c058c0ea646bcc3410e0d26db6b42a5471a8fa45a1df62b9ec85f2bf7d03f660",
|
||||
"retained_failure_history": [
|
||||
"proof.json",
|
||||
"proof-normalized.json",
|
||||
"proof-classified.json",
|
||||
"proof-both-diffs.json"
|
||||
],
|
||||
"secret_reads": 0,
|
||||
"subprocess_output_suppressed": true,
|
||||
"validated_at": "2026-10-11T19:24:51.574009+00:00"
|
||||
}
|
||||
355
scripts/argocd_adoption_proof.py
Normal file
355
scripts/argocd_adoption_proof.py
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Read-only Argo adoption proof. Capture/discard raw subprocess output on errors.
|
||||
|
||||
Use --live on the workstation; SSH collects only allowlisted metadata on the
|
||||
verified estate. The explicit contract binds source, revision and resource set.
|
||||
A receipt is proof of observation, never authority for a subsequent mutation.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import base64
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
PIN = 'd2dbc19c254247652c49fda8721c80d53bca206a'
|
||||
CONTRACT = {
|
||||
'estate_ip': '92.205.62.239', 'application': 'openbao-secretstore',
|
||||
'project': 'railiance-platform-addons',
|
||||
'destination': {'server': 'https://kubernetes.default.svc', 'namespace': 'external-secrets'},
|
||||
'repo_url': 'https://forgejo.coulomb.social/coulomb/railiance-platform.git',
|
||||
'source_path': 'argocd/platform-addons/openbao-secretstore', 'revision': PIN,
|
||||
'resources': [{'group': 'external-secrets.io', 'kind': 'ClusterSecretStore',
|
||||
'namespace': '', 'name': 'openbao'}],
|
||||
'ready_checks': [{'kind': 'ClusterSecretStore', 'namespace': '', 'name': 'openbao',
|
||||
'tracking_id': 'openbao-secretstore:external-secrets.io/ClusterSecretStore:external-secrets/openbao'},
|
||||
{'kind': 'ExternalSecret', 'namespace': 'issue-core',
|
||||
'name': 'issue-core-runtime'}],
|
||||
'tracking_method': 'annotation', 'controller_version': 'v3.5.3',
|
||||
'minimum_proving_seconds': 86400, 'maximum_observation_age_seconds': 300,
|
||||
}
|
||||
|
||||
|
||||
def checked_run(command, *, input=None, timeout=30):
|
||||
try:
|
||||
result = subprocess.run(command, input=input, capture_output=True, timeout=timeout)
|
||||
if result.returncode:
|
||||
raise RuntimeError('operation unverified; protected output suppressed')
|
||||
return result.stdout
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
raise RuntimeError('operation unverified; protected output suppressed') from None
|
||||
|
||||
|
||||
def identity(resource):
|
||||
return tuple(resource.get(k) or '' for k in ('group', 'kind', 'namespace', 'name'))
|
||||
|
||||
|
||||
def timestamp(value):
|
||||
if not isinstance(value, str):
|
||||
raise ValueError('invalid proof timestamp')
|
||||
stamp = datetime.fromisoformat(value.replace('Z', '+00:00'))
|
||||
if stamp.tzinfo is None:
|
||||
raise ValueError('proof timestamp requires timezone')
|
||||
return stamp
|
||||
|
||||
|
||||
def validate_contract(contract):
|
||||
if (not re.fullmatch(r'[a-f0-9]{40}', contract['revision'])
|
||||
or not re.fullmatch(r'[a-z0-9][a-z0-9.-]*', contract['application'])
|
||||
or not re.fullmatch(r'[a-z0-9/-]+', contract['source_path'])
|
||||
or '..' in contract['source_path'] or contract['source_path'].startswith('/')):
|
||||
raise ValueError('invalid adoption contract')
|
||||
for check in contract['ready_checks']:
|
||||
if check['kind'] not in ('ClusterSecretStore', 'ExternalSecret'):
|
||||
raise ValueError('secret objects are excluded from the metadata contract')
|
||||
for key in ('namespace', 'name'):
|
||||
if not re.fullmatch(r'[a-z0-9.-]*', check[key]) or (key == 'name' and not check[key]):
|
||||
raise ValueError('invalid metadata identity')
|
||||
if (not contract['resources'] or len({identity(r) for r in contract['resources']}) != len(contract['resources'])
|
||||
or contract['minimum_proving_seconds'] < 86400
|
||||
or not 0 < contract['maximum_observation_age_seconds'] <= 300):
|
||||
raise ValueError('invalid proof bounds')
|
||||
|
||||
|
||||
def tracking_settings(contract):
|
||||
cm = json.loads(checked_run(['kubectl', '-n', 'argocd', 'get', 'configmap', 'argocd-cm', '-o', 'json']))
|
||||
controller = json.loads(checked_run(['kubectl', '-n', 'argocd', 'get', 'statefulset', 'argocd-application-controller', '-o', 'json']))
|
||||
containers = controller['spec']['template']['spec']['containers']
|
||||
images = [c['image'] for c in containers if c['name'] == 'argocd-application-controller']
|
||||
if len(images) != 1 or not images[0].endswith(':' + contract['controller_version']):
|
||||
raise ValueError('expected Argo controller version unverified')
|
||||
data = cm.get('data', {})
|
||||
# Argo 3.0 changed the default from label to annotation. Bind that default to
|
||||
# an observed pinned 3.x controller; never infer it for an unknown runtime.
|
||||
method = data.get('application.resourceTrackingMethod', 'annotation')
|
||||
label_key = data.get('application.instanceLabelKey', 'app.kubernetes.io/instance')
|
||||
if method != contract['tracking_method'] or data.get('installationID'):
|
||||
raise ValueError('tracking semantics differ from this adoption contract')
|
||||
return {'method': method, 'instance_label_key': label_key,
|
||||
'controller_version': contract['controller_version'], 'installation_id_absent': True}
|
||||
|
||||
|
||||
def argo_desired(source, contract, settings):
|
||||
"""Inject only Argo-owned tracking metadata, never copy live fields into source."""
|
||||
import copy
|
||||
desired = copy.deepcopy(source)
|
||||
if (desired.get('apiVersion') != 'external-secrets.io/v1beta1'
|
||||
or desired.get('kind') != 'ClusterSecretStore'
|
||||
or desired.get('metadata', {}).get('name') != 'openbao'
|
||||
or desired['metadata'].get('namespace')):
|
||||
raise ValueError('pinned source identity differs from admitted store')
|
||||
if (settings['method'] != contract['tracking_method']
|
||||
or settings['controller_version'] != contract['controller_version']
|
||||
or settings['installation_id_absent'] is not True):
|
||||
raise ValueError('unsupported Argo render semantics')
|
||||
tracking = f"{contract['application']}:external-secrets.io/ClusterSecretStore:{contract['destination']['namespace']}/openbao"
|
||||
annotations = desired['metadata'].setdefault('annotations', {})
|
||||
if 'argocd.argoproj.io/tracking-id' in annotations and annotations['argocd.argoproj.io/tracking-id'] != tracking:
|
||||
raise ValueError('source tracking annotation conflicts with Argo identity')
|
||||
annotations['argocd.argoproj.io/tracking-id'] = tracking
|
||||
return desired
|
||||
|
||||
|
||||
def changed_paths(left, right, path=''):
|
||||
"""JSON pointers only: never return old/new values or annotation contents."""
|
||||
if type(left) is not type(right):
|
||||
return [path]
|
||||
if isinstance(left, dict):
|
||||
result = []
|
||||
for key in sorted(set(left) | set(right)):
|
||||
child = path + '/' + key.replace('~', '~0').replace('/', '~1')
|
||||
result.extend([child] if key not in left or key not in right else changed_paths(left[key], right[key], child))
|
||||
return result
|
||||
return [] if left == right else [path]
|
||||
|
||||
|
||||
def stable_object(value):
|
||||
import copy
|
||||
value = copy.deepcopy(value)
|
||||
for field in ('creationTimestamp', 'managedFields', 'resourceVersion', 'uid', 'generation'):
|
||||
value['metadata'].pop(field, None)
|
||||
value.pop('status', None)
|
||||
return value
|
||||
|
||||
|
||||
def remote_diff(rendered, contract):
|
||||
def get(args, input=None):
|
||||
return json.loads(checked_run(['kubectl', *args], input=input))
|
||||
nodes = get(['get', 'nodes', '-o', 'json'])
|
||||
if contract['estate_ip'] not in [a['address'] for n in nodes['items'] for a in n['status']['addresses']]:
|
||||
raise ValueError('estate identity unverified')
|
||||
settings = tracking_settings(contract)
|
||||
source = get(['create', '--dry-run=client', '--validate=false', '-f', '-', '-o', 'json'], rendered)
|
||||
normalized = json.dumps(argo_desired(source, contract, settings), sort_keys=True).encode()
|
||||
live = get(['get', '--raw', '/apis/external-secrets.io/v1beta1/clustersecretstores/openbao'])
|
||||
if live.get('apiVersion') != source['apiVersion']:
|
||||
raise ValueError('same source API-version read unverified')
|
||||
def diff(payload, server):
|
||||
command = ['kubectl', 'diff', *(['--server-side'] if server else []), '-f', '-']
|
||||
result = subprocess.run(command, input=payload, capture_output=True, timeout=30)
|
||||
if result.returncode not in (0, 1):
|
||||
raise ValueError('native diff failed; output suppressed')
|
||||
return result.returncode
|
||||
raw_client = get(['apply', '--dry-run=server', '-f', '-', '-o', 'json'], rendered)
|
||||
raw_server = get(['apply', '--server-side', '--dry-run=server', '-f', '-', '-o', 'json'], rendered)
|
||||
normalized_client = get(['apply', '--dry-run=server', '-f', '-', '-o', 'json'], normalized)
|
||||
normalized_server = get(['apply', '--server-side', '--dry-run=server', '-f', '-', '-o', 'json'], normalized)
|
||||
return {'revision': contract['revision'], 'path': contract['source_path'],
|
||||
'render_sha256': hashlib.sha256(rendered).hexdigest(), 'render_resource_count': 1,
|
||||
'normalized_render_sha256': hashlib.sha256(normalized).hexdigest(),
|
||||
'normalization': 'argocd-v3-annotation-tracking-destination-namespace',
|
||||
'tracking_settings': settings, 'same_api_version': True,
|
||||
'raw_server_exit_code': diff(rendered, True), 'raw_client_exit_code': diff(rendered, False),
|
||||
'raw_client_changed_paths': changed_paths(stable_object(live), stable_object(raw_client)),
|
||||
'raw_server_changed_paths': changed_paths(stable_object(live), stable_object(raw_server)),
|
||||
'exit_code': diff(normalized, True), 'client_side_exit_code': diff(normalized, False),
|
||||
'normalized_client_changed_paths': changed_paths(stable_object(live), stable_object(normalized_client)),
|
||||
'normalized_server_changed_paths': changed_paths(stable_object(live), stable_object(normalized_server)),
|
||||
'output_suppressed': True}
|
||||
|
||||
|
||||
def collect(contract):
|
||||
validate_contract(contract)
|
||||
def get(args):
|
||||
return json.loads(checked_run(['kubectl', *args, '-o', 'json']))
|
||||
nodes = get(['get', 'nodes'])
|
||||
addresses = [a.get('address') for n in nodes['items'] for a in n.get('status', {}).get('addresses', [])]
|
||||
if contract['estate_ip'] not in addresses:
|
||||
raise ValueError('estate identity unverified')
|
||||
obj = get(['-n', 'argocd', 'get', 'application', contract['application']])
|
||||
spec, status = obj['spec'], obj.get('status', {})
|
||||
op = status.get('operationState', {})
|
||||
sync_operation = op.get('operation', {}).get('sync', {})
|
||||
app = {'name': obj['metadata']['name'], 'uid': obj['metadata']['uid'],
|
||||
'project': spec.get('project'), 'destination': spec.get('destination'), 'source': {k: spec.get('source', {}).get(k)
|
||||
for k in ('repoURL', 'path', 'targetRevision')},
|
||||
'automated_present': 'automated' in spec.get('syncPolicy', {}),
|
||||
'sync': status.get('sync', {}).get('status'), 'health': status.get('health', {}).get('status'),
|
||||
'revision': status.get('sync', {}).get('revision'),
|
||||
'pending_operation': bool(obj.get('operation')),
|
||||
'condition_types': [c.get('type') for c in status.get('conditions', [])],
|
||||
'operation': {k: op.get(k) for k in ('phase', 'startedAt', 'finishedAt')},
|
||||
'sync_prune': sync_operation.get('prune', False),
|
||||
'sync_strategy_apply': 'apply' in sync_operation.get('syncStrategy', {}),
|
||||
'operation_revision': sync_operation.get('revision'),
|
||||
'resources': [{k: r.get(k) for k in ('group', 'kind', 'namespace', 'name', 'status')}
|
||||
for r in status.get('resources', [])]}
|
||||
checks = []
|
||||
for check in contract['ready_checks']:
|
||||
args = ['-n', check['namespace']] if check['namespace'] else []
|
||||
obj = get([*args, 'get', check['kind'], check['name']])
|
||||
ready = [c for c in obj.get('status', {}).get('conditions', []) if c.get('type') == 'Ready']
|
||||
checks.append({'kind': check['kind'], 'namespace': check['namespace'], 'name': check['name'],
|
||||
'uid': obj['metadata']['uid'],
|
||||
'ready': [{k: c.get(k) for k in ('status', 'lastTransitionTime')} for c in ready],
|
||||
'tracking_matches': obj['metadata'].get('annotations', {}).get('argocd.argoproj.io/tracking-id') == check['tracking_id']
|
||||
if 'tracking_id' in check else None})
|
||||
return {'tracking_settings': tracking_settings(contract),
|
||||
'captured_at': datetime.now(timezone.utc).isoformat(),
|
||||
'estate_ip': contract['estate_ip'], 'application': app, 'ready_checks': checks}
|
||||
|
||||
|
||||
def verify(observation, contract, *, now=None):
|
||||
validate_contract(contract)
|
||||
now = now or datetime.now(timezone.utc)
|
||||
age = (now - timestamp(observation['captured_at'])).total_seconds()
|
||||
if not 0 <= age <= contract['maximum_observation_age_seconds']:
|
||||
raise ValueError('observation stale or from future')
|
||||
if observation['estate_ip'] != contract['estate_ip']:
|
||||
raise ValueError('estate identity mismatch')
|
||||
settings = observation.get('tracking_settings', {})
|
||||
if (settings.get('method') != contract['tracking_method'] or settings.get('controller_version') != contract['controller_version']
|
||||
or settings.get('installation_id_absent') is not True):
|
||||
raise ValueError('observed Argo tracking semantics unverified')
|
||||
app = observation['application']
|
||||
if (app['name'] != contract['application'] or not app['uid'] or app['project'] != contract['project']
|
||||
or app['source'] != {'repoURL': contract['repo_url'], 'path': contract['source_path'], 'targetRevision': contract['revision']}
|
||||
or app['revision'] != contract['revision'] or app.get('destination') != contract['destination']):
|
||||
raise ValueError('application source or identity mismatch')
|
||||
if app['automated_present'] is not False or app['pending_operation'] is not False:
|
||||
raise ValueError('manual idle policy unverified')
|
||||
if app['sync'] != 'Synced' or app['health'] != 'Healthy':
|
||||
raise ValueError('application sync or health unverified')
|
||||
if any(t != 'OrphanedResourceWarning' for t in app['condition_types']):
|
||||
raise ValueError('application condition prevents proof')
|
||||
op = app['operation']
|
||||
if (op['phase'] != 'Succeeded' or app['sync_prune'] is not False
|
||||
or app['sync_strategy_apply'] is not True or app['operation_revision'] != contract['revision']):
|
||||
raise ValueError('successful pinned manual apply unverified')
|
||||
start, finish = timestamp(op['startedAt']), timestamp(op['finishedAt'])
|
||||
proof_age = (timestamp(observation['captured_at']) - finish).total_seconds()
|
||||
if finish < start or proof_age < contract['minimum_proving_seconds']:
|
||||
raise ValueError('completed adoption proving period unverified')
|
||||
resources = app['resources']
|
||||
expected = {identity(r) for r in contract['resources']}
|
||||
if (len(resources) != len(expected) or {identity(r) for r in resources} != expected
|
||||
or any(r['status'] != 'Synced' for r in resources)):
|
||||
raise ValueError('exact tracked resource set unverified')
|
||||
checks = observation['ready_checks']
|
||||
expected_checks = {(c['kind'], c['namespace'], c['name']): c for c in contract['ready_checks']}
|
||||
if len(checks) != len(expected_checks) or {(c['kind'], c['namespace'], c['name']) for c in checks} != set(expected_checks):
|
||||
raise ValueError('readiness checks incomplete')
|
||||
for check in checks:
|
||||
key = (check['kind'], check['namespace'], check['name'])
|
||||
ready = check['ready']
|
||||
if not check['uid'] or len(ready) != 1 or ready[0]['status'] != 'True':
|
||||
raise ValueError('resource readiness unverified')
|
||||
if timestamp(ready[0]['lastTransitionTime']) > start:
|
||||
raise ValueError('resource readiness changed during adoption proving period')
|
||||
if 'tracking_id' in expected_checks[key] and check['tracking_matches'] is not True:
|
||||
raise ValueError('resource tracking identity unverified')
|
||||
diff = observation.get('pinned_render_diff', {})
|
||||
allowed_raw_paths = {
|
||||
'/metadata/annotations/argocd.argoproj.io~1tracking-id',
|
||||
'/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration',
|
||||
}
|
||||
for mode in ('client', 'server'):
|
||||
paths = diff.get(f'raw_{mode}_changed_paths')
|
||||
code = diff.get(f'raw_{mode}_exit_code')
|
||||
if (not isinstance(paths, list) or any(not isinstance(path, str) for path in paths)
|
||||
or not set(paths) <= allowed_raw_paths or type(code) is not int
|
||||
or code not in (0, 1) or bool(paths) != bool(code)):
|
||||
raise ValueError('raw source drift contains unknown or substantive fields')
|
||||
if (diff.get('revision') != contract['revision'] or diff.get('path') != contract['source_path']
|
||||
or type(diff.get('exit_code')) is not int or diff['exit_code'] != 0
|
||||
or type(diff.get('client_side_exit_code')) is not int or diff['client_side_exit_code'] != 0
|
||||
or diff.get('normalization') != 'argocd-v3-annotation-tracking-destination-namespace'
|
||||
or diff.get('tracking_settings') != settings or diff.get('same_api_version') is not True
|
||||
or diff.get('normalized_client_changed_paths') != [] or diff.get('normalized_server_changed_paths') != []
|
||||
or not re.fullmatch(r'[a-f0-9]{64}', diff.get('normalized_render_sha256', ''))
|
||||
or diff.get('render_resource_count') != len(expected)
|
||||
or not re.fullmatch(r'[a-f0-9]{64}', diff.get('render_sha256', ''))):
|
||||
raise ValueError('exact pinned render zero-diff unverified')
|
||||
return {'schema_version': 'argocd-adoption-proof/v1', 'status': 'verified',
|
||||
'application': contract['application'], 'revision': contract['revision'],
|
||||
'captured_at': observation['captured_at'], 'verified_at': now.isoformat(),
|
||||
'proving_seconds': proof_age, 'resource_count': len(expected),
|
||||
'ready_count': len(checks), 'manual_apply_prune_off': True,
|
||||
'warnings': sorted(set(app['condition_types'])), 'native_mutation': False,
|
||||
'verifier_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
|
||||
'contract_sha256': hashlib.sha256(json.dumps(contract,sort_keys=True).encode()).hexdigest(),
|
||||
'observation_sha256': hashlib.sha256(json.dumps(observation,sort_keys=True).encode()).hexdigest()}
|
||||
|
||||
|
||||
def render_diff(source_root, contract):
|
||||
"""Render a pinned tracked source into a disposable local directory; no checkout."""
|
||||
with tempfile.TemporaryDirectory(prefix='adoption-pin-') as temporary:
|
||||
archive = checked_run(['git', '-C', str(source_root), 'archive', contract['revision'], contract['source_path']])
|
||||
checked_run(['tar', '-x', '-C', temporary], input=archive)
|
||||
rendered = checked_run(['kubectl', 'kustomize', str(Path(temporary) / contract['source_path'])])
|
||||
# Fixed admission for this phase: exactly one public ClusterSecretStore,
|
||||
# never forward a Secret or extra document to a diff operation.
|
||||
if re.findall(rb'^kind: (.+)$', rendered, re.M) != [b'ClusterSecretStore']:
|
||||
raise ValueError('pinned render resource kind unverified')
|
||||
encoded = base64.b64encode(rendered).decode()
|
||||
return json.loads(checked_run(['ssh', 'railiance01', 'python3', '-', '--remote-diff', '--render-base64', encoded],
|
||||
input=Path(__file__).read_bytes(), timeout=150))
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--live', action='store_true')
|
||||
parser.add_argument('--remote-diff', action='store_true', help=argparse.SUPPRESS)
|
||||
parser.add_argument('--render-base64', help=argparse.SUPPRESS)
|
||||
parser.add_argument('--remote-collect', action='store_true', help=argparse.SUPPRESS)
|
||||
parser.add_argument('--observation', type=Path)
|
||||
parser.add_argument('--source-root', type=Path)
|
||||
parser.add_argument('--receipt', type=Path)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
if args.remote_diff:
|
||||
print(json.dumps(remote_diff(base64.b64decode(args.render_base64, validate=True), CONTRACT), sort_keys=True))
|
||||
return 0
|
||||
if args.remote_collect:
|
||||
print(json.dumps(collect(CONTRACT), sort_keys=True))
|
||||
return 0
|
||||
if args.live:
|
||||
if not args.source_root:
|
||||
raise ValueError('source root required for pinned rendering')
|
||||
raw = checked_run(['ssh', 'railiance01', 'python3', '-', '--remote-collect'],
|
||||
input=Path(__file__).read_bytes(), timeout=120)
|
||||
observation = json.loads(raw)
|
||||
observation['pinned_render_diff'] = render_diff(args.source_root, CONTRACT)
|
||||
if args.observation:
|
||||
args.observation.write_text(json.dumps(observation,indent=2)+'\n')
|
||||
elif args.observation:
|
||||
observation = json.loads(args.observation.read_text())
|
||||
else:
|
||||
raise ValueError('live read or observation required')
|
||||
receipt = verify(observation, CONTRACT)
|
||||
except Exception:
|
||||
receipt = {'schema_version':'argocd-adoption-proof/v1', 'status':'unverified',
|
||||
'error':'adoption proof incomplete; raw output suppressed', 'native_mutation':False}
|
||||
if args.receipt:
|
||||
args.receipt.write_text(json.dumps(receipt,indent=2)+'\n')
|
||||
print(json.dumps(receipt,sort_keys=True))
|
||||
return 0 if receipt.get('status') == 'verified' else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
143
tests/test_argocd_adoption_proof.py
Normal file
143
tests/test_argocd_adoption_proof.py
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import copy
|
||||
from datetime import datetime, timezone, timedelta
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
SPEC=importlib.util.spec_from_file_location('adoption',Path(__file__).parents[1]/'scripts/argocd_adoption_proof.py')
|
||||
p=importlib.util.module_from_spec(SPEC);SPEC.loader.exec_module(p)
|
||||
NOW=datetime(2026,10,11,20,tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def fixture():
|
||||
return {'captured_at':NOW.isoformat(),'estate_ip':'92.205.62.239',
|
||||
'tracking_settings':{'method':'annotation','controller_version':'v3.5.3','installation_id_absent':True,'instance_label_key':'app.kubernetes.io/instance'},
|
||||
'application':{'name':'openbao-secretstore','uid':'app-uid','project':p.CONTRACT['project'],'destination':p.CONTRACT['destination'],
|
||||
'source':{'repoURL':p.CONTRACT['repo_url'],'path':p.CONTRACT['source_path'],'targetRevision':p.PIN},
|
||||
'revision':p.PIN,'automated_present':False,'pending_operation':False,'sync':'Synced','health':'Healthy',
|
||||
'condition_types':['OrphanedResourceWarning'],'sync_prune':False,'sync_strategy_apply':True,
|
||||
'operation_revision':p.PIN,'operation':{'phase':'Succeeded','startedAt':'2026-09-21T17:09:16Z','finishedAt':'2026-09-21T17:09:18Z'},
|
||||
'resources':[dict(p.CONTRACT['resources'][0],status='Synced')]},
|
||||
'ready_checks':[{'kind':c['kind'],'namespace':c['namespace'],'name':c['name'],'uid':'resource-uid',
|
||||
'ready':[{'status':'True','lastTransitionTime':'2026-08-19T19:49:59Z'}],
|
||||
'tracking_matches':True if 'tracking_id' in c else None} for c in p.CONTRACT['ready_checks']],
|
||||
'pinned_render_diff':{'revision':p.PIN,'path':p.CONTRACT['source_path'],'exit_code':0,'render_resource_count':1,'render_sha256':'a'*64,
|
||||
'client_side_exit_code':0,'normalized_render_sha256':'b'*64,'same_api_version':True,
|
||||
'normalization':'argocd-v3-annotation-tracking-destination-namespace',
|
||||
'tracking_settings':{'method':'annotation','controller_version':'v3.5.3','installation_id_absent':True,'instance_label_key':'app.kubernetes.io/instance'},
|
||||
'normalized_client_changed_paths':[],'normalized_server_changed_paths':[],
|
||||
'raw_client_exit_code':1,'raw_server_exit_code':1,
|
||||
'raw_client_changed_paths':['/metadata/annotations/argocd.argoproj.io~1tracking-id','/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration'],
|
||||
'raw_server_changed_paths':['/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration']}}
|
||||
|
||||
|
||||
class ProofTest(unittest.TestCase):
|
||||
def test_complete_fixture_passes(self):
|
||||
self.assertEqual(p.verify(fixture(),p.CONTRACT,now=NOW)['status'],'verified')
|
||||
|
||||
def test_wrong_estate_stale_future_or_missing_observation_refused(self):
|
||||
for values in [{'estate_ip':'other'},{'captured_at':(NOW-timedelta(minutes=6)).isoformat()},
|
||||
{'captured_at':(NOW+timedelta(seconds=1)).isoformat()},{'captured_at':'2026-10-11'}]:
|
||||
with self.subTest(values=values), self.assertRaises(ValueError):
|
||||
p.verify(fixture()|values,p.CONTRACT,now=NOW)
|
||||
|
||||
def test_exact_pin_project_source_policy_and_health_refusals(self):
|
||||
for key,value in [('revision','b'*40),('destination',{'server':'other'}),('project','other'),('automated_present',True),
|
||||
('pending_operation',True),('sync','OutOfSync'),('health','Degraded'),
|
||||
('condition_types',['ComparisonError']),('sync_prune',True),
|
||||
('sync_strategy_apply',False),('operation_revision','b'*40),
|
||||
('source',{'repoURL':'different'})]:
|
||||
data=fixture();data['application'][key]=value
|
||||
with self.subTest(key=key),self.assertRaises(ValueError):p.verify(data,p.CONTRACT,now=NOW)
|
||||
|
||||
def test_extra_duplicate_missing_or_wrong_tracked_resources_refused(self):
|
||||
for resources in [[],[p.CONTRACT['resources'][0]]*2,
|
||||
[dict(p.CONTRACT['resources'][0],name='another',status='Synced')],
|
||||
[dict(p.CONTRACT['resources'][0],status='OutOfSync')]]:
|
||||
data=fixture();data['application']['resources']=resources
|
||||
with self.assertRaises(ValueError):p.verify(data,p.CONTRACT,now=NOW)
|
||||
|
||||
def test_proving_period_and_nonmonotonic_operation_refusals(self):
|
||||
for operation in [{'phase':'Succeeded','startedAt':NOW.isoformat(),'finishedAt':NOW.isoformat()},
|
||||
{'phase':'Failed','startedAt':'2026-09-21T17:09:16Z','finishedAt':'2026-09-21T17:09:18Z'},
|
||||
{'phase':'Succeeded','startedAt':'2026-09-22T17:09:16Z','finishedAt':'2026-09-21T17:09:18Z'}]:
|
||||
data=fixture();data['application']['operation']=operation
|
||||
with self.assertRaises(ValueError):p.verify(data,p.CONTRACT,now=NOW)
|
||||
|
||||
def test_readiness_tracking_and_incomplete_health_refusals(self):
|
||||
for key,value in [('tracking_matches',False),('ready',[]),
|
||||
('ready',[{'status':'False','lastTransitionTime':'2026-08-19T19:49:59Z'}]),
|
||||
('ready',[{'status':'True','lastTransitionTime':'2026-10-11T19:49:59Z'}])]:
|
||||
data=fixture();data['ready_checks'][0][key]=value
|
||||
with self.assertRaises(ValueError):p.verify(data,p.CONTRACT,now=NOW)
|
||||
data=fixture();data['ready_checks']=[]
|
||||
with self.assertRaises(ValueError):p.verify(data,p.CONTRACT,now=NOW)
|
||||
|
||||
def test_nonzero_wrong_pin_missing_or_invalid_diff_refusals(self):
|
||||
for key,value in [('exit_code',1),('exit_code',2),('revision','b'*40),
|
||||
('render_resource_count',2),('render_sha256','wrong'),('client_side_exit_code',1),
|
||||
('exit_code',False),('same_api_version',False),('normalization','unknown'),
|
||||
('normalized_client_changed_paths',['/spec/provider']),
|
||||
('normalized_server_changed_paths',['/metadata/labels/unknown']),
|
||||
('raw_client_changed_paths',['unknown']),('raw_server_changed_paths',['/spec/provider'])]:
|
||||
data=fixture();data['pinned_render_diff'][key]=value
|
||||
with self.assertRaises(ValueError):p.verify(data,p.CONTRACT,now=NOW)
|
||||
data=fixture();del data['pinned_render_diff']
|
||||
with self.assertRaises(ValueError):p.verify(data,p.CONTRACT,now=NOW)
|
||||
|
||||
def test_subprocess_failure_and_timeout_do_not_leak_output(self):
|
||||
import subprocess
|
||||
from types import SimpleNamespace
|
||||
secret='CANARY-DO-NOT-EMIT'
|
||||
with mock.patch.object(p.subprocess,'run',return_value=SimpleNamespace(returncode=1,stdout=secret,stderr=secret)):
|
||||
with self.assertRaises(RuntimeError) as exc:p.checked_run(['kubectl','get','nodes'])
|
||||
self.assertNotIn(secret,str(exc.exception))
|
||||
with mock.patch.object(p.subprocess,'run',side_effect=subprocess.TimeoutExpired('cmd',1,output=secret,stderr=secret)):
|
||||
with self.assertRaises(RuntimeError) as exc:p.checked_run(['kubectl','get','nodes'])
|
||||
self.assertNotIn(secret,str(exc.exception))
|
||||
|
||||
def test_argo_tracking_injection_preserves_source_spec_and_annotations(self):
|
||||
source={'apiVersion':'external-secrets.io/v1beta1','kind':'ClusterSecretStore',
|
||||
'metadata':{'name':'openbao','annotations':{'source-owned':'preserve'}},
|
||||
'spec':{'provider':{'vault':{'path':'platform'}}}}
|
||||
original=copy.deepcopy(source)
|
||||
settings=fixture()['tracking_settings']
|
||||
desired=p.argo_desired(source,p.CONTRACT,settings)
|
||||
self.assertEqual(source,original)
|
||||
self.assertEqual(desired['spec'],source['spec'])
|
||||
self.assertEqual(desired['metadata']['annotations']['source-owned'],'preserve')
|
||||
self.assertEqual(desired['metadata']['annotations']['argocd.argoproj.io/tracking-id'],
|
||||
'openbao-secretstore:external-secrets.io/ClusterSecretStore:external-secrets/openbao')
|
||||
self.assertNotIn('labels',desired['metadata'])
|
||||
|
||||
def test_unknown_tracking_semantics_conflicting_id_or_secret_render_refused(self):
|
||||
source={'apiVersion':'external-secrets.io/v1beta1','kind':'ClusterSecretStore','metadata':{'name':'openbao'}}
|
||||
settings=fixture()['tracking_settings']
|
||||
for delta in [{'method':'label'},{'controller_version':'unknown'},{'installation_id_absent':False}]:
|
||||
with self.assertRaises(ValueError):p.argo_desired(source,p.CONTRACT,settings|delta)
|
||||
for delta in [{'kind':'Secret'},{'apiVersion':'external-secrets.io/v1'},
|
||||
{'metadata':{'name':'openbao','namespace':'external-secrets'}},
|
||||
{'metadata':{'name':'openbao','annotations':{'argocd.argoproj.io/tracking-id':'foreign-owner'}}}]:
|
||||
with self.assertRaises(ValueError):p.argo_desired(source|delta,p.CONTRACT,settings)
|
||||
|
||||
def test_changed_paths_preserve_unknown_fields_and_never_expose_values(self):
|
||||
before={'metadata':{'annotations':{'x/y~z':'CANARY-PRIVATE'}},'spec':{'path':'old'}}
|
||||
after={'metadata':{'annotations':{'x/y~z':'OTHER-PRIVATE'}},'spec':{'path':'new'}}
|
||||
paths=p.changed_paths(before,after)
|
||||
self.assertEqual(paths,['/metadata/annotations/x~1y~0z','/spec/path'])
|
||||
self.assertNotIn('CANARY',str(paths))
|
||||
self.assertNotIn('PRIVATE',str(paths))
|
||||
|
||||
def test_runtime_tracking_and_diagnostic_integrity_refusals(self):
|
||||
for delta in [{'method':'label'},{'controller_version':'v2.14.0'},{'installation_id_absent':False}]:
|
||||
data=fixture();data['tracking_settings'].update(delta)
|
||||
with self.assertRaises(ValueError):p.verify(data,p.CONTRACT,now=NOW)
|
||||
for delta in [{'raw_client_changed_paths':None},{'raw_client_exit_code':0},
|
||||
{'raw_server_changed_paths':['unknown']},{'raw_server_exit_code':2}]:
|
||||
data=fixture();data['pinned_render_diff'].update(delta)
|
||||
with self.assertRaises(ValueError):p.verify(data,p.CONTRACT,now=NOW)
|
||||
|
||||
def test_secret_read_contract_is_rejected(self):
|
||||
contract=copy.deepcopy(p.CONTRACT);contract['ready_checks'][0]['kind']='Secret'
|
||||
with self.assertRaises(ValueError):p.validate_contract(contract)
|
||||
|
|
@ -9,7 +9,7 @@ flavor: planning
|
|||
owner: railiance-platform
|
||||
topic_slug: railiance
|
||||
created: "2026-09-21"
|
||||
updated: "2026-10-03"
|
||||
updated: "2026-10-11"
|
||||
related: [RPF-WP-0043, RPF-WP-0022]
|
||||
state_hub_workstream_id: "98140775-3b9a-5cf9-9af6-722502d487dc"
|
||||
---
|
||||
|
|
@ -185,7 +185,7 @@ Rollback:
|
|||
|
||||
```task
|
||||
id: RPF-WP-0044-T03
|
||||
status: todo
|
||||
status: done
|
||||
decision_id: 89f1058f-33bc-47a8-8a49-0b44d426c67e
|
||||
priority: high
|
||||
state_hub_task_id: "6d5fc801-e361-579b-bcd8-6fe719a82e94"
|
||||
|
|
@ -233,6 +233,37 @@ Rollback (R1): `git revert` the merge commit and push, then
|
|||
|
||||
2026-10-03: human gate routed to hub decision 89f1058f-33bc-47a8-8a49-0b44d426c67e (D2: ArgoCD owns production: go-ahead sequence for railiance-apps-root, issue-core, policy-nexus and activity-core automation) by CUST-WP-0075; resolving that decision raises C-40 here.
|
||||
|
||||
2026-10-11: **24h adoption proof verified; T03 done.**
|
||||
`scripts/argocd_adoption_proof.py` freshly verifies estate IP 92.205.62.239,
|
||||
local-cluster destination, exact source pin `d2dbc19`, one tracked
|
||||
ClusterSecretStore/openbao, manual idle policy, successful Sep21 apply/prune-off
|
||||
sync, exact ownership annotation and uninterrupted Ready conditions on the store
|
||||
and issue-core consumer since Aug19. The completed proving period is 1,735,984
|
||||
seconds. Current Application is Synced/Healthy; the nonblocking
|
||||
OrphanedResourceWarning is retained and no broader namespace adoption is claimed.
|
||||
|
||||
Both server-side and client-side native pinned-render diffs return **0** after
|
||||
canonical Argo v3 annotation injection. This is bound to the observed v3.5.3
|
||||
controller, `argocd-cm` tracking configuration and the Application destination
|
||||
namespace. The cluster-scoped store's tracking ID uses `external-secrets` as
|
||||
Argo's destination-namespace fallback; no instance label is injected in annotation
|
||||
mode. Same-source-API-version server dry-runs show **no changed JSON paths**.
|
||||
Only source-derived desired fields and Argo's exact tracking annotation are
|
||||
rendered; no live values are copied into the desired source, and unknown or
|
||||
substantive diffs fail closed. Upstream implementation:
|
||||
https://github.com/argoproj/argo-cd/blob/v3.5.3/util/argo/resource_tracking.go
|
||||
(`UnstructuredToAppInstanceValue`, `SetAppInstance`).
|
||||
|
||||
Evidence: `docs/evidence/2026-10-11-openbao-adoption-proof/`:
|
||||
`observation-argo-normalized.json`, `proof-argo-normalized-final.json`, and
|
||||
`validation.json`. The proof binds the verifier and observation digests. Thirteen
|
||||
meaningful fixture tests and compilation pass. Earlier empty-namespace and raw
|
||||
source diff refusals remain visible: raw source omits Argo-owned tracking and
|
||||
last-applied metadata; an initial API-version comparison also used the API
|
||||
default rather than the pinned source version. No native mutation, Secret read
|
||||
or annotation value/error output occurred. This closes the existing adoption's
|
||||
proving period without changing sync/automation policy or using a new human gate.
|
||||
|
||||
## Adopt issue-core (from rapp-issue-core)
|
||||
|
||||
```task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue