From f5b03e147423b6b41ffddd655eb494ce74f161bc Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 1 Sep 2026 16:09:23 +0200 Subject: [PATCH] Declare the directories that hold the data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit novox/hq 04-ISSUES/026. Four modules mounted fourteen host paths that no resource declared — the mail spool, the databases, the object store's data. Each would be created by the container runtime as root, with a mode nobody chose, so `owner` and `mode` went unapplied on exactly the directories that matter. The worse half: a directory the mesh declared and no longer wants is kept rather than removed when it holds anything the mesh did not put there. That rule is the answer to what happens to data when a module goes away, and it is written in terms of declared directories. An undeclared one is not covered. So the one rule guarding against data loss reached the configuration directories, which are cheap to lose, and missed the data directories, which are why the rule exists. The cause is worth naming. These manifests were written by reading the arrangement being replaced and carrying its compose files across — service, image, ports, volumes, environment. The container shape can express all of that, which is what made the transliteration feel like progress. A shape that can express a compose file gets filled in like one, and a volume line borrowed from compose declares no owner, no mode and no intent. Declared parent-first, because the host applies in the order written and does not sort. The check is mechanical now, because a person comparing volumes against directories by hand is the process that produced this. Still open, and bigger: whether these paths are where a module's data should live at all. They were inherited whole, and they decide what a person backs up. --- examples/modules/gitea.json | 105 +++++++--- examples/modules/mailu.json | 346 +++++++++++++++++++++++-------- examples/modules/minio.json | 149 +++++++++---- examples/modules/modules_test.go | 64 ++++++ examples/modules/postgres.json | 145 +++++++++---- 5 files changed, 607 insertions(+), 202 deletions(-) diff --git a/examples/modules/gitea.json b/examples/modules/gitea.json index dd033fa..ba01a97 100644 --- a/examples/modules/gitea.json +++ b/examples/modules/gitea.json @@ -1,36 +1,83 @@ { "module": "gitea", "version": "1", - - "requires": ["postgres-database"], - "contributes": { - "postgres-database": {"name": "gitea"} - }, - "binds": {"postgres-database": "/var/lib/gitea/database.json"}, - "secrets": {"postgres-database": "/var/lib/gitea/database.secret"}, - - "capabilities": ["container-runtime"], - - "listens": [ - {"port": 3000, "protocol": "tcp", "from": "mesh", "why": "the forge, over http"}, - {"port": 2222, "protocol": "tcp", "from": "mesh", - "why": "git over ssh. Not 22: the machine's own daemon holds that, and a module does not take it"} + "requires": [ + "postgres-database" ], - - "own-secrets": {"internal-token": "/var/lib/gitea/internal-token.secret"}, - + "contributes": { + "postgres-database": { + "name": "gitea" + } + }, + "binds": { + "postgres-database": "/var/lib/gitea/database.json" + }, + "secrets": { + "postgres-database": "/var/lib/gitea/database.secret" + }, + "capabilities": [ + "container-runtime" + ], + "listens": [ + { + "port": 3000, + "protocol": "tcp", + "from": "mesh", + "why": "the forge, over http" + }, + { + "port": 2222, + "protocol": "tcp", + "from": "mesh", + "why": "git over ssh. Not 22: the machine's own daemon holds that, and a module does not take it" + } + ], + "own-secrets": { + "internal-token": "/var/lib/gitea/internal-token.secret" + }, "resources": [ - {"id": "state", "type": "directory", "path": "/var/lib/gitea", "mode": "0700"}, - - {"id": "server-env", "type": "file", "path": "/var/lib/gitea/server.env", "mode": "0600", - "content": "GITEA__security__INTERNAL_TOKEN=${secret:internal-token}\nGITEA__database__DB_TYPE=postgres\nGITEA__database__HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\nGITEA__database__NAME=gitea\nGITEA__database__USER=${bound:postgres-database:as}\nGITEA__database__PASSWD=${secret:postgres-database}\n"}, - - {"id": "server", "type": "container", "name": "gitea", - "image": "gitea/gitea@sha256:dfc61e347c8b582df918f4556401bf2cecdfbdb56c5282ae9488dd76fca3e41c", - "env": {"DB_TYPE": "postgres", "USER_UID": "1000", "USER_GID": "1000"}, - "env-file": ["/var/lib/gitea/server.env"], - "ports": ["3000:3000", "2222:22"], - "volumes": ["/services/gitea/gitea:/data"], - "restart-on": ["server-env"]} + { + "id": "state", + "type": "directory", + "path": "/var/lib/gitea", + "mode": "0700" + }, + { + "id": "server-env", + "type": "file", + "path": "/var/lib/gitea/server.env", + "mode": "0600", + "content": "GITEA__security__INTERNAL_TOKEN=${secret:internal-token}\nGITEA__database__DB_TYPE=postgres\nGITEA__database__HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\nGITEA__database__NAME=gitea\nGITEA__database__USER=${bound:postgres-database:as}\nGITEA__database__PASSWD=${secret:postgres-database}\n" + }, + { + "id": "data", + "type": "directory", + "path": "/services/gitea/gitea", + "mode": "0700" + }, + { + "id": "server", + "type": "container", + "name": "gitea", + "image": "gitea/gitea@sha256:dfc61e347c8b582df918f4556401bf2cecdfbdb56c5282ae9488dd76fca3e41c", + "env": { + "DB_TYPE": "postgres", + "USER_UID": "1000", + "USER_GID": "1000" + }, + "env-file": [ + "/var/lib/gitea/server.env" + ], + "ports": [ + "3000:3000", + "2222:22" + ], + "volumes": [ + "/services/gitea/gitea:/data" + ], + "restart-on": [ + "server-env" + ] + } ] } diff --git a/examples/modules/mailu.json b/examples/modules/mailu.json index a156f2d..46bafca 100644 --- a/examples/modules/mailu.json +++ b/examples/modules/mailu.json @@ -1,97 +1,275 @@ { "module": "mailu", "version": "1", - - "capabilities": ["container-runtime"], - + "capabilities": [ + "container-runtime" + ], "listens": [ - {"port": 25, "protocol": "tcp", "from": "anywhere", "why": "mail from other mail servers"}, - {"port": 465, "protocol": "tcp", "from": "anywhere", "why": "submission over TLS"}, - {"port": 587, "protocol": "tcp", "from": "anywhere", "why": "submission"}, - {"port": 993, "protocol": "tcp", "from": "anywhere", "why": "IMAP over TLS"}, - {"port": 7080, "protocol": "tcp", "from": "mesh", "why": "the web interface, behind a proxy"} + { + "port": 25, + "protocol": "tcp", + "from": "anywhere", + "why": "mail from other mail servers" + }, + { + "port": 465, + "protocol": "tcp", + "from": "anywhere", + "why": "submission over TLS" + }, + { + "port": 587, + "protocol": "tcp", + "from": "anywhere", + "why": "submission" + }, + { + "port": 993, + "protocol": "tcp", + "from": "anywhere", + "why": "IMAP over TLS" + }, + { + "port": 7080, + "protocol": "tcp", + "from": "mesh", + "why": "the web interface, behind a proxy" + } ], - "own-secrets": { "secret-key": "/var/lib/mailu/secret-key.secret", "database": "/var/lib/mailu/database.secret", "admin": "/var/lib/mailu/admin.secret" }, - "resources": [ - {"id": "state", "type": "directory", "path": "/var/lib/mailu", "mode": "0700"}, - - {"id": "secret-env", "type": "file", "path": "/var/lib/mailu/secret.env", "mode": "0600", - "content": "SECRET_KEY=${secret:secret-key}\n"}, - {"id": "database-env", "type": "file", "path": "/var/lib/mailu/database.env", "mode": "0600", - "content": "POSTGRES_USER=mailu\nPOSTGRES_DB=mailu\nPOSTGRES_PASSWORD=${secret:database}\nDB_USER=mailu\nDB_NAME=mailu\nDB_PW=${secret:database}\n"}, - {"id": "admin-env", "type": "file", "path": "/var/lib/mailu/admin.env", "mode": "0600", - "content": "INITIAL_ADMIN_PW=${secret:admin}\n"}, - - {"id": "net", "type": "network", "name": "mailu"}, - - {"id": "resolver", "type": "container", "name": "mailu-resolver", - "image": "ghcr.io/mailu/unbound@sha256:142aaad82ad1b0d5b59a5f1303778dba61a3e0a540f5d969c48862bcc99f6f5d", - "network": "mailu", - "env-file": ["/var/lib/mailu/secret.env"]}, - - {"id": "redis", "type": "container", "name": "mailu-redis", - "image": "redis@sha256:1db42ccef14898aa29bae778452d567534b59c107129cbc1163fb552de184d3c", - "network": "mailu", - "volumes": ["/services/mailu/data/redis:/data"]}, - - {"id": "admindb", "type": "container", "name": "mailu-admindb", - "image": "postgres@sha256:7456ef82e5f5bc43d997f4781bbd7c0d6389bff397564649a356e206ba473aee", - "network": "mailu", - "env": {"PGDATA": "/var/lib/postgresql/data/pgdata"}, - "env-file": ["/var/lib/mailu/database.env"], - "volumes": ["/services/mailu/data/data/psql_admindb/pgdata:/var/lib/postgresql/data/pgdata"]}, - - {"id": "admin", "type": "container", "name": "mailu-admin", - "image": "ghcr.io/mailu/admin@sha256:dcac20e9cbdad560faef9653b1b5ac0d9266f4098dc00f0e7f0d35f4e70ed8f1", - "network": "mailu", - "env-file": ["/var/lib/mailu/secret.env", "/var/lib/mailu/database.env", "/var/lib/mailu/admin.env"], - "volumes": [ - "/services/mailu/data/data:/data", - "/services/mailu/data/dkim:/dkim" - ]}, - - {"id": "imap", "type": "container", "name": "mailu-imap", - "image": "ghcr.io/mailu/dovecot@sha256:46d18ba51032be8ebd6841aa49c1ef8762c729038c5fd86a081b5b884d478af9", - "network": "mailu", - "env-file": ["/var/lib/mailu/secret.env"], - "volumes": [ - "/services/mailu/data/mail:/mail", - "/services/mailu/data/overrides/dovecot:/overrides:ro" - ]}, - - {"id": "smtp", "type": "container", "name": "mailu-smtp", - "image": "ghcr.io/mailu/postfix@sha256:bbf882880f68849511710b35237a933f3fe80c4b28bf48ff20205dbd1f1433d7", - "network": "mailu", - "env-file": ["/var/lib/mailu/secret.env"], - "volumes": ["/services/mailu/data/mailqueue:/queue"]}, - - {"id": "antispam", "type": "container", "name": "mailu-antispam", - "image": "ghcr.io/mailu/rspamd@sha256:e87ab93dd252cc69499caa5317dd10d445fd4291a7ecf6bca09793c7d475a0c8", - "network": "mailu", - "env-file": ["/var/lib/mailu/secret.env"], - "volumes": ["/services/mailu/data/filter:/var/lib/rspamd"]}, - - {"id": "webmail", "type": "container", "name": "mailu-webmail", - "image": "ghcr.io/mailu/webmail@sha256:076b720fc766e58a97321cdb700e887c2008d6d323685fe59f323088333059dc", - "network": "mailu", - "env-file": ["/var/lib/mailu/secret.env"], - "volumes": ["/services/mailu/data/webmail:/data"]}, - - {"id": "front", "type": "container", "name": "mailu-front", - "image": "ghcr.io/mailu/nginx@sha256:09f28ab6d36367fcacc7994f7021f132ac845bdc05f04bf80906102d11aaa057", - "network": "mailu", - "env-file": ["/var/lib/mailu/secret.env"], - "ports": ["25:25", "465:465", "587:587", "993:993", "7080:80"], - "volumes": [ - "/services/mailu/data/certs:/certs", - "/services/mailu/data/overrides/nginx:/overrides:ro" - ], - "restart-on": ["imap", "smtp", "admin"]} + { + "id": "state", + "type": "directory", + "path": "/var/lib/mailu", + "mode": "0700" + }, + { + "id": "secret-env", + "type": "file", + "path": "/var/lib/mailu/secret.env", + "mode": "0600", + "content": "SECRET_KEY=${secret:secret-key}\n" + }, + { + "id": "database-env", + "type": "file", + "path": "/var/lib/mailu/database.env", + "mode": "0600", + "content": "POSTGRES_USER=mailu\nPOSTGRES_DB=mailu\nPOSTGRES_PASSWORD=${secret:database}\nDB_USER=mailu\nDB_NAME=mailu\nDB_PW=${secret:database}\n" + }, + { + "id": "admin-env", + "type": "file", + "path": "/var/lib/mailu/admin.env", + "mode": "0600", + "content": "INITIAL_ADMIN_PW=${secret:admin}\n" + }, + { + "id": "data-certs", + "type": "directory", + "path": "/services/mailu/data/certs", + "mode": "0700" + }, + { + "id": "data-data", + "type": "directory", + "path": "/services/mailu/data/data", + "mode": "0700" + }, + { + "id": "data-dkim", + "type": "directory", + "path": "/services/mailu/data/dkim", + "mode": "0700" + }, + { + "id": "data-filter", + "type": "directory", + "path": "/services/mailu/data/filter", + "mode": "0700" + }, + { + "id": "data-mail", + "type": "directory", + "path": "/services/mailu/data/mail", + "mode": "0700" + }, + { + "id": "data-mailqueue", + "type": "directory", + "path": "/services/mailu/data/mailqueue", + "mode": "0700" + }, + { + "id": "data-redis", + "type": "directory", + "path": "/services/mailu/data/redis", + "mode": "0700" + }, + { + "id": "data-webmail", + "type": "directory", + "path": "/services/mailu/data/webmail", + "mode": "0700" + }, + { + "id": "data-dovecot", + "type": "directory", + "path": "/services/mailu/data/overrides/dovecot", + "mode": "0700" + }, + { + "id": "data-nginx", + "type": "directory", + "path": "/services/mailu/data/overrides/nginx", + "mode": "0700" + }, + { + "id": "data-pgdata", + "type": "directory", + "path": "/services/mailu/data/data/psql_admindb/pgdata", + "mode": "0700" + }, + { + "id": "net", + "type": "network", + "name": "mailu" + }, + { + "id": "resolver", + "type": "container", + "name": "mailu-resolver", + "image": "ghcr.io/mailu/unbound@sha256:142aaad82ad1b0d5b59a5f1303778dba61a3e0a540f5d969c48862bcc99f6f5d", + "network": "mailu", + "env-file": [ + "/var/lib/mailu/secret.env" + ] + }, + { + "id": "redis", + "type": "container", + "name": "mailu-redis", + "image": "redis@sha256:1db42ccef14898aa29bae778452d567534b59c107129cbc1163fb552de184d3c", + "network": "mailu", + "volumes": [ + "/services/mailu/data/redis:/data" + ] + }, + { + "id": "admindb", + "type": "container", + "name": "mailu-admindb", + "image": "postgres@sha256:7456ef82e5f5bc43d997f4781bbd7c0d6389bff397564649a356e206ba473aee", + "network": "mailu", + "env": { + "PGDATA": "/var/lib/postgresql/data/pgdata" + }, + "env-file": [ + "/var/lib/mailu/database.env" + ], + "volumes": [ + "/services/mailu/data/data/psql_admindb/pgdata:/var/lib/postgresql/data/pgdata" + ] + }, + { + "id": "admin", + "type": "container", + "name": "mailu-admin", + "image": "ghcr.io/mailu/admin@sha256:dcac20e9cbdad560faef9653b1b5ac0d9266f4098dc00f0e7f0d35f4e70ed8f1", + "network": "mailu", + "env-file": [ + "/var/lib/mailu/secret.env", + "/var/lib/mailu/database.env", + "/var/lib/mailu/admin.env" + ], + "volumes": [ + "/services/mailu/data/data:/data", + "/services/mailu/data/dkim:/dkim" + ] + }, + { + "id": "imap", + "type": "container", + "name": "mailu-imap", + "image": "ghcr.io/mailu/dovecot@sha256:46d18ba51032be8ebd6841aa49c1ef8762c729038c5fd86a081b5b884d478af9", + "network": "mailu", + "env-file": [ + "/var/lib/mailu/secret.env" + ], + "volumes": [ + "/services/mailu/data/mail:/mail", + "/services/mailu/data/overrides/dovecot:/overrides:ro" + ] + }, + { + "id": "smtp", + "type": "container", + "name": "mailu-smtp", + "image": "ghcr.io/mailu/postfix@sha256:bbf882880f68849511710b35237a933f3fe80c4b28bf48ff20205dbd1f1433d7", + "network": "mailu", + "env-file": [ + "/var/lib/mailu/secret.env" + ], + "volumes": [ + "/services/mailu/data/mailqueue:/queue" + ] + }, + { + "id": "antispam", + "type": "container", + "name": "mailu-antispam", + "image": "ghcr.io/mailu/rspamd@sha256:e87ab93dd252cc69499caa5317dd10d445fd4291a7ecf6bca09793c7d475a0c8", + "network": "mailu", + "env-file": [ + "/var/lib/mailu/secret.env" + ], + "volumes": [ + "/services/mailu/data/filter:/var/lib/rspamd" + ] + }, + { + "id": "webmail", + "type": "container", + "name": "mailu-webmail", + "image": "ghcr.io/mailu/webmail@sha256:076b720fc766e58a97321cdb700e887c2008d6d323685fe59f323088333059dc", + "network": "mailu", + "env-file": [ + "/var/lib/mailu/secret.env" + ], + "volumes": [ + "/services/mailu/data/webmail:/data" + ] + }, + { + "id": "front", + "type": "container", + "name": "mailu-front", + "image": "ghcr.io/mailu/nginx@sha256:09f28ab6d36367fcacc7994f7021f132ac845bdc05f04bf80906102d11aaa057", + "network": "mailu", + "env-file": [ + "/var/lib/mailu/secret.env" + ], + "ports": [ + "25:25", + "465:465", + "587:587", + "993:993", + "7080:80" + ], + "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 6b73c12..46aea2d 100644 --- a/examples/modules/minio.json +++ b/examples/modules/minio.json @@ -1,54 +1,115 @@ { "module": "minio", "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"} + { + "port": 9000, + "protocol": "tcp", + "from": "mesh", + "why": "the S3 endpoint" + } ], - "serves": { - "s3-bucket": {"port": 9000, "scheme": "http", "region": "us-east-1"} + "s3-bucket": { + "port": 9000, + "scheme": "http", + "region": "us-east-1" + } + }, + "receives": { + "s3-bucket": "/var/lib/minio/grants/mesh.json" + }, + "grants": { + "s3-bucket": "/var/lib/minio/grants" + }, + "own-secrets": { + "root": "/var/lib/minio/root.secret" }, - - "receives": {"s3-bucket": "/var/lib/minio/grants/mesh.json"}, - "grants": {"s3-bucket": "/var/lib/minio/grants"}, - - "own-secrets": {"root": "/var/lib/minio/root.secret"}, - "resources": [ - {"id": "state", "type": "directory", "path": "/var/lib/minio", "mode": "0700"}, - {"id": "grants", "type": "directory", "path": "/var/lib/minio/grants", "mode": "0700"}, - - {"id": "root-env", "type": "file", "path": "/var/lib/minio/root.env", "mode": "0600", - "content": "MINIO_ROOT_USER=meshroot\nMINIO_ROOT_PASSWORD=${secret:root}\n"}, - - {"id": "net", "type": "network", "name": "minio"}, - - {"id": "server", "type": "container", "name": "minio", - "image": "minio/minio@sha256:aefec8a86702aff0b0dcfdd9284bd7ab7c5631cbf9be63275799e6edcb30dfa2", - "network": "minio", - "args": ["server", "/data", "--console-address", ":9001"], - "env-file": ["/var/lib/minio/root.env"], - "ports": ["9000:9000"], - "volumes": ["/services/minio/data/data1-1:/data"], - "restart-on": ["root-env"]}, - - {"id": "provisioner", "type": "container", "name": "mesh-provision-objectstore", - "image": "mesh-provision-objectstore@sha256:0000000000000000000000000000000000000000000000000000000000000000", - "network": "minio", - "env": { - "GRANTS": "/var/lib/minio/grants", - "MESH_OBJECTSTORE_URL": "http://minio:9000", - "MESH_OBJECTSTORE_ROOT_USER": "meshroot", - "MESH_OBJECTSTORE_ROOT_PASSWORD_FILE": "/run/secrets/root" - }, - "volumes": [ - "/var/lib/minio/grants:/var/lib/minio/grants:ro", - "/var/lib/minio/root.secret:/run/secrets/root:ro" - ], - "restart-on": ["grants", "root-env"]} + { + "id": "state", + "type": "directory", + "path": "/var/lib/minio", + "mode": "0700" + }, + { + "id": "grants", + "type": "directory", + "path": "/var/lib/minio/grants", + "mode": "0700" + }, + { + "id": "root-env", + "type": "file", + "path": "/var/lib/minio/root.env", + "mode": "0600", + "content": "MINIO_ROOT_USER=meshroot\nMINIO_ROOT_PASSWORD=${secret:root}\n" + }, + { + "id": "data", + "type": "directory", + "path": "/services/minio/data/data1-1", + "mode": "0700" + }, + { + "id": "net", + "type": "network", + "name": "minio" + }, + { + "id": "server", + "type": "container", + "name": "minio", + "image": "minio/minio@sha256:aefec8a86702aff0b0dcfdd9284bd7ab7c5631cbf9be63275799e6edcb30dfa2", + "network": "minio", + "args": [ + "server", + "/data", + "--console-address", + ":9001" + ], + "env-file": [ + "/var/lib/minio/root.env" + ], + "ports": [ + "9000:9000" + ], + "volumes": [ + "/services/minio/data/data1-1:/data" + ], + "restart-on": [ + "root-env" + ] + }, + { + "id": "provisioner", + "type": "container", + "name": "mesh-provision-objectstore", + "image": "mesh-provision-objectstore@sha256:0000000000000000000000000000000000000000000000000000000000000000", + "network": "minio", + "env": { + "GRANTS": "/var/lib/minio/grants", + "MESH_OBJECTSTORE_URL": "http://minio:9000", + "MESH_OBJECTSTORE_ROOT_USER": "meshroot", + "MESH_OBJECTSTORE_ROOT_PASSWORD_FILE": "/run/secrets/root" + }, + "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 4d31e5d..abb3f21 100644 --- a/examples/modules/modules_test.go +++ b/examples/modules/modules_test.go @@ -505,3 +505,67 @@ func TestEveryImageTheExamplesNameIsOneThisRepositoryBuilds(t *testing.T) { t.Fatal("no example names an image this repository builds, so this proves nothing") } } + +// Every host path a container mounts is a directory the module declared. +// +// **The mesh owns a directory or it does not** (novox/hq 04-ISSUES/026). A bind mount whose source +// does not exist is created by the container runtime as root, with a mode nobody chose — so +// `owner` and `mode` go unapplied on exactly the directories that hold the data. +// +// Worse, the rule that a directory is *kept* rather than removed when it holds something the mesh +// did not put there (ADR 0030) is written in terms of declared directories. An undeclared one is +// not covered by it. So the single rule guarding against data loss reached the configuration and +// not the data. +// +// These manifests were written by carrying compose files across, and a container shape that can +// express a compose file gets filled in like one. This is the check that says so. +func TestEveryMountedPathIsADirectoryTheModuleDeclared(t *testing.T) { + found, _ := filepath.Glob("*.json") + var checked int + for _, name := range found { + m := read(t, name) + declared := map[string]bool{} + for _, r := range m.Resources { + if fmt.Sprint(r["type"]) == "directory" { + declared[fmt.Sprint(r["path"])] = true + } + } + for _, r := range m.Resources { + for _, v := range stringsOfTest(r["volumes"]) { + host, _, _ := strings.Cut(v, ":") + if !strings.HasPrefix(host, "/") { + continue // a named volume, which the runtime owns and the mesh does not + } + checked++ + var covered bool + for d := range declared { + if host == d || strings.HasPrefix(host, strings.TrimRight(d, "/")+"/") { + covered = true + } + } + if !covered { + t.Errorf( + "%s: %v mounts %s and no resource declares it. The runtime will create it "+ + "as root, and the rule that keeps a directory holding data does not "+ + "reach a directory the mesh never declared", + name, r["id"], host) + } + } + } + } + if checked == 0 { + t.Fatal("no example mounts a host path, so this test proves nothing") + } +} + +func stringsOfTest(v any) []string { + list, ok := v.([]any) + if !ok { + return nil + } + out := make([]string, 0, len(list)) + for _, item := range list { + out = append(out, fmt.Sprint(item)) + } + return out +} diff --git a/examples/modules/postgres.json b/examples/modules/postgres.json index 2422811..e240757 100644 --- a/examples/modules/postgres.json +++ b/examples/modules/postgres.json @@ -1,55 +1,110 @@ { "module": "postgres", "version": "1", - - "provides": [{"name": "postgres-database", "scope": "mesh"}], - "capabilities": ["container-runtime"], - + "provides": [ + { + "name": "postgres-database", + "scope": "mesh" + } + ], + "capabilities": [ + "container-runtime" + ], "listens": [ - {"port": 5432, "protocol": "tcp", "from": "mesh", - "why": "modules on any machine that were granted a database"} + { + "port": 5432, + "protocol": "tcp", + "from": "mesh", + "why": "modules on any machine that were granted a database" + } ], - "serves": { - "postgres-database": {"port": 5432} + "postgres-database": { + "port": 5432 + } + }, + "receives": { + "postgres-database": "/var/lib/postgres/grants/mesh.json" + }, + "grants": { + "postgres-database": "/var/lib/postgres/grants" + }, + "own-secrets": { + "superuser": "/var/lib/postgres/superuser.secret" }, - - "receives": {"postgres-database": "/var/lib/postgres/grants/mesh.json"}, - "grants": {"postgres-database": "/var/lib/postgres/grants"}, - - "own-secrets": {"superuser": "/var/lib/postgres/superuser.secret"}, - "resources": [ - {"id": "state", "type": "directory", "path": "/var/lib/postgres", "mode": "0700"}, - {"id": "grants", "type": "directory", "path": "/var/lib/postgres/grants", "mode": "0700"}, - - {"id": "superuser-env", "type": "file", "path": "/var/lib/postgres/superuser.env", - "mode": "0600", - "content": "POSTGRES_PASSWORD=${secret:superuser}\n"}, - - {"id": "net", "type": "network", "name": "postgres"}, - - {"id": "server", "type": "container", "name": "postgres", - "image": "postgres@sha256:7456ef82e5f5bc43d997f4781bbd7c0d6389bff397564649a356e206ba473aee", - "network": "postgres", - "env": {"POSTGRES_USER": "postgres", "POSTGRES_DB": "postgres"}, - "env-file": ["/var/lib/postgres/superuser.env"], - "ports": ["5432:5432"], - "volumes": ["/services/postgres/db-data:/var/lib/postgresql/data"], - "restart-on": ["superuser-env"]}, - - {"id": "provisioner", "type": "container", "name": "mesh-provision-postgres", - "image": "mesh-provision-postgres@sha256:0000000000000000000000000000000000000000000000000000000000000000", - "network": "postgres", - "env": { - "GRANTS": "/var/lib/postgres/grants", - "MESH_PROVISION_POSTGRES": "postgres://postgres@postgres:5432/postgres?sslmode=disable", - "MESH_PROVISION_PASSWORD_FILE": "/run/secrets/superuser" - }, - "volumes": [ - "/var/lib/postgres/grants:/var/lib/postgres/grants:ro", - "/var/lib/postgres/superuser.secret:/run/secrets/superuser:ro" - ], - "restart-on": ["grants", "superuser-env"]} + { + "id": "state", + "type": "directory", + "path": "/var/lib/postgres", + "mode": "0700" + }, + { + "id": "grants", + "type": "directory", + "path": "/var/lib/postgres/grants", + "mode": "0700" + }, + { + "id": "superuser-env", + "type": "file", + "path": "/var/lib/postgres/superuser.env", + "mode": "0600", + "content": "POSTGRES_PASSWORD=${secret:superuser}\n" + }, + { + "id": "data", + "type": "directory", + "path": "/services/postgres/db-data", + "mode": "0700" + }, + { + "id": "net", + "type": "network", + "name": "postgres" + }, + { + "id": "server", + "type": "container", + "name": "postgres", + "image": "postgres@sha256:7456ef82e5f5bc43d997f4781bbd7c0d6389bff397564649a356e206ba473aee", + "network": "postgres", + "env": { + "POSTGRES_USER": "postgres", + "POSTGRES_DB": "postgres" + }, + "env-file": [ + "/var/lib/postgres/superuser.env" + ], + "ports": [ + "5432:5432" + ], + "volumes": [ + "/services/postgres/db-data:/var/lib/postgresql/data" + ], + "restart-on": [ + "superuser-env" + ] + }, + { + "id": "provisioner", + "type": "container", + "name": "mesh-provision-postgres", + "image": "mesh-provision-postgres@sha256:0000000000000000000000000000000000000000000000000000000000000000", + "network": "postgres", + "env": { + "GRANTS": "/var/lib/postgres/grants", + "MESH_PROVISION_POSTGRES": "postgres://postgres@postgres:5432/postgres?sslmode=disable", + "MESH_PROVISION_PASSWORD_FILE": "/run/secrets/superuser" + }, + "volumes": [ + "/var/lib/postgres/grants:/var/lib/postgres/grants:ro", + "/var/lib/postgres/superuser.secret:/run/secrets/superuser:ro" + ], + "restart-on": [ + "grants", + "superuser-env" + ] + } ] }