From a5d85266d0e0cebc0de1c31be6c437af57ce71ad Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 1 Sep 2026 17:19:04 +0200 Subject: [PATCH] A container does not take restart-on, and nine of them did MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is what stopped the forge. The host refused the whole declaration: resource "postgres.server": a container does not use "restart-on", and it is set. Refused rather than ignored `restart-on` belongs to a service. I put it on containers this morning so one would pick up a rotated credential — nine times across seven modules — and nothing between the manifest and the machine said a word. The control plane composed it happily; the parser accepted it; the manifest tests passed. The only thing that knew was the host, five steps downstream, and hearing from it cost a seventeen-minute run. The host was right twice over. It refused, and it refused *everything*, because applying the parts it understood would leave a machine that looks configured and is not. One misplaced key therefore stops a module dead, which is the correct severity and an argument for catching it where it is written. So the shapes and their keys are now written down here and checked. They are duplicated from another repository deliberately — this is its wire format, like the shape of a grant file — and a contract with two copies and no check is a contract until somebody edits one. What this does not fix is why I reached for it: a container cannot follow a file. Filed separately. --- examples/modules/gitea.json | 3 - examples/modules/keycloak.json | 108 ++++++++++++++++++++--------- examples/modules/mailu.json | 5 -- examples/modules/minio.json | 7 -- examples/modules/modules_test.go | 61 ++++++++++++++++ examples/modules/object-store.json | 108 +++++++++++++++++++---------- examples/modules/photos.json | 53 ++++++++------ examples/modules/postgres.json | 7 -- 8 files changed, 239 insertions(+), 113 deletions(-) diff --git a/examples/modules/gitea.json b/examples/modules/gitea.json index ba01a97..987add6 100644 --- a/examples/modules/gitea.json +++ b/examples/modules/gitea.json @@ -74,9 +74,6 @@ ], "volumes": [ "/services/gitea/gitea:/data" - ], - "restart-on": [ - "server-env" ] } ] diff --git a/examples/modules/keycloak.json b/examples/modules/keycloak.json index 9aab8f9..530dbf0 100644 --- a/examples/modules/keycloak.json +++ b/examples/modules/keycloak.json @@ -1,41 +1,81 @@ { "module": "keycloak", "version": "1", - - "requires": ["postgres-database"], - "contributes": { - "postgres-database": {"name": "keycloak"} - }, - "binds": {"postgres-database": "/var/lib/keycloak/database.json"}, - "secrets": {"postgres-database": "/var/lib/keycloak/database.secret"}, - - "capabilities": ["container-runtime"], - - "listens": [ - {"port": 8080, "protocol": "tcp", "from": "mesh", - "why": "anything the mesh runs that authenticates a person"} + "requires": [ + "postgres-database" ], - - "own-secrets": {"admin": "/var/lib/keycloak/admin.secret"}, - + "contributes": { + "postgres-database": { + "name": "keycloak" + } + }, + "binds": { + "postgres-database": "/var/lib/keycloak/database.json" + }, + "secrets": { + "postgres-database": "/var/lib/keycloak/database.secret" + }, + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 8080, + "protocol": "tcp", + "from": "mesh", + "why": "anything the mesh runs that authenticates a person" + } + ], + "own-secrets": { + "admin": "/var/lib/keycloak/admin.secret" + }, "resources": [ - {"id": "state", "type": "directory", "path": "/var/lib/keycloak", "mode": "0700"}, - - {"id": "admin-env", "type": "file", "path": "/var/lib/keycloak/admin.env", "mode": "0600", - "content": "KEYCLOAK_ADMIN=admin\nKEYCLOAK_ADMIN_PASSWORD=${secret:admin}\n"}, - - {"id": "database-env", "type": "file", "path": "/var/lib/keycloak/database.env", "mode": "0600", - "content": "KC_DB_URL=jdbc:postgresql://${bound:postgres-database:at}:${bound:postgres-database:port}/keycloak\nKC_DB_USERNAME=${bound:postgres-database:as}\nKC_DB_PASSWORD=${secret:postgres-database}\n"}, - - {"id": "net", "type": "network", "name": "keycloak"}, - - {"id": "server", "type": "container", "name": "keycloak", - "image": "quay.io/keycloak/keycloak@sha256:ecd43971114b0c764f8a3288dddab73f98cb473daccc4feaffe4dc14adeaf866", - "network": "keycloak", - "args": ["start-dev"], - "env": {"KC_DB": "postgres", "KC_HTTP_ENABLED": "true", "KC_HEALTH_ENABLED": "true"}, - "env-file": ["/var/lib/keycloak/admin.env", "/var/lib/keycloak/database.env"], - "ports": ["8080:8080"], - "restart-on": ["admin-env", "database-env"]} + { + "id": "state", + "type": "directory", + "path": "/var/lib/keycloak", + "mode": "0700" + }, + { + "id": "admin-env", + "type": "file", + "path": "/var/lib/keycloak/admin.env", + "mode": "0600", + "content": "KEYCLOAK_ADMIN=admin\nKEYCLOAK_ADMIN_PASSWORD=${secret:admin}\n" + }, + { + "id": "database-env", + "type": "file", + "path": "/var/lib/keycloak/database.env", + "mode": "0600", + "content": "KC_DB_URL=jdbc:postgresql://${bound:postgres-database:at}:${bound:postgres-database:port}/keycloak\nKC_DB_USERNAME=${bound:postgres-database:as}\nKC_DB_PASSWORD=${secret:postgres-database}\n" + }, + { + "id": "net", + "type": "network", + "name": "keycloak" + }, + { + "id": "server", + "type": "container", + "name": "keycloak", + "image": "quay.io/keycloak/keycloak@sha256:ecd43971114b0c764f8a3288dddab73f98cb473daccc4feaffe4dc14adeaf866", + "network": "keycloak", + "args": [ + "start-dev" + ], + "env": { + "KC_DB": "postgres", + "KC_HTTP_ENABLED": "true", + "KC_HEALTH_ENABLED": "true" + }, + "env-file": [ + "/var/lib/keycloak/admin.env", + "/var/lib/keycloak/database.env" + ], + "ports": [ + "8080:8080" + ] + } ] } diff --git a/examples/modules/mailu.json b/examples/modules/mailu.json index 46bafca..c0d7a2c 100644 --- a/examples/modules/mailu.json +++ b/examples/modules/mailu.json @@ -264,11 +264,6 @@ "volumes": [ "/services/mailu/data/certs:/certs", "/services/mailu/data/overrides/nginx:/overrides:ro" - ], - "restart-on": [ - "imap", - "smtp", - "admin" ] } ] diff --git a/examples/modules/minio.json b/examples/modules/minio.json index 46aea2d..1feb6a5 100644 --- a/examples/modules/minio.json +++ b/examples/modules/minio.json @@ -85,9 +85,6 @@ ], "volumes": [ "/services/minio/data/data1-1:/data" - ], - "restart-on": [ - "root-env" ] }, { @@ -105,10 +102,6 @@ "volumes": [ "/var/lib/minio/grants:/var/lib/minio/grants:ro", "/var/lib/minio/root.secret:/run/secrets/root:ro" - ], - "restart-on": [ - "grants", - "root-env" ] } ] diff --git a/examples/modules/modules_test.go b/examples/modules/modules_test.go index abb3f21..cbd1a2a 100644 --- a/examples/modules/modules_test.go +++ b/examples/modules/modules_test.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "regexp" + "slices" "strings" "testing" @@ -569,3 +570,63 @@ func stringsOfTest(v any) []string { } return out } + +// A resource uses only the keys its shape has. +// +// **The host is the only thing that knew, and it is five steps downstream.** A container carrying +// `restart-on` — which belongs to a service — composed into a declaration without complaint, was +// pushed, and was refused on the machine. The host refused *the whole declaration*, correctly, +// because applying the parts it understood would leave a machine that looks configured and is +// not. So one misplaced key stopped a module dead, and the only place that said so was a log on a +// lab machine after a seventeen-minute run. +// +// Nine of them had shipped across seven modules. +// +// The lists are written out rather than imported: the host is another repository and this is its +// wire format, like the shape of a grant file. Duplicated deliberately, and checked — a contract +// with two copies and no check is a contract until somebody edits one. +func TestAResourceUsesOnlyTheKeysItsShapeHas(t *testing.T) { + common := []string{"id", "type"} + shapes := map[string][]string{ + "file": {"path", "content", "bytes", "sealed", "secrets", "mode", "owner"}, + "directory": {"path", "mode", "owner"}, + "container": {"name", "image", "env", "env-file", "ports", "volumes", "args", "hosts", "network", "artifact"}, + "service": {"unit", "state", "boot", "restart-on"}, + "package": {"package", "state"}, + "network": {"name"}, + "archive": {"path", "artifact", "digest", "owner", "mode"}, + "user": {"name", "shell", "groups", "home"}, + "action": {"command", "verify", "in"}, + } + + found, _ := filepath.Glob("*.json") + var checked int + for _, name := range found { + for _, r := range read(t, name).Resources { + kind := fmt.Sprint(r["type"]) + allowed, known := shapes[kind] + if !known { + t.Errorf("%s: %v is a %q, which is not a shape the mesh has", name, r["id"], kind) + continue + } + for key := range r { + checked++ + // `merge` and `protected` are read by the control plane and removed before a + // machine sees them, so they are legal here and unknown to the host. + if key == "merge" || key == "protected" { + continue + } + if !slices.Contains(common, key) && !slices.Contains(allowed, key) { + t.Errorf( + "%s: %v is a %s and carries %q, which that shape does not have. It would "+ + "compose cleanly and be refused on the machine — and the host refuses "+ + "the whole declaration, so this stops the module entirely", + name, r["id"], kind, key) + } + } + } + } + if checked == 0 { + t.Fatal("no example declares a resource, so this test proves nothing") + } +} diff --git a/examples/modules/object-store.json b/examples/modules/object-store.json index c2ede0b..22243b8 100644 --- a/examples/modules/object-store.json +++ b/examples/modules/object-store.json @@ -1,15 +1,23 @@ { "module": "object-store", "version": "1", - - "provides": [{"name": "s3-bucket", "scope": "mesh"}], - "capabilities": ["container-runtime"], - + "provides": [ + { + "name": "s3-bucket", + "scope": "mesh" + } + ], + "capabilities": [ + "container-runtime" + ], "listens": [ - {"port": 9000, "protocol": "tcp", "from": "mesh", - "why": "the S3 endpoint, for modules on any machine that were granted a bucket"} + { + "port": 9000, + "protocol": "tcp", + "from": "mesh", + "why": "the S3 endpoint, for modules on any machine that were granted a bucket" + } ], - "serves": { "s3-bucket": { "port": 9000, @@ -17,35 +25,63 @@ "region": "us-east-1" } }, - - "receives": {"s3-bucket": "/var/lib/objectstore/grants"}, - "grants": {"s3-bucket": "/var/lib/objectstore/grants"}, - - "own-secrets": {"root": "/var/lib/objectstore/root.secret"}, - + "receives": { + "s3-bucket": "/var/lib/objectstore/grants" + }, + "grants": { + "s3-bucket": "/var/lib/objectstore/grants" + }, + "own-secrets": { + "root": "/var/lib/objectstore/root.secret" + }, "resources": [ - {"id": "state", "type": "directory", "path": "/var/lib/objectstore", "mode": "0700"}, - {"id": "grants", "type": "directory", "path": "/var/lib/objectstore/grants", "mode": "0700"}, - - {"id": "store", "type": "container", "name": "mesh-store", - "image": "minio/minio@sha256:aefec8a86702aff0b0dcfdd9284bd7ab7c5631cbf9be63275799e6edcb30dfa2", - "args": ["server", "/data"], - "env": {"MINIO_ROOT_USER": "meshroot"}, - "ports": ["9000:9000"], - "volumes": ["mesh-store-data:/data", "/var/lib/objectstore/root.secret:/run/secrets/root:ro"]}, - - {"id": "provisioner", "type": "container", "name": "mesh-provision-objectstore", - "image": "mesh-provision-objectstore@sha256:0000000000000000000000000000000000000000000000000000000000000000", - "env": { - "GRANTS": "/var/lib/objectstore/grants", - "MESH_OBJECTSTORE_URL": "http://127.0.0.1:9000", - "MESH_OBJECTSTORE_ROOT_USER": "meshroot", - "MESH_OBJECTSTORE_ROOT_PASSWORD_FILE": "/run/secrets/root" - }, - "volumes": [ - "/var/lib/objectstore/grants:/var/lib/objectstore/grants:ro", - "/var/lib/objectstore/root.secret:/run/secrets/root:ro" - ], - "restart-on": ["grants"]} + { + "id": "state", + "type": "directory", + "path": "/var/lib/objectstore", + "mode": "0700" + }, + { + "id": "grants", + "type": "directory", + "path": "/var/lib/objectstore/grants", + "mode": "0700" + }, + { + "id": "store", + "type": "container", + "name": "mesh-store", + "image": "minio/minio@sha256:aefec8a86702aff0b0dcfdd9284bd7ab7c5631cbf9be63275799e6edcb30dfa2", + "args": [ + "server", + "/data" + ], + "env": { + "MINIO_ROOT_USER": "meshroot" + }, + "ports": [ + "9000:9000" + ], + "volumes": [ + "mesh-store-data:/data", + "/var/lib/objectstore/root.secret:/run/secrets/root:ro" + ] + }, + { + "id": "provisioner", + "type": "container", + "name": "mesh-provision-objectstore", + "image": "mesh-provision-objectstore@sha256:0000000000000000000000000000000000000000000000000000000000000000", + "env": { + "GRANTS": "/var/lib/objectstore/grants", + "MESH_OBJECTSTORE_URL": "http://127.0.0.1:9000", + "MESH_OBJECTSTORE_ROOT_USER": "meshroot", + "MESH_OBJECTSTORE_ROOT_PASSWORD_FILE": "/run/secrets/root" + }, + "volumes": [ + "/var/lib/objectstore/grants:/var/lib/objectstore/grants:ro", + "/var/lib/objectstore/root.secret:/run/secrets/root:ro" + ] + } ] } diff --git a/examples/modules/photos.json b/examples/modules/photos.json index 30a303b..e3bac87 100644 --- a/examples/modules/photos.json +++ b/examples/modules/photos.json @@ -1,29 +1,40 @@ { "module": "photos", "version": "1", - - "requires": ["s3-bucket"], - + "requires": [ + "s3-bucket" + ], "contributes": { - "s3-bucket": {"bucket": "photos"} + "s3-bucket": { + "bucket": "photos" + } + }, + "binds": { + "s3-bucket": "/etc/photos/store.json" + }, + "secrets": { + "s3-bucket": "/etc/photos/store.secret" }, - - "binds": {"s3-bucket": "/etc/photos/store.json"}, - "secrets": {"s3-bucket": "/etc/photos/store.secret"}, - "resources": [ - {"id": "config", "type": "directory", "path": "/etc/photos", "mode": "0750"}, - - {"id": "app", "type": "container", "name": "photos", - "image": "alpine@sha256:c64c687cbea9300178b30c95835354e34c4e4febc4badfe27102879de0483b5e", - "env": { - "PHOTOS_STORE": "/etc/photos/store.json", - "PHOTOS_STORE_SECRET_FILE": "/etc/photos/store.secret" - }, - "volumes": [ - "/etc/photos/store.json:/etc/photos/store.json:ro", - "/etc/photos/store.secret:/etc/photos/store.secret:ro" - ], - "restart-on": ["config"]} + { + "id": "config", + "type": "directory", + "path": "/etc/photos", + "mode": "0750" + }, + { + "id": "app", + "type": "container", + "name": "photos", + "image": "alpine@sha256:c64c687cbea9300178b30c95835354e34c4e4febc4badfe27102879de0483b5e", + "env": { + "PHOTOS_STORE": "/etc/photos/store.json", + "PHOTOS_STORE_SECRET_FILE": "/etc/photos/store.secret" + }, + "volumes": [ + "/etc/photos/store.json:/etc/photos/store.json:ro", + "/etc/photos/store.secret:/etc/photos/store.secret:ro" + ] + } ] } diff --git a/examples/modules/postgres.json b/examples/modules/postgres.json index e240757..7bd4edb 100644 --- a/examples/modules/postgres.json +++ b/examples/modules/postgres.json @@ -81,9 +81,6 @@ ], "volumes": [ "/services/postgres/db-data:/var/lib/postgresql/data" - ], - "restart-on": [ - "superuser-env" ] }, { @@ -100,10 +97,6 @@ "volumes": [ "/var/lib/postgres/grants:/var/lib/postgres/grants:ro", "/var/lib/postgres/superuser.secret:/run/secrets/superuser:ro" - ], - "restart-on": [ - "grants", - "superuser-env" ] } ]