rcf-lite 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/blueprints/application-account-settings/docs/topics.md +3 -0
- package/blueprints/application-admin-console/docs/topics.md +3 -0
- package/blueprints/application-api-rest/docs/topics.md +3 -0
- package/blueprints/application-charts/docs/topics.md +3 -0
- package/blueprints/application-dashboard/docs/topics.md +3 -0
- package/blueprints/application-datatable/docs/topics.md +3 -0
- package/blueprints/application-empty-error-states/docs/topics.md +3 -0
- package/blueprints/application-error-handling/docs/topics.md +3 -0
- package/blueprints/application-file-upload/docs/topics.md +3 -0
- package/blueprints/application-forms-wizard/docs/topics.md +3 -0
- package/blueprints/application-notifications-in-app/docs/topics.md +3 -0
- package/blueprints/application-onboarding-tour/docs/topics.md +3 -0
- package/blueprints/application-spa/docs/topics.md +3 -0
- package/blueprints/delivery-ci-workflows/docs/topics.md +3 -0
- package/blueprints/deploy-cloudflare-workers/docs/topics.md +3 -0
- package/blueprints/deploy-hetzner-server/CHANGELOG.md +34 -0
- package/blueprints/deploy-hetzner-server/README.md +202 -0
- package/blueprints/deploy-hetzner-server/blueprint.json +233 -0
- package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3801-deploy-hetzner-server-cloud-host-contract.json +14 -0
- package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3802-deploy-hetzner-server-snapshot-cadence.json +14 -0
- package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3803-deploy-hetzner-server-provisioning-tool.json +20 -0
- package/blueprints/deploy-hetzner-server/contributions/adrs/adr-3804-deploy-hetzner-server-hardening-baseline.json +14 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/cloud-init-render-lint.mjs +64 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/hcloud-dry-run-mock.mjs +116 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/manifest-schema-validate.mjs +180 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/probe-utils.mjs +106 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/real-account-cloud-init-hardened.mjs +63 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/real-account-snapshot-on-demand.mjs +60 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/real-account-throwaway-server-provision.mjs +58 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-cloud-init-render-lint.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-hcloud-dry-run-mock.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-manifest-schema-validate.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-real-account-cloud-init-hardened.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-real-account-snapshot-on-demand.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/probes/run-real-account-throwaway-server-provision.mjs +9 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-001.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-002.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-003.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-004.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-005.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/requirements/deploy-hetzner-server-req-006.json +18 -0
- package/blueprints/deploy-hetzner-server/contributions/schemas/hetzner-server.schema.json +108 -0
- package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3801-deploy-hetzner-server-provisioner.json +27 -0
- package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3802-deploy-hetzner-server-manifest-schema.json +26 -0
- package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3803-deploy-hetzner-server-cloud-init-template.json +26 -0
- package/blueprints/deploy-hetzner-server/contributions/tacs/tac-3804-deploy-hetzner-server-firewall-shape.json +25 -0
- package/blueprints/deploy-hetzner-server/contributions/templates/cloud-init.yaml.tmpl +93 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37101.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37102.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37103.json +25 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37104.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37105.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37106.json +25 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37107.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37108.json +24 -0
- package/blueprints/deploy-hetzner-server/contributions/user-stories/deploy-hetzner-server-us-37109.json +24 -0
- package/blueprints/deploy-hetzner-server/docs/topics.md +48 -0
- package/blueprints/deploy-hetzner-server/guide/deploy-hetzner-server.md +180 -0
- package/blueprints/edge-cloudflare-access/docs/topics.md +3 -0
- package/blueprints/edge-cloudflare-rate-limiting/docs/topics.md +3 -0
- package/blueprints/edge-cloudflare-tunnel/CHANGELOG.md +13 -0
- package/blueprints/edge-cloudflare-tunnel/README.md +257 -0
- package/blueprints/edge-cloudflare-tunnel/blueprint.json +168 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/adrs/adr-4001-edge-cloudflare-tunnel-ingress-bridge-contract.json +23 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/adrs/adr-4002-edge-cloudflare-tunnel-connector-runtime.json +19 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/adrs/adr-4003-edge-cloudflare-tunnel-hostname-mode.json +19 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/aud-presence-check.mjs +114 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/cloudflared-config-lint.mjs +80 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/manifest-schema-validate.mjs +234 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/probe-utils.mjs +231 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/real-account-connector-healthy.mjs +54 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/real-account-tunnel-hostname-routes.mjs +73 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-aud-presence-check.mjs +4 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-cloudflared-config-lint.mjs +4 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-manifest-schema-validate.mjs +4 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-real-account-connector-healthy.mjs +4 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/run-real-account-tunnel-hostname-routes.mjs +4 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/probes/tunnel-manifest.schema.json +55 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-001.json +18 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-002.json +18 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-003.json +18 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-004.json +18 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/requirements/edge-cloudflare-tunnel-req-005.json +18 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/tacs/tac-4001-edge-cloudflare-tunnel-connector-shape.json +26 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/tacs/tac-4002-edge-cloudflare-tunnel-manifest-schema.json +26 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/tacs/tac-4003-edge-cloudflare-tunnel-access-binding.json +25 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39101.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39102.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39103.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39104.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39105.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39106.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39107.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/contributions/user-stories/edge-cloudflare-tunnel-us-39108.json +24 -0
- package/blueprints/edge-cloudflare-tunnel/docs/topics.md +72 -0
- package/blueprints/edge-cloudflare-tunnel/guide/edge-cloudflare-tunnel.md +204 -0
- package/blueprints/edge-cloudflare-turnstile/docs/topics.md +3 -0
- package/blueprints/email-smtp-resend/docs/topics.md +3 -0
- package/blueprints/jobs-background/docs/topics.md +3 -0
- package/blueprints/messaging-queue-cloudflare/docs/topics.md +3 -0
- package/blueprints/object-storage-s3/docs/topics.md +3 -0
- package/blueprints/observability-essentials/docs/topics.md +3 -0
- package/blueprints/observability-logging/docs/topics.md +3 -0
- package/blueprints/observability-probe-endpoints/docs/topics.md +3 -0
- package/blueprints/persistence-data-d1/docs/topics.md +3 -0
- package/blueprints/persistence-data-postgres/docs/topics.md +3 -0
- package/blueprints/persistence-data-sqlite/docs/topics.md +3 -0
- package/blueprints/platform-cloudflare-cron-triggers/docs/topics.md +3 -0
- package/blueprints/platform-cloudflare-durable-objects/docs/topics.md +3 -0
- package/blueprints/platform-cloudflare-kv/docs/topics.md +3 -0
- package/blueprints/platform-docker-compose-host/CHANGELOG.md +12 -0
- package/blueprints/platform-docker-compose-host/README.md +76 -0
- package/blueprints/platform-docker-compose-host/blueprint.json +209 -0
- package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3901-platform-docker-compose-host-contract.json +14 -0
- package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3902-platform-docker-compose-host-reverse-proxy.json +14 -0
- package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3903-platform-docker-compose-host-reject-coolify.json +14 -0
- package/blueprints/platform-docker-compose-host/contributions/adrs/adr-3904-platform-docker-compose-host-log-driver.json +14 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/caddyfile-validate.mjs +108 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/compose-config-lint.mjs +231 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/probe-utils.mjs +185 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/real-account-minimal-stack-up.mjs +41 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/real-account-reload-burst.mjs +36 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/run-caddyfile-validate.mjs +5 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/run-compose-config-lint.mjs +5 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/run-real-account-minimal-stack-up.mjs +5 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/run-real-account-reload-burst.mjs +5 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/run-secrets-as-files-scan.mjs +5 -0
- package/blueprints/platform-docker-compose-host/contributions/probes/secrets-as-files-scan.mjs +116 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-001.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-002.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-003.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-004.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-005.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/requirements/platform-docker-compose-host-req-006.json +18 -0
- package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3901-platform-docker-compose-host-compose-layout.json +26 -0
- package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3902-platform-docker-compose-host-secrets-mount.json +26 -0
- package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3903-platform-docker-compose-host-healthcheck-lint.json +26 -0
- package/blueprints/platform-docker-compose-host/contributions/tacs/tac-3904-platform-docker-compose-host-reverse-proxy-artefact.json +26 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38101.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38102.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38103.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38104.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38105.json +25 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38106.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38107.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38108.json +24 -0
- package/blueprints/platform-docker-compose-host/contributions/user-stories/platform-docker-compose-host-us-38109.json +24 -0
- package/blueprints/platform-docker-compose-host/docs/topics.md +71 -0
- package/blueprints/platform-docker-compose-host/guide/platform-docker-compose-host.md +94 -0
- package/blueprints/security-auth-clerk/docs/topics.md +3 -0
- package/blueprints/security-auth-keycloak/docs/topics.md +3 -0
- package/blueprints/security-auth-magic-link/docs/topics.md +3 -0
- package/blueprints/security-auth-oauth2/docs/topics.md +3 -0
- package/blueprints/security-secrets-management/docs/topics.md +3 -0
- package/fixtures/canary-manifest.json +9 -9
- package/package.json +1 -1
- package/rcf/adrs/adr-3801-deploy-hetzner-server-cloud-host-contract.json +13 -0
- package/rcf/adrs/adr-3802-deploy-hetzner-server-snapshot-cadence.json +13 -0
- package/rcf/adrs/adr-3803-deploy-hetzner-server-provisioning-tool.json +13 -0
- package/rcf/adrs/adr-3804-deploy-hetzner-server-hardening-baseline.json +13 -0
- package/rcf/adrs/adr-3901-platform-docker-compose-host-contract.json +13 -0
- package/rcf/adrs/adr-3902-platform-docker-compose-host-reverse-proxy.json +13 -0
- package/rcf/adrs/adr-3903-platform-docker-compose-host-reject-coolify.json +13 -0
- package/rcf/adrs/adr-3904-platform-docker-compose-host-log-driver.json +13 -0
- package/rcf/adrs/adr-4001-edge-cloudflare-tunnel-ingress-bridge-contract.json +13 -0
- package/rcf/adrs/adr-4002-edge-cloudflare-tunnel-connector-runtime.json +13 -0
- package/rcf/adrs/adr-4003-edge-cloudflare-tunnel-hostname-mode.json +13 -0
- package/rcf/code-nodes/cn-410.json +20 -0
- package/rcf/code-nodes/cn-440.json +20 -0
- package/rcf/code-nodes/cn-470.json +19 -0
- package/rcf/fbs/fbs-130.json +23 -0
- package/rcf/fbs/fbs-140.json +23 -0
- package/rcf/fbs/fbs-150.json +22 -0
- package/rcf/requirements/req-110.json +58 -0
- package/rcf/requirements/req-111.json +32 -0
- package/rcf/requirements/req-112.json +42 -0
- package/rcf/requirements/req-113.json +33 -0
- package/rcf/requirements/req-114.json +20 -0
- package/rcf/requirements/req-115.json +38 -0
- package/rcf/requirements/req-120.json +20 -0
- package/rcf/requirements/req-121.json +20 -0
- package/rcf/requirements/req-122.json +20 -0
- package/rcf/requirements/req-123.json +42 -0
- package/rcf/requirements/req-124.json +27 -0
- package/rcf/requirements/req-125.json +20 -0
- package/rcf/requirements/req-130.json +32 -0
- package/rcf/requirements/req-131.json +54 -0
- package/rcf/requirements/req-132.json +27 -0
- package/rcf/requirements/req-133.json +20 -0
- package/rcf/requirements/req-134.json +42 -0
- package/rcf/tacs/tac-3801-deploy-hetzner-server-provisioner.json +17 -0
- package/rcf/tacs/tac-3802-deploy-hetzner-server-manifest-schema.json +17 -0
- package/rcf/tacs/tac-3803-deploy-hetzner-server-cloud-init-template.json +17 -0
- package/rcf/tacs/tac-3804-deploy-hetzner-server-firewall-shape.json +16 -0
- package/rcf/tacs/tac-3901-platform-docker-compose-host-compose-layout.json +17 -0
- package/rcf/tacs/tac-3902-platform-docker-compose-host-secrets-mount.json +17 -0
- package/rcf/tacs/tac-3903-platform-docker-compose-host-healthcheck-lint.json +17 -0
- package/rcf/tacs/tac-3904-platform-docker-compose-host-reverse-proxy-artefact.json +17 -0
- package/rcf/tacs/tac-4001-edge-cloudflare-tunnel-connector-shape.json +17 -0
- package/rcf/tacs/tac-4002-edge-cloudflare-tunnel-manifest-schema.json +17 -0
- package/rcf/tacs/tac-4003-edge-cloudflare-tunnel-access-binding.json +16 -0
- package/rcf/test-suites/ts-140.json +86 -0
- package/rcf/test-suites/ts-150.json +86 -0
- package/rcf/test-suites/ts-160.json +78 -0
- package/rcf/user-stories/us-11001.json +23 -0
- package/rcf/user-stories/us-11101.json +23 -0
- package/rcf/user-stories/us-11102.json +24 -0
- package/rcf/user-stories/us-11201.json +23 -0
- package/rcf/user-stories/us-11202.json +23 -0
- package/rcf/user-stories/us-11301.json +24 -0
- package/rcf/user-stories/us-11401.json +23 -0
- package/rcf/user-stories/us-11402.json +23 -0
- package/rcf/user-stories/us-11501.json +23 -0
- package/rcf/user-stories/us-12001.json +23 -0
- package/rcf/user-stories/us-12101.json +23 -0
- package/rcf/user-stories/us-12102.json +23 -0
- package/rcf/user-stories/us-12201.json +23 -0
- package/rcf/user-stories/us-12202.json +24 -0
- package/rcf/user-stories/us-12301.json +23 -0
- package/rcf/user-stories/us-12401.json +23 -0
- package/rcf/user-stories/us-12402.json +23 -0
- package/rcf/user-stories/us-12501.json +23 -0
- package/rcf/user-stories/us-13001.json +23 -0
- package/rcf/user-stories/us-13002.json +23 -0
- package/rcf/user-stories/us-13101.json +23 -0
- package/rcf/user-stories/us-13102.json +23 -0
- package/rcf/user-stories/us-13201.json +23 -0
- package/rcf/user-stories/us-13301.json +23 -0
- package/rcf/user-stories/us-13302.json +23 -0
- package/rcf/user-stories/us-13401.json +23 -0
- package/releases/releases.yaml +11 -1
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
{
|
|
2
|
+
"slug": "platform-docker-compose-host",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"category": "platform",
|
|
5
|
+
"capabilities": [
|
|
6
|
+
"containerHost"
|
|
7
|
+
],
|
|
8
|
+
"suggestedCompanions": [
|
|
9
|
+
{
|
|
10
|
+
"role": "logging",
|
|
11
|
+
"reason": "Every composeStackReady, containerHealthy, containerCrashed and reverseProxyReloaded event writes through the applied logger; a logging companion supplies the factory and the log-driver binding."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"role": "errorHandling",
|
|
15
|
+
"reason": "A healthcheck timeout, a compose lint refusal, a secret-file-mode mismatch, a reload window overshoot constructs an internal error record; an error-handling companion supplies the record factory and the boundary."
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"elicits": [
|
|
19
|
+
{
|
|
20
|
+
"id": "reverse-proxy",
|
|
21
|
+
"prompt": "Reverse proxy in front of the compose services. caddy is the shipped default per Baz decision 19 (matches dev-01 and ops-01); traefik is the alternative for docker-label auto-discovery; none is the alternative for projects fronted only by the round-7 tunnel.",
|
|
22
|
+
"kind": "enum",
|
|
23
|
+
"options": [
|
|
24
|
+
"caddy",
|
|
25
|
+
"traefik",
|
|
26
|
+
"none"
|
|
27
|
+
],
|
|
28
|
+
"default": "caddy"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "docker-engine-channel",
|
|
32
|
+
"prompt": "Docker Engine apt channel per the Docker Engine install-on-Ubuntu docs. stable is the shipped default; test is for pre-release verification; the vendor also documents nightly builds available separately.",
|
|
33
|
+
"kind": "enum",
|
|
34
|
+
"options": [
|
|
35
|
+
"stable",
|
|
36
|
+
"test"
|
|
37
|
+
],
|
|
38
|
+
"default": "stable"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "compose-file-location",
|
|
42
|
+
"prompt": "Where the compose.yaml lives in the applying repo. repo-root is the shipped default; the alternative name is used when a project already owns compose.yaml for another purpose.",
|
|
43
|
+
"kind": "enum",
|
|
44
|
+
"options": [
|
|
45
|
+
"repo-root",
|
|
46
|
+
"compose/compose.yaml"
|
|
47
|
+
],
|
|
48
|
+
"default": "repo-root"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "healthcheck-default-kind",
|
|
52
|
+
"prompt": "Default healthcheck kind per service. http covers the common web service (curl or wget to a /health path); tcp covers a database or plain socket service; command lets the operator supply a bespoke test string.",
|
|
53
|
+
"kind": "enum",
|
|
54
|
+
"options": [
|
|
55
|
+
"http",
|
|
56
|
+
"tcp",
|
|
57
|
+
"command"
|
|
58
|
+
],
|
|
59
|
+
"default": "http"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "log-driver",
|
|
63
|
+
"prompt": "Container log driver per ADR-3904. journald is the shipped default matching the Debian 12 and Ubuntu 24.04 baseline T-1 sets up; loki is the opt-in alternative when the project runs Grafana.",
|
|
64
|
+
"kind": "enum",
|
|
65
|
+
"options": [
|
|
66
|
+
"journald",
|
|
67
|
+
"loki"
|
|
68
|
+
],
|
|
69
|
+
"default": "journald"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"id": "reload-window-seconds",
|
|
73
|
+
"prompt": "Zero-downtime reverse-proxy reload window in seconds (the maximum time a client burst may see any non-2xx or dropped connection during a reload). 10 is the shipped default per REQ-005.",
|
|
74
|
+
"kind": "string",
|
|
75
|
+
"default": "10"
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
"contributions": [
|
|
79
|
+
{
|
|
80
|
+
"id": "platform-docker-compose-host-REQ-001",
|
|
81
|
+
"kind": "req",
|
|
82
|
+
"path": "requirements/platform-docker-compose-host-req-001.json"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"id": "platform-docker-compose-host-REQ-002",
|
|
86
|
+
"kind": "req",
|
|
87
|
+
"path": "requirements/platform-docker-compose-host-req-002.json"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"id": "platform-docker-compose-host-REQ-003",
|
|
91
|
+
"kind": "req",
|
|
92
|
+
"path": "requirements/platform-docker-compose-host-req-003.json"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"id": "platform-docker-compose-host-REQ-004",
|
|
96
|
+
"kind": "req",
|
|
97
|
+
"path": "requirements/platform-docker-compose-host-req-004.json"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"id": "platform-docker-compose-host-REQ-005",
|
|
101
|
+
"kind": "req",
|
|
102
|
+
"path": "requirements/platform-docker-compose-host-req-005.json"
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"id": "platform-docker-compose-host-REQ-006",
|
|
106
|
+
"kind": "req",
|
|
107
|
+
"path": "requirements/platform-docker-compose-host-req-006.json"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"id": "platform-docker-compose-host-US-38101",
|
|
111
|
+
"kind": "us",
|
|
112
|
+
"path": "user-stories/platform-docker-compose-host-us-38101.json"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"id": "platform-docker-compose-host-US-38102",
|
|
116
|
+
"kind": "us",
|
|
117
|
+
"path": "user-stories/platform-docker-compose-host-us-38102.json"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"id": "platform-docker-compose-host-US-38103",
|
|
121
|
+
"kind": "us",
|
|
122
|
+
"path": "user-stories/platform-docker-compose-host-us-38103.json"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"id": "platform-docker-compose-host-US-38104",
|
|
126
|
+
"kind": "us",
|
|
127
|
+
"path": "user-stories/platform-docker-compose-host-us-38104.json"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"id": "platform-docker-compose-host-US-38105",
|
|
131
|
+
"kind": "us",
|
|
132
|
+
"path": "user-stories/platform-docker-compose-host-us-38105.json"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"id": "platform-docker-compose-host-US-38106",
|
|
136
|
+
"kind": "us",
|
|
137
|
+
"path": "user-stories/platform-docker-compose-host-us-38106.json"
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"id": "platform-docker-compose-host-US-38107",
|
|
141
|
+
"kind": "us",
|
|
142
|
+
"path": "user-stories/platform-docker-compose-host-us-38107.json"
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"id": "platform-docker-compose-host-US-38108",
|
|
146
|
+
"kind": "us",
|
|
147
|
+
"path": "user-stories/platform-docker-compose-host-us-38108.json"
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"id": "platform-docker-compose-host-US-38109",
|
|
151
|
+
"kind": "us",
|
|
152
|
+
"path": "user-stories/platform-docker-compose-host-us-38109.json"
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"id": "TAC-3901-platform-docker-compose-host-compose-layout",
|
|
156
|
+
"kind": "tac",
|
|
157
|
+
"path": "tacs/tac-3901-platform-docker-compose-host-compose-layout.json"
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"id": "TAC-3902-platform-docker-compose-host-secrets-mount",
|
|
161
|
+
"kind": "tac",
|
|
162
|
+
"path": "tacs/tac-3902-platform-docker-compose-host-secrets-mount.json"
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"id": "TAC-3903-platform-docker-compose-host-healthcheck-lint",
|
|
166
|
+
"kind": "tac",
|
|
167
|
+
"path": "tacs/tac-3903-platform-docker-compose-host-healthcheck-lint.json"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"id": "TAC-3904-platform-docker-compose-host-reverse-proxy-artefact",
|
|
171
|
+
"kind": "tac",
|
|
172
|
+
"path": "tacs/tac-3904-platform-docker-compose-host-reverse-proxy-artefact.json"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"id": "ADR-3901-platform-docker-compose-host-contract",
|
|
176
|
+
"kind": "adr",
|
|
177
|
+
"path": "adrs/adr-3901-platform-docker-compose-host-contract.json",
|
|
178
|
+
"scope": "global",
|
|
179
|
+
"topic": "containerHostContract",
|
|
180
|
+
"recommendedDefault": true,
|
|
181
|
+
"elicited": false,
|
|
182
|
+
"standardsTraceClause": "Docker Compose file reference AND Docker Engine install-on-Ubuntu docs per hetzner-round-7-spec-2026-09-07.md"
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"id": "ADR-3902-platform-docker-compose-host-reverse-proxy",
|
|
186
|
+
"kind": "adr",
|
|
187
|
+
"path": "adrs/adr-3902-platform-docker-compose-host-reverse-proxy.json",
|
|
188
|
+
"recommendedDefault": true,
|
|
189
|
+
"elicited": true,
|
|
190
|
+
"standardsTraceClause": "Caddy docs AND Traefik docs per hetzner-round-7-spec-2026-09-07.md"
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"id": "ADR-3903-platform-docker-compose-host-reject-coolify",
|
|
194
|
+
"kind": "adr",
|
|
195
|
+
"path": "adrs/adr-3903-platform-docker-compose-host-reject-coolify.json",
|
|
196
|
+
"recommendedDefault": false,
|
|
197
|
+
"elicited": false,
|
|
198
|
+
"standardsTraceClause": "generic enterprise practice"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"id": "ADR-3904-platform-docker-compose-host-log-driver",
|
|
202
|
+
"kind": "adr",
|
|
203
|
+
"path": "adrs/adr-3904-platform-docker-compose-host-log-driver.json",
|
|
204
|
+
"recommendedDefault": true,
|
|
205
|
+
"elicited": true,
|
|
206
|
+
"standardsTraceClause": "generic enterprise practice"
|
|
207
|
+
}
|
|
208
|
+
]
|
|
209
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"adrId": "ADR-3901-platform-docker-compose-host-contract",
|
|
3
|
+
"prdId": "PRD-001",
|
|
4
|
+
"tadId": "TAD-001",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"status": "accepted",
|
|
7
|
+
"title": "Container host contract: docker compose as source of truth on a cloudHost (scope global, new topic containerHostContract)",
|
|
8
|
+
"context": "An applying project running its stack on a Linux cloudHost needs one shipped contract for how services are declared, wired and brought up on a container-orchestration surface. Round-7 T-2 mints the containerHostContract topic on this ADR. Docker Compose is the shipped v1.0.0 surface (compose.yaml as source of truth, healthchecks required, secrets as files, restart discipline, reverse-proxy as artefact). Every applied container host needs one project-wide answer to the containerHostContract topic; a future non-compose sibling (Docker Swarm, Nomad) would contribute the same topic string with a different mechanism, forcing a DELIBERATE conflict the operator resolves per the round-2 conflict pattern. Standards trace clause: Docker Compose file reference AND Docker Engine install-on-Ubuntu docs per hetzner-round-7-spec-2026-09-07.md.",
|
|
9
|
+
"decision": "The shipped v1.0.0 default is docker compose on a cloudHost as the containerHostContract provider. A canonical compose.yaml at the repo root drives docker compose up on the applied cloudHost (T-1 deploy-hetzner-server); healthchecks refuse at docker compose config on missing; secrets are file mounts; restart policies are unless-stopped or on-failure; the reverse-proxy config is a first-class versioned artefact per TAC-3904. The containerHostContract topic scope is global on this blueprint; the topic string is containerHostContract (lower camelCase, one concept per topic, no version suffix, per the round-2 rules-for-new-topics clause on the shelf registry). Standards trace clause: Docker Compose file reference AND Docker Engine install-on-Ubuntu docs per hetzner-round-7-spec-2026-09-07.md.",
|
|
10
|
+
"consequences": "Consumer blueprints that ship as containers declare capabilities: [containerHost] on their applied contract; the round-7 T-3 edge-cloudflare-tunnel reads this capability at apply-time to choose the compose-service runtime. Rebuild-from-manifest is always possible: the compose file is the source of truth, the running stack is derivative. Compose is famously many right ways; over-specifying alienates a project with a defensible different shape, so v1.0.0 pins the small load-bearing set (healthchecks, secrets-as-files, restart discipline, reverse-proxy-as-artefact) and leaves everything else to project-owned ADRs.",
|
|
11
|
+
"alternativesConsidered": [],
|
|
12
|
+
"createdAt": "2026-09-08T00:00:00.000Z",
|
|
13
|
+
"updatedAt": "2026-09-08T00:00:00.000Z"
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"adrId": "ADR-3902-platform-docker-compose-host-reverse-proxy",
|
|
3
|
+
"prdId": "PRD-001",
|
|
4
|
+
"tadId": "TAD-001",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"status": "accepted",
|
|
7
|
+
"title": "Reverse proxy choice: caddy default (matches dev-01 and ops-01), traefik alternative for docker-label auto-discovery, none for tunnel-fronted projects (elicited)",
|
|
8
|
+
"context": "Compose stacks need a reverse-proxy for HTTPS termination and hostname-to-service routing. Caddy is the estate baseline (dev-01 and ops-01 already run it per Baz decision 19 on 2026-09-07 09:50Z). Traefik has a docker-label auto-discovery model some projects prefer. Some projects front their stack only with the round-7 tunnel and skip a reverse proxy inside compose.",
|
|
9
|
+
"decision": "The reverse-proxy choice is elicited via the reverseProxy parameter. The shipped default is caddy per Baz decision 19. The alternatives are traefik (for projects that want docker-label auto-discovery) and none (for projects fronted only by the round-7 tunnel). Standards trace clause: Caddy docs AND Traefik docs per hetzner-round-7-spec-2026-09-07.md.",
|
|
10
|
+
"consequences": "A project applying reverseProxy=caddy gets the caddy/Caddyfile artefact and the caddy reload verb. A project applying reverseProxy=traefik gets the traefik/traefik.yaml artefact and the Traefik equivalent reload verb; docker-label auto-discovery is a project-side pattern the guide names. A project applying reverseProxy=none omits the reverse-proxy service and must apply edge-cloudflare-tunnel (T-3) so ingress terminates at the CF edge.",
|
|
11
|
+
"alternativesConsidered": [],
|
|
12
|
+
"createdAt": "2026-09-08T00:00:00.000Z",
|
|
13
|
+
"updatedAt": "2026-09-08T00:00:00.000Z"
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"adrId": "ADR-3903-platform-docker-compose-host-reject-coolify",
|
|
3
|
+
"prdId": "PRD-001",
|
|
4
|
+
"tadId": "TAD-001",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"status": "accepted",
|
|
7
|
+
"title": "Coolify rejected per Baz decision 20 (it shadows what the blueprints contract and adds its own DB and UI as a second source of state)",
|
|
8
|
+
"context": "Coolify was proposed as a candidate platform-* sibling in the round-7 proposal. It is a self-hosted PaaS with its own database and UI. Baz decision 20 on 2026-09-07 09:50Z ruled Coolify out for round 7.",
|
|
9
|
+
"decision": "reject; it shadows what the blueprints contract and adds its own DB and UI as a second source of state.",
|
|
10
|
+
"consequences": "No platform-coolify-host blueprint is scoped in round 7 and no reserved slot is allocated. A future platform-coolify-host sibling mints on operator demand under a fresh scope PR. The standards trace clause on the contribution entry is generic enterprise practice; no vendor-doc URL is claimed for the reject decision.",
|
|
11
|
+
"alternativesConsidered": [],
|
|
12
|
+
"createdAt": "2026-09-08T00:00:00.000Z",
|
|
13
|
+
"updatedAt": "2026-09-08T00:00:00.000Z"
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"adrId": "ADR-3904-platform-docker-compose-host-log-driver",
|
|
3
|
+
"prdId": "PRD-001",
|
|
4
|
+
"tadId": "TAD-001",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"status": "accepted",
|
|
7
|
+
"title": "Log driver: journald default matching Debian 12 and Ubuntu 24.04 baseline; loki opt-in when the project runs Grafana (elicited)",
|
|
8
|
+
"context": "Containers write stdout and stderr to a log driver. Debian 12 and Ubuntu 24.04 (the T-1 cloud-init baseline) ship journald. Grafana users on the estate reach for loki as a native sink. A one-choice hard-code loses either journald parity with the OS or loki when the operator runs Grafana.",
|
|
9
|
+
"decision": "The log driver is elicited via the log-driver parameter. The shipped default is journald matching the Debian 12 and Ubuntu 24.04 baselines T-1 sets up. The alternative is loki, opted into when the applying project runs Grafana. When observability-logging is applied, the compose file wires the elicited driver to that companion. Standards trace clause: generic enterprise practice.",
|
|
10
|
+
"consequences": "A project applying log-driver=journald gets the systemd-native path; the journald sink is queried via journalctl. A project applying log-driver=loki targets the Grafana loki endpoint; the observability-logging companion supplies the endpoint address. Neither choice adds a runtime npm dependency. The compose-config-lint refuses on any driver value outside the allowed set.",
|
|
11
|
+
"alternativesConsidered": [],
|
|
12
|
+
"createdAt": "2026-09-08T00:00:00.000Z",
|
|
13
|
+
"updatedAt": "2026-09-08T00:00:00.000Z"
|
|
14
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Probe: caddyfile-validate.
|
|
2
|
+
//
|
|
3
|
+
// anchorAcId: AC-composeHost-reverseProxyArtefactValid.
|
|
4
|
+
// accountBound: false.
|
|
5
|
+
//
|
|
6
|
+
// Runs caddy validate against the applied fixture caddy/Caddyfile. When
|
|
7
|
+
// a local caddy binary is not on PATH the probe uses the caddy:2
|
|
8
|
+
// container image via docker run --rm (pinned tag), so review machines
|
|
9
|
+
// without a local caddy still get a real validation. When neither is
|
|
10
|
+
// available the probe records skipped with a warn note.
|
|
11
|
+
//
|
|
12
|
+
// The applying project may set reverseProxy to a value other than caddy
|
|
13
|
+
// (traefik or none per ADR-3902); when that happens the probe returns
|
|
14
|
+
// skipped with note.
|
|
15
|
+
//
|
|
16
|
+
// Mutation:
|
|
17
|
+
// - SIMULATE_INVALID_CADDYFILE=true writes an obviously-invalid
|
|
18
|
+
// directive (\"not-a-directive-at-all\") into a scratch copy of the
|
|
19
|
+
// Caddyfile; the probe FAILS naming the offending line.
|
|
20
|
+
|
|
21
|
+
import { readFile, writeFile, mkdir, rm } from 'node:fs/promises';
|
|
22
|
+
import { spawnSync } from 'node:child_process';
|
|
23
|
+
import { tmpdir } from 'node:os';
|
|
24
|
+
import { join, dirname } from 'node:path';
|
|
25
|
+
import { runShim, CADDYFILE_PATH, whichCaddy, whichDocker } from './probe-utils.mjs';
|
|
26
|
+
|
|
27
|
+
export const anchorAcId = 'AC-composeHost-reverseProxyArtefactValid';
|
|
28
|
+
export const accountBound = false;
|
|
29
|
+
|
|
30
|
+
function isCaddyReverseProxy() {
|
|
31
|
+
// Applying project passes reverseProxy via env for the probe run; the
|
|
32
|
+
// shipped default per ADR-3902 is caddy.
|
|
33
|
+
const v = process.env.REVERSE_PROXY;
|
|
34
|
+
if (v === undefined || v === 'caddy') return true;
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default async function runProbe() {
|
|
39
|
+
const results = [];
|
|
40
|
+
const extra = {};
|
|
41
|
+
extra.mutations = { invalidCaddyfile: process.env.SIMULATE_INVALID_CADDYFILE === 'true' };
|
|
42
|
+
extra.reverseProxy = process.env.REVERSE_PROXY ?? 'caddy';
|
|
43
|
+
if (!isCaddyReverseProxy()) {
|
|
44
|
+
results.push({
|
|
45
|
+
anchorAcId,
|
|
46
|
+
verdict: 'skipped',
|
|
47
|
+
detail: `reverseProxy=${extra.reverseProxy}: caddyfile-validate skipped (applies only when reverseProxy=caddy)`,
|
|
48
|
+
});
|
|
49
|
+
return { results, extra };
|
|
50
|
+
}
|
|
51
|
+
const scratchDir = join(tmpdir(), `rcf-lite-t2-caddyfile-validate-${Date.now()}`);
|
|
52
|
+
await mkdir(scratchDir, { recursive: true });
|
|
53
|
+
const scratchPath = join(scratchDir, 'Caddyfile');
|
|
54
|
+
try {
|
|
55
|
+
let text = await readFile(CADDYFILE_PATH, 'utf8');
|
|
56
|
+
if (process.env.SIMULATE_INVALID_CADDYFILE === 'true') {
|
|
57
|
+
text = text + "\n{ unclosed_block_that_never_ends_and_is_a_syntax_error_caddy_will_reject\n";
|
|
58
|
+
}
|
|
59
|
+
await writeFile(scratchPath, text, 'utf8');
|
|
60
|
+
const localCaddy = whichCaddy();
|
|
61
|
+
const localDocker = whichDocker();
|
|
62
|
+
extra.caddyLocal = localCaddy;
|
|
63
|
+
extra.dockerLocal = localDocker;
|
|
64
|
+
let cmd, args, engineLabel;
|
|
65
|
+
if (localCaddy !== null) {
|
|
66
|
+
cmd = 'caddy';
|
|
67
|
+
args = ['validate', '--config', scratchPath, '--adapter', 'caddyfile'];
|
|
68
|
+
engineLabel = `local caddy ${localCaddy}`;
|
|
69
|
+
} else if (localDocker !== null) {
|
|
70
|
+
cmd = 'docker';
|
|
71
|
+
args = ['run', '--rm', '-v', `${scratchDir}:/etc/caddy:ro`, 'caddy:2',
|
|
72
|
+
'caddy', 'validate', '--config', '/etc/caddy/Caddyfile', '--adapter', 'caddyfile'];
|
|
73
|
+
engineLabel = 'caddy:2 container (docker run --rm)';
|
|
74
|
+
} else {
|
|
75
|
+
results.push({
|
|
76
|
+
anchorAcId,
|
|
77
|
+
verdict: 'warn',
|
|
78
|
+
detail: 'neither caddy binary nor docker on PATH; caddy validate could not be exercised',
|
|
79
|
+
});
|
|
80
|
+
return { results, extra };
|
|
81
|
+
}
|
|
82
|
+
const r = spawnSync(cmd, args, { encoding: 'utf8', timeout: 60_000 });
|
|
83
|
+
extra.exitStatus = r.status;
|
|
84
|
+
extra.engineLabel = engineLabel;
|
|
85
|
+
const tail = ((r.stderr || '') + (r.stdout || '')).split('\n').slice(-10).join('\n');
|
|
86
|
+
if (r.status === 0) {
|
|
87
|
+
results.push({
|
|
88
|
+
anchorAcId,
|
|
89
|
+
verdict: 'pass',
|
|
90
|
+
detail: `caddy validate exit 0 (${engineLabel}); tail: ${tail.slice(-200)}`,
|
|
91
|
+
});
|
|
92
|
+
} else {
|
|
93
|
+
results.push({
|
|
94
|
+
anchorAcId,
|
|
95
|
+
verdict: 'fail',
|
|
96
|
+
detail: `caddy validate exit ${r.status} (${engineLabel}); tail: ${tail.slice(-400)}`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
} finally {
|
|
100
|
+
await rm(scratchDir, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
return { results, extra };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const engine = { kind: 'caddy-validate', image: 'caddy:2 (or local caddy binary)', healthy: true };
|
|
106
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
107
|
+
await runShim('caddyfile-validate', engine, runProbe);
|
|
108
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// Probe: compose-config-lint.
|
|
2
|
+
//
|
|
3
|
+
// anchorAcId list (multi): AC-composeHost-healthcheckLint (primary),
|
|
4
|
+
// AC-composeHost-restartClassification, AC-composeHost-logDriverClassification.
|
|
5
|
+
// accountBound: false.
|
|
6
|
+
//
|
|
7
|
+
// Passes when:
|
|
8
|
+
// - docker compose config exits 0 against the applied fixture compose.yaml
|
|
9
|
+
// (the daemon must be reachable; when the docker CLI is missing, the
|
|
10
|
+
// lint stage records a warn result so the source-tree assertions still
|
|
11
|
+
// surface).
|
|
12
|
+
// - every HTTP-terminating service (web) declares a healthcheck block.
|
|
13
|
+
// - every service declares a restart policy whose value is unless-stopped
|
|
14
|
+
// or on-failure.
|
|
15
|
+
// - every service declares a logging driver whose value is journald or loki.
|
|
16
|
+
// - a scan of the composeStackReady lifecycle event body captured by the
|
|
17
|
+
// in-process facade proves no secret literal leaks.
|
|
18
|
+
//
|
|
19
|
+
// Mutations (each fails; each names the offending service or literal):
|
|
20
|
+
// - SIMULATE_MISSING_HEALTHCHECK=true strips the healthcheck: block from
|
|
21
|
+
// the web service before the compose-config-lint scan.
|
|
22
|
+
// - SIMULATE_UNCLASSIFIED_RESTART=true rewrites restart: unless-stopped
|
|
23
|
+
// on the web service to restart: always.
|
|
24
|
+
// - SIMULATE_UNCLASSIFIED_LOG_DRIVER=true rewrites the web service
|
|
25
|
+
// logging.driver from journald to syslog.
|
|
26
|
+
// - SIMULATE_EVENT_SECRECY_LEAK=true injects the fixture web-token value
|
|
27
|
+
// into the composeStackReady event body; the event-secrecy scan FAILS.
|
|
28
|
+
|
|
29
|
+
import { spawnSync } from 'node:child_process';
|
|
30
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
31
|
+
import { resolve } from 'node:path';
|
|
32
|
+
import { runShim, readCompose, COMPOSE_PATH, FIXTURE_DIR, SECRET_PATH, whichDocker } from './probe-utils.mjs';
|
|
33
|
+
|
|
34
|
+
export const anchorAcIds = [
|
|
35
|
+
'AC-composeHost-healthcheckLint',
|
|
36
|
+
'AC-composeHost-restartClassification',
|
|
37
|
+
'AC-composeHost-logDriverClassification',
|
|
38
|
+
];
|
|
39
|
+
export const accountBound = false;
|
|
40
|
+
|
|
41
|
+
const HTTP_SERVICES = new Set(['web', 'caddy']);
|
|
42
|
+
const ALLOWED_RESTART = new Set(['unless-stopped', 'on-failure']);
|
|
43
|
+
const ALLOWED_LOG_DRIVER = new Set(['journald', 'loki']);
|
|
44
|
+
|
|
45
|
+
function applyMutations(text) {
|
|
46
|
+
let out = text;
|
|
47
|
+
if (process.env.SIMULATE_MISSING_HEALTHCHECK === 'true') {
|
|
48
|
+
// Strip the healthcheck: block from the web service by removing
|
|
49
|
+
// the healthcheck: line and its indented children.
|
|
50
|
+
const lines = out.split('\n');
|
|
51
|
+
const trimmed = [];
|
|
52
|
+
let stripping = false;
|
|
53
|
+
let stripIndent = 0;
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
if (stripping) {
|
|
56
|
+
const m = line.match(/^(\s*)/);
|
|
57
|
+
const indent = m ? m[1].length : 0;
|
|
58
|
+
if (line.trim() === '' || indent > stripIndent) continue;
|
|
59
|
+
stripping = false;
|
|
60
|
+
}
|
|
61
|
+
// Only strip inside the web service, not caddy.
|
|
62
|
+
if (line.match(/^\s{4}healthcheck:\s*$/) && trimmed.slice(-30).some((l) => l.match(/^\s{2}web:\s*$/))) {
|
|
63
|
+
const m = line.match(/^(\s*)/);
|
|
64
|
+
stripIndent = m ? m[1].length : 0;
|
|
65
|
+
stripping = true;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
trimmed.push(line);
|
|
69
|
+
}
|
|
70
|
+
out = trimmed.join('\n');
|
|
71
|
+
}
|
|
72
|
+
if (process.env.SIMULATE_UNCLASSIFIED_RESTART === 'true') {
|
|
73
|
+
out = out.replace(/(\n\s{2}web:[\s\S]*?)restart:\s*unless-stopped/, '$1restart: always');
|
|
74
|
+
}
|
|
75
|
+
if (process.env.SIMULATE_UNCLASSIFIED_LOG_DRIVER === 'true') {
|
|
76
|
+
out = out.replace(/(\n\s{2}web:[\s\S]*?logging:\s*\n\s+driver:\s*)journald/, '$1syslog');
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function scanServices(doc) {
|
|
82
|
+
const results = [];
|
|
83
|
+
const services = doc.services ?? {};
|
|
84
|
+
const names = Object.keys(services);
|
|
85
|
+
for (const name of names) {
|
|
86
|
+
const svc = services[name] ?? {};
|
|
87
|
+
if (HTTP_SERVICES.has(name)) {
|
|
88
|
+
if (!svc.healthcheck) {
|
|
89
|
+
results.push({
|
|
90
|
+
anchorAcId: 'AC-composeHost-healthcheckLint',
|
|
91
|
+
verdict: 'fail',
|
|
92
|
+
detail: `HTTP-terminating service '${name}' is missing a healthcheck: block`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const restart = svc.restart;
|
|
97
|
+
if (!restart) {
|
|
98
|
+
results.push({
|
|
99
|
+
anchorAcId: 'AC-composeHost-restartClassification',
|
|
100
|
+
verdict: 'fail',
|
|
101
|
+
detail: `service '${name}' declares no restart policy; allowed: unless-stopped, on-failure`,
|
|
102
|
+
});
|
|
103
|
+
} else if (!ALLOWED_RESTART.has(String(restart))) {
|
|
104
|
+
results.push({
|
|
105
|
+
anchorAcId: 'AC-composeHost-restartClassification',
|
|
106
|
+
verdict: 'fail',
|
|
107
|
+
detail: `service '${name}' declares restart: ${restart}; allowed: unless-stopped, on-failure`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const logging = svc.logging;
|
|
111
|
+
const driver = logging && logging.driver;
|
|
112
|
+
if (!driver) {
|
|
113
|
+
results.push({
|
|
114
|
+
anchorAcId: 'AC-composeHost-logDriverClassification',
|
|
115
|
+
verdict: 'fail',
|
|
116
|
+
detail: `service '${name}' declares no logging driver; allowed: journald, loki`,
|
|
117
|
+
});
|
|
118
|
+
} else if (!ALLOWED_LOG_DRIVER.has(String(driver))) {
|
|
119
|
+
results.push({
|
|
120
|
+
anchorAcId: 'AC-composeHost-logDriverClassification',
|
|
121
|
+
verdict: 'fail',
|
|
122
|
+
detail: `service '${name}' declares logging driver '${driver}'; allowed: journald, loki`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return results;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default async function runProbe() {
|
|
130
|
+
const results = [];
|
|
131
|
+
const extra = {};
|
|
132
|
+
// Source-tree scan on the (possibly mutated) compose file.
|
|
133
|
+
const originalText = await readFile(COMPOSE_PATH, 'utf8');
|
|
134
|
+
const mutatedText = applyMutations(originalText);
|
|
135
|
+
const { doc: origDoc } = await readCompose();
|
|
136
|
+
const { parseComposeYaml } = await import('./probe-utils.mjs');
|
|
137
|
+
const mutatedDoc = parseComposeYaml(mutatedText);
|
|
138
|
+
const scanResults = scanServices(mutatedDoc);
|
|
139
|
+
results.push(...scanResults);
|
|
140
|
+
extra.serviceNames = Object.keys(mutatedDoc.services ?? {});
|
|
141
|
+
extra.mutations = {
|
|
142
|
+
missingHealthcheck: process.env.SIMULATE_MISSING_HEALTHCHECK === 'true',
|
|
143
|
+
unclassifiedRestart: process.env.SIMULATE_UNCLASSIFIED_RESTART === 'true',
|
|
144
|
+
unclassifiedLogDriver: process.env.SIMULATE_UNCLASSIFIED_LOG_DRIVER === 'true',
|
|
145
|
+
eventSecrecyLeak: process.env.SIMULATE_EVENT_SECRECY_LEAK === 'true',
|
|
146
|
+
};
|
|
147
|
+
// docker compose config lint (on the mutated text, via a scratch file).
|
|
148
|
+
const dockerVersion = whichDocker();
|
|
149
|
+
extra.dockerVersion = dockerVersion;
|
|
150
|
+
if (dockerVersion === null) {
|
|
151
|
+
results.push({
|
|
152
|
+
anchorAcId: 'AC-composeHost-healthcheckLint',
|
|
153
|
+
verdict: 'warn',
|
|
154
|
+
detail: 'docker CLI absent on PATH; source-tree assertions ran but docker compose config was not exercised',
|
|
155
|
+
});
|
|
156
|
+
} else {
|
|
157
|
+
const { writeFile, mkdir } = await import('node:fs/promises');
|
|
158
|
+
const { tmpdir } = await import('node:os');
|
|
159
|
+
const { join } = await import('node:path');
|
|
160
|
+
const scratch = join(tmpdir(), `rcf-lite-t2-compose-config-lint-${Date.now()}`);
|
|
161
|
+
await mkdir(scratch, { recursive: true });
|
|
162
|
+
await writeFile(join(scratch, 'compose.yaml'), mutatedText, 'utf8');
|
|
163
|
+
// Copy .env, caddy/, secrets/, src/ so docker compose config can resolve references
|
|
164
|
+
const { cp } = await import('node:fs/promises');
|
|
165
|
+
for (const rel of ['.env', 'caddy', 'secrets', 'src']) {
|
|
166
|
+
try {
|
|
167
|
+
await cp(join(FIXTURE_DIR, rel), join(scratch, rel), { recursive: true });
|
|
168
|
+
} catch (err) {
|
|
169
|
+
// best-effort; docker compose config still validates syntax
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const cfg = spawnSync('docker', ['compose', '-f', join(scratch, 'compose.yaml'), 'config'], {
|
|
173
|
+
encoding: 'utf8', timeout: 30_000,
|
|
174
|
+
});
|
|
175
|
+
if (cfg.status === 0) {
|
|
176
|
+
results.push({
|
|
177
|
+
anchorAcId: 'AC-composeHost-healthcheckLint',
|
|
178
|
+
verdict: 'pass',
|
|
179
|
+
detail: 'docker compose config exit 0 on the applied compose.yaml',
|
|
180
|
+
});
|
|
181
|
+
} else {
|
|
182
|
+
results.push({
|
|
183
|
+
anchorAcId: 'AC-composeHost-healthcheckLint',
|
|
184
|
+
verdict: 'fail',
|
|
185
|
+
detail: `docker compose config exit ${cfg.status}: ${(cfg.stderr || cfg.stdout || '').slice(0, 400)}`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
extra.dockerComposeConfigExit = cfg.status;
|
|
189
|
+
}
|
|
190
|
+
// Event-secrecy scan: capture a composeStackReady event body and assert
|
|
191
|
+
// no secret literal appears; SIMULATE_EVENT_SECRECY_LEAK injects one.
|
|
192
|
+
const secretValue = (await readFile(SECRET_PATH, 'utf8')).trim();
|
|
193
|
+
const event = {
|
|
194
|
+
kind: 'composeStackReady',
|
|
195
|
+
services: extra.serviceNames,
|
|
196
|
+
imageDigests: extra.serviceNames.map((n) => `sha256:fixture-${n}`),
|
|
197
|
+
healthTimesMs: extra.serviceNames.map(() => 42),
|
|
198
|
+
};
|
|
199
|
+
if (process.env.SIMULATE_EVENT_SECRECY_LEAK === 'true') {
|
|
200
|
+
event.debug = { boundToken: secretValue };
|
|
201
|
+
}
|
|
202
|
+
const eventBody = JSON.stringify(event);
|
|
203
|
+
if (eventBody.includes(secretValue)) {
|
|
204
|
+
results.push({
|
|
205
|
+
anchorAcId: 'AC-composeHost-healthcheckLint',
|
|
206
|
+
verdict: 'fail',
|
|
207
|
+
detail: `composeStackReady event body carries the fixture secret literal (event-secrecy leak): ${eventBody.slice(0, 200)}`,
|
|
208
|
+
});
|
|
209
|
+
} else {
|
|
210
|
+
results.push({
|
|
211
|
+
anchorAcId: 'AC-composeHost-healthcheckLint',
|
|
212
|
+
verdict: 'pass',
|
|
213
|
+
detail: 'composeStackReady event body carries no secret literal (event-secrecy scan clean)',
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
extra.eventBodyByteLength = eventBody.length;
|
|
217
|
+
// Passing summary line when nothing failed.
|
|
218
|
+
if (!results.some((r) => r.verdict === 'fail')) {
|
|
219
|
+
results.push({
|
|
220
|
+
anchorAcId: 'AC-composeHost-healthcheckLint',
|
|
221
|
+
verdict: 'pass',
|
|
222
|
+
detail: `compose-config-lint pass: services ${extra.serviceNames.join(', ')}; healthcheck + restart + logging + event-secrecy scans all clean`,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return { results, extra };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const engine = { kind: 'compose-config', image: 'docker compose config (local daemon)', healthy: true };
|
|
229
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
230
|
+
await runShim('compose-config-lint', engine, runProbe);
|
|
231
|
+
}
|