A container does not take restart-on, and nine of them did

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.
This commit is contained in:
2026-09-01 17:19:04 +02:00
parent f5b03e1474
commit a5d85266d0
8 changed files with 239 additions and 113 deletions
-3
View File
@@ -74,9 +74,6 @@
],
"volumes": [
"/services/gitea/gitea:/data"
],
"restart-on": [
"server-env"
]
}
]
+74 -34
View File
@@ -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"
]
}
]
}
-5
View File
@@ -264,11 +264,6 @@
"volumes": [
"/services/mailu/data/certs:/certs",
"/services/mailu/data/overrides/nginx:/overrides:ro"
],
"restart-on": [
"imap",
"smtp",
"admin"
]
}
]
-7
View File
@@ -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"
]
}
]
+61
View File
@@ -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")
}
}
+72 -36
View File
@@ -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"
]
}
]
}
+32 -21
View File
@@ -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"
]
}
]
}
-7
View File
@@ -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"
]
}
]