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" ] } ]