Compare commits
75
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
048f1b8284 | ||
|
|
278610c0c3 | ||
|
|
6bedcd3f21 | ||
|
|
968473219a | ||
|
|
ad219beee2 | ||
|
|
fd09b1a50e | ||
|
|
7aea08d6c3 | ||
|
|
23d735a0bf | ||
|
|
226eab4c6f | ||
|
|
bb8f2e76a9 | ||
|
|
372450851f | ||
|
|
f4e4e12c99 | ||
|
|
fd9be011c0 | ||
|
|
a85b0ee346 | ||
|
|
34243c9e34 | ||
|
|
870a541072 | ||
|
|
a59750fa28 | ||
|
|
81592a3b2c | ||
|
|
705ceec1e7 | ||
|
|
afdd149ab7 | ||
|
|
dde8b15483 | ||
|
|
8a046be198 | ||
|
|
668278bde4 | ||
|
|
6761bb02a1 | ||
|
|
f98c9859d2 | ||
|
|
cac5eab7da | ||
|
|
770c9f6a78 | ||
|
|
5427118614 | ||
|
|
53765335cf | ||
|
|
5fd2ed9686 | ||
|
|
f7887d706d | ||
|
|
d6dd21a091 | ||
|
|
a844701577 | ||
|
|
50a99f022c | ||
|
|
382a44621e | ||
|
|
ddb67fc095 | ||
|
|
78595e4db3 | ||
|
|
37634de1e3 | ||
|
|
36c5f87130 | ||
|
|
b2e39eb2cd | ||
|
|
3d73c9f54e | ||
|
|
fb95eb6e46 | ||
|
|
4d715f8b73 | ||
|
|
afe8aae826 | ||
|
|
fa91be4941 | ||
|
|
87f73dce6a | ||
|
|
fc5ccdfe2a | ||
|
|
4489e56935 | ||
|
|
08e947e4c8 | ||
|
|
7fb9dd0254 | ||
|
|
420d05e8dd | ||
|
|
6769e66c82 | ||
|
|
15b35b53db | ||
|
|
6177565741 | ||
|
|
6b2ea0972a | ||
|
|
a978b53d1c | ||
|
|
7501c1db9e | ||
|
|
00ada1e9f7 | ||
|
|
67b443d5ad | ||
|
|
a2da2e4910 | ||
|
|
bcb9ca8f93 | ||
|
|
2409afda60 | ||
|
|
511200ed9c | ||
|
|
b704bf5ad8 | ||
|
|
142d65c52a | ||
|
|
45dd036623 | ||
|
|
107090310d | ||
|
|
440e3e446e | ||
|
|
20df40c949 | ||
|
|
6a6dd4a7dc | ||
|
|
973d80aaa2 | ||
|
|
945390e59a | ||
|
|
ed0f4602a6 | ||
|
|
13d0361640 | ||
|
|
61eb201f8a |
@@ -6,19 +6,19 @@
|
||||
],
|
||||
"claims": [
|
||||
{
|
||||
"name": "the-build-machine",
|
||||
"scope": "node"
|
||||
"name": "mesh-build-machine",
|
||||
"scope": "mesh"
|
||||
}
|
||||
],
|
||||
"requires": [
|
||||
"artifact-store",
|
||||
"package-registry"
|
||||
"npm-package-registry"
|
||||
],
|
||||
"binds": {
|
||||
"package-registry": "/var/lib/mesh/builder/package-registry.json"
|
||||
"npm-package-registry": "/var/lib/mesh/builder/package-registry.json"
|
||||
},
|
||||
"secrets": {
|
||||
"package-registry": "/var/lib/mesh/builder/package-registry.secret"
|
||||
"npm-package-registry": "/var/lib/mesh/builder/package-registry.secret"
|
||||
},
|
||||
"emits": [
|
||||
"module.builder.built"
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# dhcpcd
|
||||
|
||||
The uplink seat's module for a machine whose own network is dhcpcd's (novox/hq ADR 0117). It
|
||||
asks two things of dhcpcd, and nothing else: leave the resolver file to the mesh, and leave the
|
||||
private network's interface alone. It never declares an interface, an address, a route, a
|
||||
wireless network or its credentials — the link dhcpcd keeps is the only channel the mesh reaches
|
||||
the machine over.
|
||||
|
||||
## What it writes
|
||||
|
||||
Two lines into `/etc/dhcpcd.conf`, as the mesh's marked region (`into: block`) — dhcpcd reads no
|
||||
drop-in directory, so the mesh writes into its one file rather than over it (ADR 0102):
|
||||
|
||||
- `nohook resolv.conf` — dhcpcd's resolv.conf hook rewrites `/etc/resolv.conf` on every lease it
|
||||
takes or renews, which would silently replace the resolver `resolv-conf` names.
|
||||
- `denyinterfaces mesh0` — dhcpcd never asks for a lease on the private network's interface, and
|
||||
never takes it down. dhcpcd leaves a point-to-point interface alone by default; this says so
|
||||
rather than relying on it.
|
||||
|
||||
**At the start of the file** (`at: start`). Both are global options, and dhcpcd reads every line
|
||||
after an `interface` or `ssid` line as that interface's own. A configured machine's file ends in
|
||||
exactly such a block (the interface, its static address), so appended at the end these two would
|
||||
quietly apply to one interface only.
|
||||
|
||||
## Why it declares no service
|
||||
|
||||
dhcpcd is the machine's, not the mesh's. The mesh never starts, stops or enables it: stopping it
|
||||
drops the address the machine is reached at, and a module unassigned by mistake must not be able
|
||||
to do that. And there is nothing to reload it with — `dhcpcd.service` reports `CanReload=no`, and
|
||||
a restart drops the lease. So the two lines take effect at **dhcpcd's next start**.
|
||||
|
||||
On an adopted machine that is normally no gap: the predecessor wrote the same `nohook` line, and
|
||||
it is already in force. **On a machine that was not adopted, it is one:** until dhcpcd next
|
||||
starts (a reboot, or the operator restarting it in a window of their choosing), a lease renewal
|
||||
still rewrites `/etc/resolv.conf`, and `resolv-conf` puts it back at the next push. Assign this
|
||||
module before `resolv-conf` on such a machine, and restart dhcpcd once, by hand, when losing the
|
||||
link for a moment is acceptable.
|
||||
|
||||
## One manager per machine
|
||||
|
||||
It claims `the-uplink`: a machine runs one network manager, and assigning a second module that
|
||||
claims the seat is refused. Assigning this one to a machine whose network is NetworkManager's
|
||||
installs the package and writes the two lines, and starts nothing.
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"module": "dhcpcd",
|
||||
"version": "1",
|
||||
"capabilities": [
|
||||
"package-manager",
|
||||
"service-manager"
|
||||
],
|
||||
"claims": [
|
||||
{
|
||||
"name": "node-uplink",
|
||||
"scope": "node"
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"id": "package",
|
||||
"type": "package",
|
||||
"package": "dhcpcd"
|
||||
},
|
||||
{
|
||||
"id": "config",
|
||||
"type": "file",
|
||||
"path": "/etc/dhcpcd.conf",
|
||||
"mode": "0644",
|
||||
"into": "block",
|
||||
"at": "start",
|
||||
"content": "# The mesh's two lines (module dhcpcd, novox/hq ADR 0117). Global options, so\n# kept above any interface line; read at dhcpcd's next start.\nnohook resolv.conf\ndenyinterfaces mesh0\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@
|
||||
],
|
||||
"claims": [
|
||||
{
|
||||
"name": "the-intrusion-prevention",
|
||||
"name": "node-intrusion-prevention",
|
||||
"scope": "node"
|
||||
}
|
||||
],
|
||||
@@ -33,7 +33,7 @@
|
||||
"type": "file",
|
||||
"path": "/etc/fail2ban/jail.local",
|
||||
"mode": "0644",
|
||||
"content": "[INCLUDES]\n\nbefore = paths-arch.conf\n\n[DEFAULT]\n\nbantime = 10m\nfindtime = 10m\nmaxretry = 5\n\nbanaction = ufw\nbanaction_allports = iptables-allports\n\n[sshd]\nenabled = true\nport = ssh\nlogpath = %(sshd_log)s\nbackend = %(sshd_backend)s\n"
|
||||
"content": "[INCLUDES]\n\nbefore = paths-arch.conf\n\n[DEFAULT]\n\n# Never act on the machine itself or on a tunnel peer: the mesh's private range is\n# ${machine:mesh-range}, named here rather than written as a value the module cannot\n# know (novox/hq ADR 0112). Without this, fail2ban could ban the mesh's own nodes.\nignoreip = 127.0.0.1/8 ::1 ${machine:mesh-range}\n\nbantime = 10m\nfindtime = 10m\nmaxretry = 5\n\nbanaction = ufw\nbanaction_allports = iptables-allports\n\n[sshd]\nenabled = true\nport = ssh\nlogpath = %(sshd_log)s\nbackend = %(sshd_backend)s\n"
|
||||
},
|
||||
{
|
||||
"id": "jail-sshd",
|
||||
|
||||
+39
-23
@@ -24,14 +24,14 @@
|
||||
}
|
||||
},
|
||||
"binds": {
|
||||
"postgres-database": "/var/lib/gitea/database.json",
|
||||
"route": "/var/lib/gitea/route.json"
|
||||
"postgres-database": "${dir:state}/database.json",
|
||||
"route": "${dir:state}/route.json"
|
||||
},
|
||||
"secrets": {
|
||||
"postgres-database": "/var/lib/gitea/database.secret",
|
||||
"postgres-database": "${dir:state}/database.secret",
|
||||
"secret": {
|
||||
"internal-token": "/var/lib/gitea/internal-token.secret",
|
||||
"admin": "/var/lib/gitea/admin.secret"
|
||||
"internal-token": "${dir:state}/internal-token.secret",
|
||||
"admin": "${dir:state}/admin.secret"
|
||||
}
|
||||
},
|
||||
"capabilities": [
|
||||
@@ -53,22 +53,36 @@
|
||||
"port": 22,
|
||||
"protocol": "tcp",
|
||||
"from": "mesh",
|
||||
"why": "git over ssh, gitea's own unmodified sshd. Published on the machine's own side at 222, the mesh's fixed public convention — not 22, which the machine's own daemon holds and a module does not take"
|
||||
"why": "git over ssh, gitea's own unmodified sshd. Published on the machine's own side at 222, the mesh's fixed public convention \u2014 not 22, which the machine's own daemon holds and a module does not take"
|
||||
}
|
||||
],
|
||||
"serves": {
|
||||
"package-registry": {
|
||||
"npm-package-registry": {
|
||||
"scheme": "http",
|
||||
"port": 3000,
|
||||
"npm-path": "/api/packages/novox/npm/"
|
||||
},
|
||||
"git": {
|
||||
"scheme": "http",
|
||||
"port": 3000
|
||||
}
|
||||
},
|
||||
"receives": {
|
||||
"package-registry": "/var/lib/gitea/grants/mesh.json"
|
||||
"npm-package-registry": "${dir:grants}/npm.json"
|
||||
},
|
||||
"grants": {
|
||||
"package-registry": "/var/lib/gitea/grants"
|
||||
"npm-package-registry": "${dir:grants}"
|
||||
},
|
||||
"claims": [
|
||||
{
|
||||
"name": "npm-package-registry",
|
||||
"scope": "mesh"
|
||||
},
|
||||
{
|
||||
"name": "git",
|
||||
"scope": "mesh"
|
||||
}
|
||||
],
|
||||
"own-secrets": {
|
||||
"broker": "/var/lib/mesh/gitea/broker"
|
||||
},
|
||||
@@ -88,26 +102,24 @@
|
||||
{
|
||||
"id": "state",
|
||||
"type": "directory",
|
||||
"path": "/var/lib/gitea",
|
||||
"mode": "0700"
|
||||
"mode": "0700",
|
||||
"place": "."
|
||||
},
|
||||
{
|
||||
"id": "grants",
|
||||
"type": "directory",
|
||||
"path": "/var/lib/gitea/grants",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "server-env",
|
||||
"type": "file",
|
||||
"path": "/var/lib/gitea/server.env",
|
||||
"path": "${dir:state}/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=${bound:postgres-database:as}\nGITEA__database__USER=${bound:postgres-database:as}\nGITEA__database__PASSWD=${secret:postgres-database}\n"
|
||||
},
|
||||
{
|
||||
"id": "data",
|
||||
"type": "directory",
|
||||
"path": "/services/gitea/gitea",
|
||||
"mode": "0700",
|
||||
"owner": "1000:1000"
|
||||
},
|
||||
@@ -122,14 +134,14 @@
|
||||
"USER_GID": "1000"
|
||||
},
|
||||
"env-file": [
|
||||
"/var/lib/gitea/server.env"
|
||||
"${dir:state}/server.env"
|
||||
],
|
||||
"ports": [
|
||||
"3000",
|
||||
"222:22"
|
||||
],
|
||||
"volumes": [
|
||||
"/services/gitea/gitea:/data"
|
||||
"${dir:data}:/data"
|
||||
],
|
||||
"secrets-in-environment": "gitea honours GITEA__database__PASSWD__FILE and GITEA__security__INTERNAL_TOKEN__FILE; convertible, awaiting a bed that proves it"
|
||||
},
|
||||
@@ -145,11 +157,11 @@
|
||||
"MESH_GITEA_ADMIN_USER": "mesh-admin"
|
||||
},
|
||||
"env-file": [
|
||||
"/var/lib/gitea/server.env"
|
||||
"${dir:state}/server.env"
|
||||
],
|
||||
"volumes": [
|
||||
"/services/gitea/gitea:/data",
|
||||
"/var/lib/gitea/admin.secret:/run/secrets/admin:ro"
|
||||
"${dir:data}:/data",
|
||||
"${dir:state}/admin.secret:/run/secrets/admin:ro"
|
||||
],
|
||||
"args": [
|
||||
"/bin/sh",
|
||||
@@ -174,8 +186,8 @@
|
||||
"volumes": [
|
||||
"/var/lib/mesh/gitea/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/mesh/gitea/config.json:/run/config/config.json:ro",
|
||||
"/var/lib/gitea/grants:/var/lib/gitea/grants:ro",
|
||||
"/var/lib/gitea/admin.secret:/run/secrets/admin:ro",
|
||||
"${dir:grants}:${dir:grants}:ro",
|
||||
"${dir:state}/admin.secret:/run/secrets/admin:ro",
|
||||
"/var/lib/mesh/gitea/state:/run/state"
|
||||
],
|
||||
"env": {
|
||||
@@ -185,7 +197,7 @@
|
||||
"MESH_GITEA_ADMIN_USER": "mesh-admin",
|
||||
"MESH_GITEA_ADMIN_PASSWORD_FILE": "/run/secrets/admin",
|
||||
"MESH_GITEA_STATE_DIR": "/run/state",
|
||||
"MESH_RECEIVES": "/var/lib/gitea/grants/mesh.json"
|
||||
"MESH_RECEIVES": "${dir:grants}/npm.json"
|
||||
},
|
||||
"artifact": "runtime",
|
||||
"restart-on": [
|
||||
@@ -195,7 +207,11 @@
|
||||
],
|
||||
"provides": [
|
||||
{
|
||||
"name": "package-registry",
|
||||
"name": "npm-package-registry",
|
||||
"scope": "mesh"
|
||||
},
|
||||
{
|
||||
"name": "git",
|
||||
"scope": "mesh"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
// gitea's provisioner — the adapter that makes gitea a provider of the mesh `package-registry`
|
||||
// interface. The reconcile loop, the contributions file, and reading the mesh's minted password are
|
||||
// the sdk harness's; this writes only the per-service half: how gitea creates and removes a
|
||||
// consumer's npm credential (novox/hq ADR 0048/0076).
|
||||
// gitea's provisioner — the adapter that makes gitea a provider of the mesh
|
||||
// `npm-package-registry` interface. The reconcile loop, the contributions file, and reading the
|
||||
// mesh's minted password are the sdk harness's; this writes only the per-service half: how gitea
|
||||
// creates and removes a consumer's npm credential (novox/hq ADR 0048/0076).
|
||||
//
|
||||
// The `package-registry` interface: a consumer authenticates to the npm registry at
|
||||
// **A package registry seat is one per ecosystem (novox/hq ADR 0109).** gitea holds the npm seat
|
||||
// (ADR 0110). Adding cargo or PyPI is adding a provision — another `provides` entry, another
|
||||
// `receives` path and another registration below — not widening this one. `git`, which gitea also
|
||||
// provides, mints nothing and so registers nothing here: the mesh's own repositories are public,
|
||||
// and a clone credential is not yet decided (ADR 0111).
|
||||
//
|
||||
// The `npm-package-registry` interface: a consumer authenticates to the npm registry at
|
||||
// `/api/packages/novox/npm/` with basic auth, as `as` with the password the mesh minted, and can
|
||||
// read and write packages under the `@novox` scope. The registry's npm owner is the gitea org
|
||||
// `novox`; a consumer is a gitea *user* placed on that org's package team.
|
||||
@@ -26,7 +32,11 @@ const PACKAGE_TEAM = "packages";
|
||||
|
||||
const gitea = GiteaAdmin.fromEnv();
|
||||
|
||||
runProvisioner("package-registry", {
|
||||
// Where this registration's contributions land comes from $MESH_RECEIVES, never a path written
|
||||
// here: the mesh writes the file where the manifest's `receives` says, and a second copy of that
|
||||
// path in code would drift from it. One variable carries one path, so a second registration in this
|
||||
// module needs the mesh to say where each provision's file is — not yet possible, and not faked.
|
||||
runProvisioner("npm-package-registry", {
|
||||
async create(p: Provision): Promise<void> {
|
||||
// The org and its package team are the same for every consumer; ensuring them per-create is
|
||||
// idempotent and needs no separate bootstrap step.
|
||||
|
||||
@@ -14,12 +14,15 @@
|
||||
"mongodb-database": {
|
||||
"name": "invoicing"
|
||||
},
|
||||
"s3-bucket": {
|
||||
"bucket": "invoicing"
|
||||
},
|
||||
"route": {
|
||||
"label": "invoicing",
|
||||
"port": 80
|
||||
"site": {
|
||||
"label": "invoicing",
|
||||
"port": 80
|
||||
},
|
||||
"api": {
|
||||
"label": "invoicing-api",
|
||||
"port": 9000
|
||||
}
|
||||
}
|
||||
},
|
||||
"binds": {
|
||||
@@ -63,7 +66,7 @@
|
||||
"type": "file",
|
||||
"path": "/var/lib/invoicing/api.env",
|
||||
"mode": "0600",
|
||||
"content": "NODE_ENV=production\nPORT=9000\nMONGO_URL=mongodb://${bound:mongodb-database:as}:${secret:mongodb-database}@${bound:mongodb-database:at}:${bound:mongodb-database:port}/invoicing?authSource=admin\nMINIO_BUCKET=invoicing\nMINIO_ENDPOINT=${bound:s3-bucket:at}\nMINIO_PORT=${bound:s3-bucket:port}\nMINIO_ACCESSKEY=${bound:s3-bucket:as}\nMINIO_SECRET=${secret:s3-bucket}\n"
|
||||
"content": "NODE_ENV=production\nPORT=9000\nMONGO_URL=mongodb://${bound:mongodb-database:as}:${secret:mongodb-database}@${bound:mongodb-database:at}:${bound:mongodb-database:port}/${bound:mongodb-database:as}?authSource=${bound:mongodb-database:as}\nMONGO_DB=${bound:mongodb-database:as}\nMINIO_BUCKET=mesh-novox-invoice\nMINIO_ENDPOINT=${bound:s3-bucket:at}\nMINIO_PORT=${bound:s3-bucket:port}\nMINIO_ACCESSKEY=${bound:s3-bucket:as}\nMINIO_SECRET=${secret:s3-bucket}\n"
|
||||
},
|
||||
{
|
||||
"id": "net",
|
||||
|
||||
@@ -88,7 +88,9 @@
|
||||
"env": {
|
||||
"KC_DB": "postgres",
|
||||
"KC_HTTP_ENABLED": "true",
|
||||
"KC_HEALTH_ENABLED": "true"
|
||||
"KC_HEALTH_ENABLED": "true",
|
||||
"KC_HOSTNAME": "https://keycloak.novox.be",
|
||||
"KC_PROXY_HEADERS": "xforwarded"
|
||||
},
|
||||
"env-file": [
|
||||
"/var/lib/keycloak/admin.env",
|
||||
@@ -118,7 +120,7 @@
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_KEYCLOAK_URL": "http://127.0.0.1:8080",
|
||||
"MESH_KEYCLOAK_URL": "http://127.0.0.1:${port:8080}",
|
||||
"MESH_KEYCLOAK_CONFIG_FILE": "/run/config/config.json"
|
||||
},
|
||||
"restart-on": [
|
||||
|
||||
@@ -81,12 +81,6 @@
|
||||
"path": "/var/lib/mesh-broker",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "broker-tls",
|
||||
"type": "directory",
|
||||
"path": "/var/lib/mesh-broker-tls",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "server",
|
||||
"type": "container",
|
||||
@@ -147,5 +141,11 @@
|
||||
"from": "Dockerfile"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"accesses": [
|
||||
{
|
||||
"path": "/var/lib/mesh-broker-tls",
|
||||
"mode": "read"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+54
-71
@@ -41,15 +41,15 @@
|
||||
}
|
||||
},
|
||||
"binds": {
|
||||
"postgres-database": "/var/lib/mailu/database.json",
|
||||
"route": "/var/lib/mailu/route.json"
|
||||
"postgres-database": "${dir:state}/database.json",
|
||||
"route": "${dir:state}/route.json"
|
||||
},
|
||||
"secrets": {
|
||||
"postgres-database": "/var/lib/mailu/database.secret",
|
||||
"postgres-database": "${dir:state}/database.secret",
|
||||
"secret": {
|
||||
"secret-key": "/var/lib/mailu/secret-key.secret",
|
||||
"admin": "/var/lib/mailu/admin.secret",
|
||||
"api-token": "/var/lib/mailu/api-token.secret"
|
||||
"secret-key": "${dir:state}/secret-key.secret",
|
||||
"admin": "${dir:state}/admin.secret",
|
||||
"api-token": "${dir:state}/api-token.secret"
|
||||
}
|
||||
},
|
||||
"emits": [
|
||||
@@ -140,143 +140,125 @@
|
||||
{
|
||||
"id": "state",
|
||||
"type": "directory",
|
||||
"path": "/var/lib/mailu",
|
||||
"mode": "0700"
|
||||
"mode": "0700",
|
||||
"place": "."
|
||||
},
|
||||
{
|
||||
"id": "grants",
|
||||
"type": "directory",
|
||||
"path": "/var/lib/mailu/grants",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "data-automx",
|
||||
"type": "directory",
|
||||
"path": "/services/mailu/data/automx",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "config-env",
|
||||
"type": "file",
|
||||
"path": "/var/lib/mailu/mailu.env",
|
||||
"path": "${dir:state}/mailu.env",
|
||||
"mode": "0644",
|
||||
"content": "ADMIN_ADDRESS=mailu-admin\nANTISPAM_ADDRESS=mailu-antispam\nANTIVIRUS_ADDRESS=mailu-antivirus\nIMAP_ADDRESS=mailu-imap\nSMTP_ADDRESS=mailu-smtp\nFRONT_ADDRESS=mailu-front\nWEBMAIL_ADDRESS=mailu-webmail\nWEBDAV_ADDRESS=mailu-webdav\nREDIS_ADDRESS=mailu-redis\nPORTS=25,80,443,465,993,995,4190,110,143,587\nDOMAIN=novox.be\nHOSTNAMES=mail.novox.be\nPOSTMASTER=admin\nSITENAME=Novox\nWEBSITE=https://novox.be\nTLS_FLAVOR=cert\nSUBNET=192.168.203.0/24\nCOMPOSE_PROJECT_NAME=mailu\nANTIVIRUS=clamav\nWEBMAIL=roundcube\nWEBDAV=radicale\nFETCHMAIL_ENABLED=True\nFETCHMAIL_DELAY=600\nADMIN=true\nWEB_ADMIN=/admin\nWEB_WEBMAIL=/webmail\nWEBROOT_REDIRECT=/webmail\nWEBMAIL_ADDRESS=webmail\nAPI=true\nWEB_API=/api\nAUTH_RATELIMIT_IP=6000/hour\nAUTH_RATELIMIT_USER=1000/day\nCREDENTIAL_ROUNDS=12\nPASSWORD_SCHEME=PBKDF2\nDISABLE_STATISTICS=True\nMESSAGE_SIZE_LIMIT=50000000\nMESSAGE_RATELIMIT=200/day\nRECIPIENT_DELIMITER=+\nPOSTFIX_MYNETWORKS=127.0.0.0/8 [::1]/128\nRELAYNETS=\nRELAYHOST=\nREJECT_UNLISTED_RECIPIENT=\nDB_FLAVOR=postgresql\nINITIAL_ADMIN_ACCOUNT=admin\nINITIAL_ADMIN_DOMAIN=novox.be\nINITIAL_ADMIN_MODE=ifmissing\nSMTP_PORT=25\nSMTPS_PORT=465\nSUBMISSION_PORT=587\nPOP3_PORT=110\nPOP3S_PORT=995\nIMAP_PORT=143\nIMAPS_PORT=993\nHTTP_PORT=7080\nHTTPS_PORT=7443\nAUTOMX_PORT=4243\nAMX_SMTP_ADDRESS=mail.novox.be\nAMX_SMTP_PORT=587\nAMX_IMAP_ADDRESS=mail.novox.be\nAMX_IMAP_PORT=143\nAMX_MAIL_DOMAINS=novox.be\nDMARC_RUA=admin\nDMARC_RUF=admin\nLETSENCRYPT_SHORTCHAIN=True\nTZ=Etc/UTC\nLOG_LEVEL=INFO\nWELCOME=false\nREAL_IP_HEADER=X-Real-IP\nREAL_IP_FROM=142.132.152.141\nCOMPRESSION=\nCOMPRESS_LEVEL=\nCOMPRESSION_LEVEL=\nBIND_ADDRESS4=127.0.0.1\nBIND_ADDRESS6=::1\nMAILU_VERSION=1.9\nDOCKER_ORG=mailu\nDOCKER_PREFIX=\nWELCOME_SUBJECT=Welcome to your new email account\nWELCOME_BODY=Welcome to your new email account, if you can read this, then it is configured properly!\n"
|
||||
"content": "ADMIN_ADDRESS=mailu-admin\nANTISPAM_ADDRESS=mailu-antispam\nANTIVIRUS_ADDRESS=mailu-antivirus\nIMAP_ADDRESS=mailu-imap\nSMTP_ADDRESS=mailu-smtp\nFRONT_ADDRESS=mailu-front\nWEBMAIL_ADDRESS=mailu-webmail\nWEBDAV_ADDRESS=mailu-webdav\nREDIS_ADDRESS=mailu-redis\nPORTS=25,80,443,465,993,995,4190,110,143,587\nDOMAIN=novox.be\nHOSTNAMES=mail.novox.be\nPOSTMASTER=admin\nSITENAME=Novox\nWEBSITE=https://novox.be\nTLS_FLAVOR=letsencrypt\nSUBNET=192.168.203.0/24\nCOMPOSE_PROJECT_NAME=mailu\nANTIVIRUS=clamav\nWEBMAIL=roundcube\nWEBDAV=radicale\nFETCHMAIL_ENABLED=True\nFETCHMAIL_DELAY=600\nADMIN=true\nWEB_ADMIN=/admin\nWEB_WEBMAIL=/webmail\nWEBROOT_REDIRECT=/webmail\nAPI=true\nWEB_API=/api\nAUTH_RATELIMIT_IP=6000/hour\nAUTH_RATELIMIT_USER=1000/day\nCREDENTIAL_ROUNDS=12\nPASSWORD_SCHEME=PBKDF2\nDISABLE_STATISTICS=True\nMESSAGE_SIZE_LIMIT=50000000\nMESSAGE_RATELIMIT=200/day\nRECIPIENT_DELIMITER=+\nPOSTFIX_MYNETWORKS=127.0.0.0/8 [::1]/128\nRELAYNETS=\nRELAYHOST=\nREJECT_UNLISTED_RECIPIENT=\nDB_FLAVOR=postgresql\nINITIAL_ADMIN_ACCOUNT=admin\nINITIAL_ADMIN_DOMAIN=novox.be\nINITIAL_ADMIN_MODE=ifmissing\nSMTP_PORT=25\nSMTPS_PORT=465\nSUBMISSION_PORT=587\nPOP3_PORT=110\nPOP3S_PORT=995\nIMAP_PORT=143\nIMAPS_PORT=993\nHTTP_PORT=7080\nHTTPS_PORT=7443\nAUTOMX_PORT=4243\nAMX_SMTP_ADDRESS=mail.novox.be\nAMX_SMTP_PORT=587\nAMX_IMAP_ADDRESS=mail.novox.be\nAMX_IMAP_PORT=143\nAMX_MAIL_DOMAINS=novox.be\nDMARC_RUA=admin\nDMARC_RUF=admin\nLETSENCRYPT_SHORTCHAIN=True\nTZ=Etc/UTC\nLOG_LEVEL=INFO\nWELCOME=false\nREAL_IP_HEADER=X-Real-IP\nREAL_IP_FROM=142.132.152.141\nCOMPRESSION=\nCOMPRESS_LEVEL=\nCOMPRESSION_LEVEL=\nBIND_ADDRESS4=127.0.0.1\nBIND_ADDRESS6=::1\nMAILU_VERSION=1.9\nDOCKER_ORG=mailu\nDOCKER_PREFIX=\nWELCOME_SUBJECT=Welcome to your new email account\nWELCOME_BODY=Welcome to your new email account, if you can read this, then it is configured properly!\n"
|
||||
},
|
||||
{
|
||||
"id": "secret-env",
|
||||
"type": "file",
|
||||
"path": "/var/lib/mailu/secret.env",
|
||||
"path": "${dir:state}/secret.env",
|
||||
"mode": "0600",
|
||||
"content": "SECRET_KEY=${secret:secret-key}\n"
|
||||
},
|
||||
{
|
||||
"id": "database-env",
|
||||
"type": "file",
|
||||
"path": "/var/lib/mailu/database.env",
|
||||
"path": "${dir:state}/database.env",
|
||||
"mode": "0600",
|
||||
"content": "DB_FLAVOR=postgresql\nDB_HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\nDB_USER=${bound:postgres-database:as}\nDB_NAME=${bound:postgres-database:as}\nDB_PW=${secret:postgres-database}\n"
|
||||
},
|
||||
{
|
||||
"id": "admin-env",
|
||||
"type": "file",
|
||||
"path": "/var/lib/mailu/admin.env",
|
||||
"path": "${dir:state}/admin.env",
|
||||
"mode": "0600",
|
||||
"content": "INITIAL_ADMIN_PW=${secret:admin}\nAPI_TOKEN=${secret:api-token}\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-mail",
|
||||
"type": "directory",
|
||||
"path": "/services/mailu/data/mail",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "data-mailqueue",
|
||||
"type": "directory",
|
||||
"path": "/services/mailu/data/mailqueue",
|
||||
"mode": "0755"
|
||||
},
|
||||
{
|
||||
"id": "data-filter",
|
||||
"type": "directory",
|
||||
"path": "/services/mailu/data/filter",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "data-clamav",
|
||||
"type": "directory",
|
||||
"path": "/services/mailu/data/clamav",
|
||||
"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-dav",
|
||||
"type": "directory",
|
||||
"path": "/services/mailu/data/dav",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "data-fetchmail",
|
||||
"type": "directory",
|
||||
"path": "/services/mailu/data/data/fetchmail",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "data-overrides-nginx",
|
||||
"type": "directory",
|
||||
"path": "/services/mailu/data/overrides/nginx",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "data-overrides-dovecot",
|
||||
"type": "directory",
|
||||
"path": "/services/mailu/data/overrides/dovecot",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "data-overrides-postfix",
|
||||
"type": "directory",
|
||||
"path": "/services/mailu/data/overrides/postfix",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "data-overrides-rspamd",
|
||||
"type": "directory",
|
||||
"path": "/services/mailu/data/overrides/rspamd",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "data-overrides-roundcube",
|
||||
"type": "directory",
|
||||
"path": "/services/mailu/data/overrides/roundcube",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
@@ -291,8 +273,8 @@
|
||||
"image": "ghcr.io/mailu/unbound@sha256:3a0fdfb364a63f4f9259526e013c1ef40f5f14de3621ce1560804b3a5909584a",
|
||||
"network": "mailu",
|
||||
"env-file": [
|
||||
"/var/lib/mailu/mailu.env",
|
||||
"/var/lib/mailu/secret.env"
|
||||
"${dir:state}/mailu.env",
|
||||
"${dir:state}/secret.env"
|
||||
],
|
||||
"secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified",
|
||||
"ip": "192.168.203.254"
|
||||
@@ -304,7 +286,7 @@
|
||||
"image": "redis@sha256:4bed291aa5efb9f0d77b76ff7d4ab71eee410962965d052552db1fb80576431d",
|
||||
"network": "mailu",
|
||||
"volumes": [
|
||||
"/services/mailu/data/redis:/data"
|
||||
"${dir:data-redis}:/data"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -314,14 +296,14 @@
|
||||
"image": "ghcr.io/mailu/admin@sha256:6dbfdadc4a9590dcb7652357b505200115b689b74008653bbf369e4599a3be5a",
|
||||
"network": "mailu",
|
||||
"env-file": [
|
||||
"/var/lib/mailu/mailu.env",
|
||||
"/var/lib/mailu/secret.env",
|
||||
"/var/lib/mailu/database.env",
|
||||
"/var/lib/mailu/admin.env"
|
||||
"${dir:state}/mailu.env",
|
||||
"${dir:state}/secret.env",
|
||||
"${dir:state}/database.env",
|
||||
"${dir:state}/admin.env"
|
||||
],
|
||||
"volumes": [
|
||||
"/services/mailu/data/data:/data",
|
||||
"/services/mailu/data/dkim:/dkim"
|
||||
"${dir:data-data}:/data",
|
||||
"${dir:data-dkim}:/dkim"
|
||||
],
|
||||
"secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified",
|
||||
"dns": [
|
||||
@@ -335,11 +317,11 @@
|
||||
"image": "ghcr.io/mailu/dovecot@sha256:7f0ed5db996fbdc00adc5c5e38a08492e04f7eb4a9fbd66a03aa9a28ddf23993",
|
||||
"network": "mailu",
|
||||
"env-file": [
|
||||
"/var/lib/mailu/mailu.env"
|
||||
"${dir:state}/mailu.env"
|
||||
],
|
||||
"volumes": [
|
||||
"/services/mailu/data/mail:/mail",
|
||||
"/services/mailu/data/overrides/dovecot:/overrides:ro"
|
||||
"${dir:data-mail}:/mail",
|
||||
"${dir:data-overrides-dovecot}:/overrides:ro"
|
||||
],
|
||||
"dns": [
|
||||
"192.168.203.254"
|
||||
@@ -352,11 +334,11 @@
|
||||
"image": "ghcr.io/mailu/postfix@sha256:e2e49f39e53b80eac9e7a2f18d9df11edeb4914fd62dbba89b3155e8e034f62e",
|
||||
"network": "mailu",
|
||||
"env-file": [
|
||||
"/var/lib/mailu/mailu.env"
|
||||
"${dir:state}/mailu.env"
|
||||
],
|
||||
"volumes": [
|
||||
"/services/mailu/data/mailqueue:/queue",
|
||||
"/services/mailu/data/overrides/postfix:/overrides:ro"
|
||||
"${dir:data-mailqueue}:/queue",
|
||||
"${dir:data-overrides-postfix}:/overrides:ro"
|
||||
],
|
||||
"dns": [
|
||||
"192.168.203.254"
|
||||
@@ -369,11 +351,11 @@
|
||||
"image": "ghcr.io/mailu/rspamd@sha256:ff3666d8a61f17d309c5c6f6bcf4d40470b82299ca706ac650301175bb1a079d",
|
||||
"network": "mailu",
|
||||
"env-file": [
|
||||
"/var/lib/mailu/mailu.env"
|
||||
"${dir:state}/mailu.env"
|
||||
],
|
||||
"volumes": [
|
||||
"/services/mailu/data/filter:/var/lib/rspamd",
|
||||
"/services/mailu/data/overrides/rspamd:/etc/rspamd/override.d:ro"
|
||||
"${dir:data-filter}:/var/lib/rspamd",
|
||||
"${dir:data-overrides-rspamd}:/etc/rspamd/override.d:ro"
|
||||
],
|
||||
"dns": [
|
||||
"192.168.203.254"
|
||||
@@ -386,7 +368,7 @@
|
||||
"image": "clamav/clamav-debian@sha256:b12ef8fefddbba7d88de59bea8a32622f365339154adf02d38fd089112e6745a",
|
||||
"network": "mailu",
|
||||
"volumes": [
|
||||
"/services/mailu/data/clamav:/var/lib/clamav"
|
||||
"${dir:data-clamav}:/var/lib/clamav"
|
||||
],
|
||||
"dns": [
|
||||
"192.168.203.254"
|
||||
@@ -399,12 +381,12 @@
|
||||
"image": "ghcr.io/mailu/webmail@sha256:bdbee44cdb05a4658f0e3b62cc448de55ca8f8aea172279fda594826144c04f6",
|
||||
"network": "mailu",
|
||||
"env-file": [
|
||||
"/var/lib/mailu/mailu.env",
|
||||
"/var/lib/mailu/secret.env"
|
||||
"${dir:state}/mailu.env",
|
||||
"${dir:state}/secret.env"
|
||||
],
|
||||
"volumes": [
|
||||
"/services/mailu/data/webmail:/data",
|
||||
"/services/mailu/data/overrides/roundcube:/overrides:ro"
|
||||
"${dir:data-webmail}:/data",
|
||||
"${dir:data-overrides-roundcube}:/overrides:ro"
|
||||
],
|
||||
"secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified",
|
||||
"dns": [
|
||||
@@ -418,11 +400,11 @@
|
||||
"image": "ghcr.io/mailu/radicale@sha256:690ed6edf189dfef100a5a8b37c195ebf5d9241ac5f23f2f44b8b7b75726e3de",
|
||||
"network": "mailu",
|
||||
"env-file": [
|
||||
"/var/lib/mailu/mailu.env",
|
||||
"/var/lib/mailu/secret.env"
|
||||
"${dir:state}/mailu.env",
|
||||
"${dir:state}/secret.env"
|
||||
],
|
||||
"volumes": [
|
||||
"/services/mailu/data/dav:/data"
|
||||
"${dir:data-dav}:/data"
|
||||
],
|
||||
"secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified",
|
||||
"dns": [
|
||||
@@ -436,11 +418,11 @@
|
||||
"image": "ghcr.io/mailu/fetchmail@sha256:f881c8412d3bbe73d638469b48321558d6403a9d45bfa043c1e52c752103d42d",
|
||||
"network": "mailu",
|
||||
"env-file": [
|
||||
"/var/lib/mailu/mailu.env",
|
||||
"/var/lib/mailu/secret.env"
|
||||
"${dir:state}/mailu.env",
|
||||
"${dir:state}/secret.env"
|
||||
],
|
||||
"volumes": [
|
||||
"/services/mailu/data/data/fetchmail:/data"
|
||||
"${dir:data-fetchmail}:/data"
|
||||
],
|
||||
"secrets-in-environment": "mailu-admin honours SECRET_KEY_FILE, DB_PW_FILE and API_TOKEN_FILE (configuration.py) but INITIAL_ADMIN_PW is env-only (start.py); the remaining containers' need for SECRET_KEY is unverified",
|
||||
"dns": [
|
||||
@@ -454,7 +436,7 @@
|
||||
"image": "ghcr.io/mailu/nginx@sha256:36f98897cd1bc9d27628bbb4e04bdf60147af2ec7507d6da77f002c4f256896d",
|
||||
"network": "mailu",
|
||||
"env-file": [
|
||||
"/var/lib/mailu/mailu.env"
|
||||
"${dir:state}/mailu.env"
|
||||
],
|
||||
"ports": [
|
||||
"25",
|
||||
@@ -468,8 +450,8 @@
|
||||
"7443:443"
|
||||
],
|
||||
"volumes": [
|
||||
"/services/mailu/data/certs:/certs",
|
||||
"/services/mailu/data/overrides/nginx:/overrides:ro"
|
||||
"${dir:data-certs}:/certs",
|
||||
"${dir:data-overrides-nginx}:/overrides:ro"
|
||||
],
|
||||
"dns": [
|
||||
"192.168.203.254"
|
||||
@@ -490,8 +472,8 @@
|
||||
"network": "mailu",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/mailu/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/mailu/api-token.secret:/run/secrets/api-token:ro",
|
||||
"/var/lib/mailu/grants:/var/lib/mailu/grants:ro",
|
||||
"${dir:state}/api-token.secret:/run/secrets/api-token:ro",
|
||||
"${dir:grants}:${dir:grants}:ro",
|
||||
"/var/lib/mesh/mailu/config.json:/run/config/config.json:ro",
|
||||
"/var/run/docker.sock:/var/run/docker.sock"
|
||||
],
|
||||
@@ -502,7 +484,7 @@
|
||||
"MESH_MAILU_IMAP_CONTAINER": "mailu-imap",
|
||||
"MESH_MAILU_CONFIG_FILE": "/run/config/config.json",
|
||||
"MESH_MAILU_DOMAIN": "novox.be",
|
||||
"MESH_RECEIVES": "/var/lib/mailu/grants/mesh.json"
|
||||
"MESH_RECEIVES": "${dir:grants}/mesh.json"
|
||||
},
|
||||
"restart-on": [
|
||||
"runtime-config"
|
||||
@@ -516,13 +498,13 @@
|
||||
"artifact": "automx",
|
||||
"network": "mailu",
|
||||
"env-file": [
|
||||
"/var/lib/mailu/mailu.env"
|
||||
"${dir:state}/mailu.env"
|
||||
],
|
||||
"ports": [
|
||||
"4243"
|
||||
],
|
||||
"volumes": [
|
||||
"/services/mailu/data/automx:/data"
|
||||
"${dir:data-automx}:/data"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -565,13 +547,14 @@
|
||||
"serves": {
|
||||
"smtp": {
|
||||
"port": 587,
|
||||
"domain": "novox.be"
|
||||
"domain": "novox.be",
|
||||
"name": "mail.novox.be"
|
||||
}
|
||||
},
|
||||
"receives": {
|
||||
"smtp": "/var/lib/mailu/grants/mesh.json"
|
||||
"smtp": "${dir:grants}/mesh.json"
|
||||
},
|
||||
"grants": {
|
||||
"smtp": "/var/lib/mailu/grants"
|
||||
"smtp": "${dir:grants}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
],
|
||||
"claims": [
|
||||
{
|
||||
"name": "the-catalogue",
|
||||
"name": "mesh-catalog",
|
||||
"scope": "mesh"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -95,14 +95,14 @@
|
||||
{
|
||||
"id": "net",
|
||||
"type": "network",
|
||||
"name": "minio"
|
||||
"name": "minio-net"
|
||||
},
|
||||
{
|
||||
"id": "server",
|
||||
"type": "container",
|
||||
"name": "minio",
|
||||
"image": "docker.io/pgsty/minio@sha256:b6bfe7239bfc83fb90d31612d9704d86039dd714f7904b3f1ad68f211e602372",
|
||||
"network": "minio",
|
||||
"network": "minio-net",
|
||||
"args": [
|
||||
"server",
|
||||
"/data",
|
||||
@@ -122,6 +122,7 @@
|
||||
],
|
||||
"env": {
|
||||
"MINIO_ROOT_PASSWORD_FILE": "/run/secrets/root",
|
||||
"MINIO_BROWSER_REDIRECT_URL": "https://files.novox.be",
|
||||
"MINIO_REGION": "eu-west"
|
||||
}
|
||||
},
|
||||
@@ -129,7 +130,7 @@
|
||||
"id": "runtime",
|
||||
"type": "container",
|
||||
"name": "mesh-minio",
|
||||
"network": "minio",
|
||||
"network": "minio-net",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/minio/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/minio/grants:/var/lib/minio/grants:ro",
|
||||
|
||||
+10
-12
@@ -32,13 +32,13 @@
|
||||
}
|
||||
},
|
||||
"receives": {
|
||||
"mongodb-database": "/var/lib/mongodb/grants/mesh.json"
|
||||
"mongodb-database": "${dir:grants}/mesh.json"
|
||||
},
|
||||
"grants": {
|
||||
"mongodb-database": "/var/lib/mongodb/grants"
|
||||
"mongodb-database": "${dir:grants}"
|
||||
},
|
||||
"own-secrets": {
|
||||
"root": "/var/lib/mongodb/root.secret",
|
||||
"root": "${dir:state}/root.secret",
|
||||
"broker": "/var/lib/mesh/mongodb/broker"
|
||||
},
|
||||
"secrets-owner": "999:999",
|
||||
@@ -52,19 +52,17 @@
|
||||
{
|
||||
"id": "state",
|
||||
"type": "directory",
|
||||
"path": "/var/lib/mongodb",
|
||||
"mode": "0700"
|
||||
"mode": "0700",
|
||||
"place": "."
|
||||
},
|
||||
{
|
||||
"id": "grants",
|
||||
"type": "directory",
|
||||
"path": "/var/lib/mongodb/grants",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "data",
|
||||
"type": "directory",
|
||||
"path": "/services/mongodb/db-data",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
@@ -86,8 +84,8 @@
|
||||
"27017"
|
||||
],
|
||||
"volumes": [
|
||||
"/services/mongodb/db-data:/data/db",
|
||||
"/var/lib/mongodb/root.secret:/run/secrets/root:ro"
|
||||
"${dir:data}:/data/db",
|
||||
"${dir:state}/root.secret:/run/secrets/root:ro"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -97,14 +95,14 @@
|
||||
"network": "mongodb",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/mongodb/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/mongodb/grants:/var/lib/mongodb/grants:ro",
|
||||
"/var/lib/mongodb/root.secret:/run/secrets/root:ro"
|
||||
"${dir:grants}:${dir:grants}:ro",
|
||||
"${dir:state}/root.secret:/run/secrets/root:ro"
|
||||
],
|
||||
"env": {
|
||||
"MESH_PROVISION_MONGODB": "mongodb://root@mongodb-server:27017/admin?authSource=admin",
|
||||
"MESH_PROVISION_PASSWORD_FILE": "/run/secrets/root",
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_RECEIVES": "/var/lib/mongodb/grants/mesh.json"
|
||||
"MESH_RECEIVES": "${dir:grants}/mesh.json"
|
||||
},
|
||||
"artifact": "runtime"
|
||||
}
|
||||
|
||||
@@ -68,7 +68,6 @@
|
||||
{
|
||||
"id": "data",
|
||||
"type": "directory",
|
||||
"path": "/services/mssql/data",
|
||||
"mode": "0700",
|
||||
"owner": "10001:0"
|
||||
},
|
||||
@@ -90,7 +89,7 @@
|
||||
"1433"
|
||||
],
|
||||
"volumes": [
|
||||
"/services/mssql/data:/var/opt/mssql"
|
||||
"${dir:data}:/var/opt/mssql"
|
||||
],
|
||||
"secrets-in-environment": "the image documents only MSSQL_SA_PASSWORD, no _FILE and no configuration field; not convertible without a wrapper entrypoint"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"module": "networkmanager",
|
||||
"version": "1",
|
||||
"capabilities": [
|
||||
"package-manager",
|
||||
"service-manager"
|
||||
],
|
||||
"claims": [
|
||||
{
|
||||
"name": "node-uplink",
|
||||
"scope": "node"
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"id": "package",
|
||||
"type": "package",
|
||||
"package": "networkmanager"
|
||||
},
|
||||
{
|
||||
"id": "config",
|
||||
"type": "file",
|
||||
"path": "/etc/NetworkManager/conf.d/50-mesh.conf",
|
||||
"mode": "0644",
|
||||
"content": "# Managed by the mesh (module networkmanager). Replaced on every push; edit the\n# catalogue instead.\n#\n# This machine's uplink is NetworkManager's, and this file is the whole of what\n# the mesh asks of it (novox/hq ADR 0117): leave the resolver file to the mesh,\n# and leave the private network's interface alone. Nothing more. The mesh never\n# declares a connection profile, an address, a route, a wireless network or its\n# credentials \u2014 those are joined at the machine, by the person using it, and\n# the link they make is the only channel the mesh reaches this machine over. A\n# push that got a link wrong could not be undone by the next one.\n#\n# A drop-in of the mesh's own, beside NetworkManager.conf and whatever else the\n# operator keeps in this directory. NetworkManager reads the files here sorted by\n# name and a later one wins a key it sets again \u2014 so a file of the operator's\n# that sorts after this one (any name starting with a letter does) and sets dns=\n# or unmanaged-devices= overrides it. That is the operator's to decide, and the\n# reason this file sets nothing but the two keys it must.\n#\n# NetworkManager itself is the machine's: the mesh never starts, stops, enables\n# or disables it (its service is declared with no state), because stopping it\n# takes every link down, this machine's channel to the mesh included \u2014 and a\n# module unassigned by mistake must not be able to do that. When this file\n# changes, a running NetworkManager is reloaded (its D-Bus Reload call, which\n# re-reads its configuration \u2014 NetworkManager(8)), never restarted.\n\n[main]\n# The resolver file is the mesh's: resolv-conf writes /etc/resolv.conf and names\n# the mesh's resolver. Without this line NetworkManager rewrites that file on\n# every connectivity change \u2014 every network joined, every lease renewed \u2014\n# and the mesh's resolver is silently replaced while every surface of the mesh\n# still reads green. none: \"NetworkManager will not modify resolv.conf. This\n# implies rc-manager unmanaged\" (NetworkManager.conf(5), 1.58). On an adopted\n# machine the predecessor wrote the same line in a file of its own; both say one\n# thing, and the predecessor's is retired by hand after the take.\ndns=none\n\n[keyfile]\n# mesh0 is the private network's interface: the mesh brings it up and the mesh\n# alone configures it. A manager that considers every interface its own could\n# try to configure it, or tear it down on a profile change.\n#\n# unmanaged-devices rather than a [device-mesh0] section with managed=0, because\n# NetworkManager.conf(5) says a device unmanaged by this key \"is strictly\n# unmanaged and cannot be overruled by using the API like nmcli device set\n# $IFNAME managed yes\", while device*.managed \"can be overruled at runtime via\n# D-Bus\". The same page adds that device*.managed \"may be a better choice\" for\n# exactly those reasons \u2014 for an interface the operator might want to hand back\n# at runtime. For the mesh's own interface, strict is the point.\n#\n# += rather than =: the same page documents appending to a list-valued key set\n# earlier (\"plugins+=another-plugin\") as an extension of its key file format,\n# and unmanaged-devices is a device list. = would replace whatever devices the\n# operator already keeps NetworkManager away from; += adds this one to them\n# (novox/hq ADR 0102: a list is added to, never replaced). A file of the\n# operator's read after this one that sets the key with = replaces it again;\n# that is the operator's to decide.\nunmanaged-devices+=interface-name:mesh0\n"
|
||||
},
|
||||
{
|
||||
"id": "service",
|
||||
"type": "service",
|
||||
"unit": "NetworkManager.service",
|
||||
"reload-on": [
|
||||
"config"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -11,29 +11,26 @@
|
||||
"postgres-database": {
|
||||
"name": "nextcloud"
|
||||
},
|
||||
"s3-bucket": {
|
||||
"bucket": "nextcloud"
|
||||
},
|
||||
"route": {
|
||||
"label": "drive",
|
||||
"port": 80
|
||||
}
|
||||
},
|
||||
"binds": {
|
||||
"postgres-database": "/var/lib/nextcloud-module/database.json",
|
||||
"s3-bucket": "/var/lib/nextcloud-module/store.json",
|
||||
"route": "/var/lib/nextcloud-module/route.json"
|
||||
"postgres-database": "${dir:state}/database.json",
|
||||
"s3-bucket": "${dir:state}/store.json",
|
||||
"route": "${dir:state}/route.json"
|
||||
},
|
||||
"secrets": {
|
||||
"postgres-database": "/var/lib/nextcloud-module/database.secret",
|
||||
"s3-bucket": "/var/lib/nextcloud-module/store.secret"
|
||||
"postgres-database": "${dir:state}/database.secret",
|
||||
"s3-bucket": "${dir:state}/store.secret"
|
||||
},
|
||||
"emits": [
|
||||
"module.nextcloud.user.created",
|
||||
"module.nextcloud.share.created"
|
||||
],
|
||||
"own-secrets": {
|
||||
"admin": "/var/lib/nextcloud-module/admin.secret",
|
||||
"admin": "${dir:state}/admin.secret",
|
||||
"broker": "/var/lib/mesh/nextcloud/broker"
|
||||
},
|
||||
"capabilities": [
|
||||
@@ -57,20 +54,19 @@
|
||||
{
|
||||
"id": "state",
|
||||
"type": "directory",
|
||||
"path": "/var/lib/nextcloud-module",
|
||||
"mode": "0700"
|
||||
"mode": "0700",
|
||||
"place": "."
|
||||
},
|
||||
{
|
||||
"id": "server-env",
|
||||
"type": "file",
|
||||
"path": "/var/lib/nextcloud-module/server.env",
|
||||
"path": "${dir:state}/server.env",
|
||||
"mode": "0600",
|
||||
"content": "POSTGRES_HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\nPOSTGRES_DB=${bound:postgres-database:as}\nPOSTGRES_USER=${bound:postgres-database:as}\nPOSTGRES_PASSWORD=${secret:postgres-database}\nNEXTCLOUD_ADMIN_USER=mesh-admin\nNEXTCLOUD_ADMIN_PASSWORD=${secret:admin}\nOBJECTSTORE_S3_HOST=${bound:s3-bucket:at}\nOBJECTSTORE_S3_PORT=${bound:s3-bucket:port}\nOBJECTSTORE_S3_BUCKET=nextcloud\nOBJECTSTORE_S3_KEY=${bound:s3-bucket:as}\nOBJECTSTORE_S3_SECRET=${secret:s3-bucket}\nOBJECTSTORE_S3_SSL=false\nOBJECTSTORE_S3_USEPATH_STYLE=true\nOBJECTSTORE_S3_REGION=${bound:s3-bucket:region}\n"
|
||||
"content": "POSTGRES_HOST=${bound:postgres-database:at}:${bound:postgres-database:port}\nPOSTGRES_DB=${bound:postgres-database:as}\nPOSTGRES_USER=${bound:postgres-database:as}\nPOSTGRES_PASSWORD=${secret:postgres-database}\nNEXTCLOUD_ADMIN_USER=mesh-admin\nNEXTCLOUD_ADMIN_PASSWORD=${secret:admin}\nOBJECTSTORE_S3_HOST=${bound:s3-bucket:at}\nOBJECTSTORE_S3_PORT=${bound:s3-bucket:port}\nOBJECTSTORE_S3_BUCKET=mesh-novox-ncloud\nOBJECTSTORE_S3_KEY=${bound:s3-bucket:as}\nOBJECTSTORE_S3_SECRET=${secret:s3-bucket}\nOBJECTSTORE_S3_SSL=false\nOBJECTSTORE_S3_USEPATH_STYLE=true\nOBJECTSTORE_S3_REGION=${bound:s3-bucket:region}\n"
|
||||
},
|
||||
{
|
||||
"id": "html",
|
||||
"type": "directory",
|
||||
"path": "/services/nextcloud/html",
|
||||
"mode": "0750",
|
||||
"owner": "33:33"
|
||||
},
|
||||
@@ -80,13 +76,13 @@
|
||||
"name": "nextcloud",
|
||||
"image": "nextcloud@sha256:fb966733647ea03f0446b0c22eac9733c8eb616d37b960caca9d4c3010e14a08",
|
||||
"env-file": [
|
||||
"/var/lib/nextcloud-module/server.env"
|
||||
"${dir:state}/server.env"
|
||||
],
|
||||
"ports": [
|
||||
"80"
|
||||
],
|
||||
"volumes": [
|
||||
"/services/nextcloud/html:/var/www/html"
|
||||
"${dir:html}:/var/www/html"
|
||||
],
|
||||
"secrets-in-environment": "the image honours POSTGRES_PASSWORD_FILE and NEXTCLOUD_ADMIN_PASSWORD_FILE (entrypoint file_env); OBJECTSTORE_S3_SECRET has none and needs a generated config fragment; convertible, awaiting a bed"
|
||||
},
|
||||
@@ -106,7 +102,7 @@
|
||||
"volumes": [
|
||||
"/var/lib/mesh/nextcloud/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/mesh/nextcloud/config.json:/run/config/config.json:ro",
|
||||
"/var/lib/nextcloud-module/admin.secret:/run/secrets/admin:ro",
|
||||
"${dir:state}/admin.secret:/run/secrets/admin:ro",
|
||||
"/var/run/docker.sock:/var/run/docker.sock"
|
||||
],
|
||||
"env": {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
],
|
||||
"claims": [
|
||||
{
|
||||
"name": "the-packet-filter",
|
||||
"name": "node-packet-filter",
|
||||
"scope": "node"
|
||||
}
|
||||
],
|
||||
@@ -30,7 +30,7 @@
|
||||
"id": "stock-unit-stop",
|
||||
"type": "file",
|
||||
"path": "/etc/systemd/system/nftables.service.d/mesh.conf",
|
||||
"content": "# The mesh: stopping the stock unit deletes only the mesh's table, never the whole ruleset\n# (novox/hq ADR 0100) — a flush would take the container runtime's rules and any firewall with it.\n[Service]\nExecStop=\nExecStop=nft delete table inet mesh\n",
|
||||
"content": "# The mesh: stopping the stock unit deletes only the mesh's table, never the whole ruleset\n# (novox/hq ADR 0100) \u2014 a flush would take the container runtime's rules and any firewall with it.\n[Service]\nExecStop=\nExecStop=nft delete table inet mesh\n",
|
||||
"mode": "0644"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
}
|
||||
},
|
||||
"binds": {
|
||||
"route": "/var/lib/only-office/route.json"
|
||||
"route": "${dir:state}/route.json"
|
||||
},
|
||||
"own-secrets": {
|
||||
"jwt": "/var/lib/only-office/jwt.secret"
|
||||
"jwt": "${dir:state}/jwt.secret"
|
||||
},
|
||||
"listens": [
|
||||
{
|
||||
@@ -32,56 +32,49 @@
|
||||
{
|
||||
"id": "state",
|
||||
"type": "directory",
|
||||
"path": "/var/lib/only-office",
|
||||
"mode": "0700"
|
||||
"mode": "0700",
|
||||
"place": "."
|
||||
},
|
||||
{
|
||||
"id": "server-env",
|
||||
"type": "file",
|
||||
"path": "/var/lib/only-office/server.env",
|
||||
"path": "${dir:state}/server.env",
|
||||
"mode": "0600",
|
||||
"content": "JWT_ENABLED=true\nJWT_SECRET=${secret:jwt}\nJWT_HEADER=Authorization\nJWT_IN_BODY=true\nALLOW_PRIVATE_IP_ADDRESS=true\n"
|
||||
},
|
||||
{
|
||||
"id": "logs",
|
||||
"type": "directory",
|
||||
"path": "/services/only-office/logs",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "data",
|
||||
"type": "directory",
|
||||
"path": "/services/only-office/data",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "lib",
|
||||
"type": "directory",
|
||||
"path": "/services/only-office/lib",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "db",
|
||||
"type": "directory",
|
||||
"path": "/services/only-office/db",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "rabbitmq",
|
||||
"type": "directory",
|
||||
"path": "/services/only-office/rabbitmq",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "redis",
|
||||
"type": "directory",
|
||||
"path": "/services/only-office/redis",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "fonts",
|
||||
"type": "directory",
|
||||
"path": "/services/only-office/fonts",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
@@ -96,19 +89,19 @@
|
||||
"image": "onlyoffice/documentserver@sha256:3ab6ebc7c605e5a32b7ae3ff19daed4925090245acc8100ce2230bd766c88212",
|
||||
"network": "only-office",
|
||||
"env-file": [
|
||||
"/var/lib/only-office/server.env"
|
||||
"${dir:state}/server.env"
|
||||
],
|
||||
"ports": [
|
||||
"80"
|
||||
"9070:80"
|
||||
],
|
||||
"volumes": [
|
||||
"/services/only-office/logs:/var/log/onlyoffice",
|
||||
"/services/only-office/data:/var/www/onlyoffice/Data",
|
||||
"/services/only-office/lib:/var/lib/onlyoffice",
|
||||
"/services/only-office/db:/var/lib/postgresql",
|
||||
"/services/only-office/rabbitmq:/var/lib/rabbitmq",
|
||||
"/services/only-office/redis:/var/lib/redis",
|
||||
"/services/only-office/fonts:/usr/share/fonts/truetype/custom"
|
||||
"${dir:logs}:/var/log/onlyoffice",
|
||||
"${dir:data}:/var/www/onlyoffice/Data",
|
||||
"${dir:lib}:/var/lib/onlyoffice",
|
||||
"${dir:db}:/var/lib/postgresql",
|
||||
"${dir:rabbitmq}:/var/lib/rabbitmq",
|
||||
"${dir:redis}:/var/lib/redis",
|
||||
"${dir:fonts}:/usr/share/fonts/truetype/custom"
|
||||
],
|
||||
"secrets-in-environment": "run-document-server.sh regenerates JWT_SECRET from the environment on every start and overwrites local.json; not convertible"
|
||||
}
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
"route"
|
||||
],
|
||||
"contributes": {
|
||||
"s3-bucket": {
|
||||
"bucket": "photos"
|
||||
},
|
||||
"mongodb-database": {
|
||||
"name": "photos"
|
||||
},
|
||||
@@ -56,7 +53,7 @@
|
||||
"type": "file",
|
||||
"path": "/var/lib/photos/server.env",
|
||||
"mode": "0600",
|
||||
"content": "NODE_ENV=production\nPORT=9000\nMONGO_URL=mongodb://${bound:mongodb-database:as}:${secret:mongodb-database}@${bound:mongodb-database:at}:${bound:mongodb-database:port}/${bound:mongodb-database:as}?authSource=admin\nMONGO_DB=${bound:mongodb-database:as}\nMINIO_ENDPOINT=${bound:s3-bucket:at}\nMINIO_PORT=${bound:s3-bucket:port}\nMINIO_BUCKET=photos\nMINIO_ACCESSKEY=${bound:s3-bucket:as}\nMINIO_SECRET=${secret:s3-bucket}\nMINIO_USE_SSL=false\n"
|
||||
"content": "NODE_ENV=production\nPORT=9000\nMONGO_URL=mongodb://${bound:mongodb-database:as}:${secret:mongodb-database}@${bound:mongodb-database:at}:${bound:mongodb-database:port}/${bound:mongodb-database:as}?authSource=admin\nMONGO_DB=${bound:mongodb-database:as}\nMINIO_ENDPOINT=${bound:s3-bucket:at}\nMINIO_PORT=${bound:s3-bucket:port}\nMINIO_BUCKET=mesh-novox-photos\nMINIO_ACCESSKEY=${bound:s3-bucket:as}\nMINIO_SECRET=${secret:s3-bucket}\nMINIO_USE_SSL=false\n"
|
||||
},
|
||||
{
|
||||
"id": "net",
|
||||
|
||||
@@ -6,11 +6,17 @@
|
||||
"container-runtime"
|
||||
],
|
||||
"listens": [
|
||||
{
|
||||
"port": 9090,
|
||||
"protocol": "tcp",
|
||||
"from": "mesh",
|
||||
"why": "the dashboard over http; portainer.novox.be is a route grant and the proxy reaches it here \u2014 the machine side of 9090:9000, the predecessor's number"
|
||||
},
|
||||
{
|
||||
"port": 9443,
|
||||
"protocol": "tcp",
|
||||
"from": "mesh",
|
||||
"why": "the container dashboard, over its own tls"
|
||||
"why": "the same dashboard over its own tls; the runtime sidecar talks to it here"
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
@@ -23,19 +29,19 @@
|
||||
{
|
||||
"id": "data",
|
||||
"type": "directory",
|
||||
"path": "/services/portainer/data",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "server",
|
||||
"type": "container",
|
||||
"name": "portainer",
|
||||
"image": "portainer/portainer-ce@sha256:511f3f06c96fe3b993ebeaafde311c1959cae73a7ef825dba6397d51b450dffa",
|
||||
"image": "portainer/portainer-ce@sha256:4d616db18cfeb5dd41a69c0958bc825c84483ea9cde1106eb82a5d26f3bd8b0e",
|
||||
"ports": [
|
||||
"9443"
|
||||
"9090:9000",
|
||||
"9443:9443"
|
||||
],
|
||||
"volumes": [
|
||||
"/services/portainer/data:/data",
|
||||
"${dir:data}:/data",
|
||||
"/var/run/docker.sock:/var/run/docker.sock"
|
||||
]
|
||||
},
|
||||
@@ -90,5 +96,17 @@
|
||||
"from": "Dockerfile"
|
||||
}
|
||||
]
|
||||
},
|
||||
"requires": [
|
||||
"route"
|
||||
],
|
||||
"contributes": {
|
||||
"route": {
|
||||
"label": "portainer",
|
||||
"port": 9090
|
||||
}
|
||||
},
|
||||
"binds": {
|
||||
"route": "/var/lib/mesh/portainer/route.json"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,8 @@
|
||||
"id": "store-data",
|
||||
"type": "directory",
|
||||
"path": "/var/lib/mesh-store",
|
||||
"mode": "0700"
|
||||
"mode": "0700",
|
||||
"owner": "999:70"
|
||||
},
|
||||
{
|
||||
"id": "server",
|
||||
|
||||
@@ -2,12 +2,22 @@
|
||||
"module": "resolv-conf",
|
||||
"version": "1",
|
||||
"slug": "resolv",
|
||||
|
||||
"requires": ["wildcard-resolution"],
|
||||
"claims": [{"name": "the-resolver-configuration", "scope": "node"}],
|
||||
|
||||
"requires": [
|
||||
"wildcard-resolution"
|
||||
],
|
||||
"claims": [
|
||||
{
|
||||
"name": "node-resolver-config",
|
||||
"scope": "node"
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{"id": "resolv", "type": "file", "path": "/etc/resolv.conf", "mode": "0644",
|
||||
"content": "# Managed by the mesh.\n#\n# For a machine where nothing else owns this file. On one where systemd-resolved\n# or NetworkManager does, assign that module instead — this one and those claim\n# the same thing, so the mesh refuses the pair rather than letting them take\n# turns overwriting each other, which is the failure this claim exists to stop.\n#\n# The mesh's resolver, and only it — the one line the predecessor wrote on every\n# machine it set up. It answers the mesh's names itself and forwards everything\n# else to upstreams named in its own configuration, never read from this file.\n# This file used to carry a second nameserver as a placeholder for \"whatever\n# this machine used before\"; that was never a fallback for names the mesh does\n# not know — a resolver's second line is asked only when the first does not\n# answer at all — and now that the first answers everything it would be a line\n# nothing ever reached.\nnameserver 127.0.0.1\noptions edns0\n"}
|
||||
{
|
||||
"id": "resolv",
|
||||
"type": "file",
|
||||
"path": "/etc/resolv.conf",
|
||||
"mode": "0644",
|
||||
"content": "# Managed by the mesh.\n#\n# For a machine where nothing else owns this file. On one where systemd-resolved\n# or NetworkManager does, assign that module instead \u2014 this one and those claim\n# the same thing, so the mesh refuses the pair rather than letting them take\n# turns overwriting each other, which is the failure this claim exists to stop.\n#\n# The mesh's resolver, and only it \u2014 the one line the predecessor wrote on every\n# machine it set up. It answers the mesh's names itself and forwards everything\n# else to upstreams named in its own configuration, never read from this file.\n# This file used to carry a second nameserver as a placeholder for \"whatever\n# this machine used before\"; that was never a fallback for names the mesh does\n# not know \u2014 a resolver's second line is asked only when the first does not\n# answer at all \u2014 and now that the first answers everything it would be a line\n# nothing ever reached.\nnameserver 127.0.0.1\noptions edns0\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,18 +2,38 @@
|
||||
"module": "resolved-split-dns",
|
||||
"version": "1",
|
||||
"slug": "splitdns",
|
||||
|
||||
"requires": ["wildcard-resolution"],
|
||||
"claims": [{"name": "the-resolver-configuration", "scope": "node"}],
|
||||
|
||||
"requires": [
|
||||
"wildcard-resolution"
|
||||
],
|
||||
"claims": [
|
||||
{
|
||||
"name": "node-resolver-config",
|
||||
"scope": "node"
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{"id": "drop-in", "type": "directory", "path": "/etc/systemd/resolved.conf.d", "mode": "0755"},
|
||||
|
||||
{"id": "route", "type": "file",
|
||||
"path": "/etc/systemd/resolved.conf.d/mesh.conf", "mode": "0644",
|
||||
"content": "# Managed by the mesh.\n#\n# **Only the mesh's names.** The tilde makes this a routing domain rather than a\n# search domain: queries under it go to the resolver below, and everything else\n# keeps going wherever this machine already sent it. A resolver that took over\n# all of DNS would be this module claiming the machine's whole network, which\n# is not what it says it claims. The mesh's resolver can forward the rest too;\n# this module is for a machine that wants systemd-resolved to stay in charge of\n# that, and only lends it the mesh's suffix.\n#\n# 127.0.0.1 is where the mesh's resolver answers on every machine — a fixed\n# address, so this file needs to know nothing about this particular machine.\n# systemd-resolved holds .53 and .54 itself, which is why the resolver is on\n# neither, and why the two coexist here.\n[Resolve]\nDNS=127.0.0.1\nDomains=~internal\n"},
|
||||
|
||||
{"id": "resolved", "type": "service", "unit": "systemd-resolved.service",
|
||||
"state": "running", "boot": "enabled", "restart-on": ["route"]}
|
||||
{
|
||||
"id": "drop-in",
|
||||
"type": "directory",
|
||||
"path": "/etc/systemd/resolved.conf.d",
|
||||
"mode": "0755"
|
||||
},
|
||||
{
|
||||
"id": "route",
|
||||
"type": "file",
|
||||
"path": "/etc/systemd/resolved.conf.d/mesh.conf",
|
||||
"mode": "0644",
|
||||
"content": "# Managed by the mesh.\n#\n# **Only the mesh's names.** The tilde makes this a routing domain rather than a\n# search domain: queries under it go to the resolver below, and everything else\n# keeps going wherever this machine already sent it. A resolver that took over\n# all of DNS would be this module claiming the machine's whole network, which\n# is not what it says it claims. The mesh's resolver can forward the rest too;\n# this module is for a machine that wants systemd-resolved to stay in charge of\n# that, and only lends it the mesh's suffix.\n#\n# 127.0.0.1 is where the mesh's resolver answers on every machine \u2014 a fixed\n# address, so this file needs to know nothing about this particular machine.\n# systemd-resolved holds .53 and .54 itself, which is why the resolver is on\n# neither, and why the two coexist here.\n[Resolve]\nDNS=127.0.0.1\nDomains=~internal\n"
|
||||
},
|
||||
{
|
||||
"id": "resolved",
|
||||
"type": "service",
|
||||
"unit": "systemd-resolved.service",
|
||||
"state": "running",
|
||||
"boot": "enabled",
|
||||
"restart-on": [
|
||||
"route"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -159,3 +159,15 @@ cd modules/route-adapter && npm test
|
||||
They hold it to what ADR 0104 says holds it: one file per contribution, a file removed when its
|
||||
contribution goes, every file it did not write left alone — and the two facts a route file has to
|
||||
get right, the port the contributor publishes and the address of the machine it is on.
|
||||
|
||||
## A body limit
|
||||
|
||||
A contribution may say `max-request-body`, in bytes, and the adapter writes it as the predecessor's
|
||||
own `buffering` middleware, named after the router so the two halves cannot drift. A route that says
|
||||
nothing gets no middleware and the predecessor's default stands.
|
||||
|
||||
This is the one thing the file shape *can* say that a policy cannot, which is why it is written
|
||||
rather than skipped: the predecessor already served its own registry name this way. A limit that is
|
||||
not a whole positive number of bytes takes the route with it — written without the limit, the
|
||||
predecessor would carry exactly what the module said not to carry, and this module would report
|
||||
success doing it.
|
||||
|
||||
@@ -75,6 +75,15 @@ export interface Route {
|
||||
from: string;
|
||||
/** Where the predecessor's proxy is to send it. */
|
||||
target: string;
|
||||
/**
|
||||
* The largest request body, in bytes, the predecessor may carry to it — the contribution's
|
||||
* `max-request-body`. Absent is whatever the predecessor does by default.
|
||||
*
|
||||
* Unlike a policy, this file shape *can* say it: the predecessor has a buffering middleware, and
|
||||
* its own registry route used exactly this. A registry takes image layers in single requests of
|
||||
* gigabytes, so a route that could not say it would be a name nothing could be pushed to.
|
||||
*/
|
||||
maxRequestBody?: number;
|
||||
}
|
||||
|
||||
/** What one pass changed. */
|
||||
@@ -176,12 +185,40 @@ export function routesFrom(document: unknown, machine: string): { routes: Route[
|
||||
}
|
||||
// Where the mesh says that machine is. Empty means this one, and this one is reached from
|
||||
// inside the predecessor's container by the machine's own name, not by loopback.
|
||||
// A limit it cannot honour is a route it does not write — skipped and named, like a port that
|
||||
// is not one. Written without the limit instead, the predecessor would carry exactly what the
|
||||
// module said not to carry, and this adapter would report success.
|
||||
const askedLimit = entry.values?.["max-request-body"];
|
||||
const limit = asBodyLimit(askedLimit);
|
||||
if (limit === null) {
|
||||
skipped.push(
|
||||
`${from} asked for route ${name} with a max-request-body of ${JSON.stringify(askedLimit)}, ` +
|
||||
`which is not a whole positive number of bytes`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const at = typeof entry.at === "string" && entry.at.trim() !== "" ? entry.at.trim() : machine;
|
||||
routes.push({ name, from, target: `http://${at}:${port}` });
|
||||
routes.push({ name, from, target: `http://${at}:${port}`, ...(limit === undefined ? {} : { maxRequestBody: limit }) });
|
||||
}
|
||||
return { routes, skipped };
|
||||
}
|
||||
|
||||
/**
|
||||
* The body limit a contribution asked for: a number, `undefined` for silence, `null` for unusable.
|
||||
*
|
||||
* Three answers rather than two, because "said nothing" and "said something wrong" must not become
|
||||
* the same route.
|
||||
*/
|
||||
function asBodyLimit(value: unknown): number | undefined | null {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** The file one route is written to. The prefix is how the mesh recognises its own. */
|
||||
export function fileNameFor(name: string): string {
|
||||
return `mesh-${name}.yml`;
|
||||
@@ -201,6 +238,10 @@ export function routerNameFor(name: string): string {
|
||||
*/
|
||||
export function routeFile(route: Route, settings: Settings): string {
|
||||
const id = routerNameFor(route.name);
|
||||
// The body limit is a middleware in the predecessor's vocabulary — its `buffering`, with the one
|
||||
// field the predecessor's own registry route set — named after the router so the two halves cannot
|
||||
// drift, and written only when the contribution asked for it.
|
||||
const limited = route.maxRequestBody !== undefined;
|
||||
return [
|
||||
marker,
|
||||
`# ${route.from} contributed this route. It is removed when that contribution goes.`,
|
||||
@@ -210,10 +251,19 @@ export function routeFile(route: Route, settings: Settings): string {
|
||||
` entryPoints: [${settings.entrypoint}]`,
|
||||
` rule: Host(\`${route.name}\`)`,
|
||||
` service: ${id}`,
|
||||
...(limited ? [` middlewares: [${id}-body]`] : []),
|
||||
" tls:",
|
||||
` certResolver: ${settings.resolver}`,
|
||||
" domains:",
|
||||
` - main: ${route.name}`,
|
||||
...(limited
|
||||
? [
|
||||
" middlewares:",
|
||||
` ${id}-body:`,
|
||||
" buffering:",
|
||||
` maxRequestBodyBytes: ${route.maxRequestBody}`,
|
||||
]
|
||||
: []),
|
||||
" services:",
|
||||
` ${id}:`,
|
||||
" loadBalancer:",
|
||||
|
||||
@@ -22,7 +22,9 @@ async function predecessor(already: Record<string, string> = {}): Promise<Settin
|
||||
}
|
||||
|
||||
/** The contributions file the mesh writes, in the shape the mesh's own proxy also reads. */
|
||||
function contributed(...given: { from: string; node?: string; at?: string; name: string; port: number }[]) {
|
||||
function contributed(
|
||||
...given: { from: string; node?: string; at?: string; name: string; port: number; limit?: unknown }[]
|
||||
) {
|
||||
return {
|
||||
contributions: 1,
|
||||
requirement: "route",
|
||||
@@ -30,7 +32,7 @@ function contributed(...given: { from: string; node?: string; at?: string; name:
|
||||
from: g.from,
|
||||
node: g.node ?? "control-node",
|
||||
at: g.at ?? "",
|
||||
values: { name: g.name, port: g.port },
|
||||
values: { name: g.name, port: g.port, ...(g.limit === undefined ? {} : { "max-request-body": g.limit }) },
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -232,3 +234,41 @@ test("it refuses when the predecessor's directory is not there, and says why", a
|
||||
const settings = { ...defaults, dynamic: join(await mkdtemp(join(tmpdir(), "route-adapter-")), "absent") };
|
||||
await assert.rejects(reconcile([], settings), /is not there.*`dynamic` setting.*mounts it/s);
|
||||
});
|
||||
|
||||
// **A registry is why a route needs to say this.** Image layers arrive as single requests of
|
||||
// gigabytes, and the predecessor served its own registry name with a `buffering` middleware for
|
||||
// exactly that reason. The contribution carries the limit as `max-request-body`, the adapter writes
|
||||
// the middleware the predecessor already understands, named after the router so the two halves
|
||||
// cannot drift — and writes nothing of the kind for a route that did not ask.
|
||||
test("a body limit is written as the predecessor's buffering middleware", async () => {
|
||||
const settings = await predecessor();
|
||||
const changed = await pass(settings, contributed(
|
||||
{ from: "registry", name: "images.example", port: 5001, limit: 21474836480 },
|
||||
{ from: "forge", name: "git.example", port: 2999 },
|
||||
));
|
||||
assert.deepEqual(changed.written, ["mesh-git.example.yml", "mesh-images.example.yml"]);
|
||||
|
||||
const written = await readFile(join(settings.dynamic, "mesh-images.example.yml"), "utf8");
|
||||
assert.match(written, /^ {6}middlewares: \[mesh-images-example-body\]$/m);
|
||||
assert.match(written, /^ {2}middlewares:\n {4}mesh-images-example-body:\n {6}buffering:\n {8}maxRequestBodyBytes: 21474836480$/m);
|
||||
|
||||
// The route that asked for nothing carries no middleware — the predecessor's default stands.
|
||||
const plain = await readFile(join(settings.dynamic, "mesh-git.example.yml"), "utf8");
|
||||
assert.doesNotMatch(plain, /middlewares|buffering/);
|
||||
});
|
||||
|
||||
// A limit it cannot honour is a route it does not write. Written without it, the predecessor would
|
||||
// carry exactly what the module said not to carry, and this module would report success.
|
||||
test("a body limit that is not a whole number of bytes is skipped and named", () => {
|
||||
for (const limit of ["20g", 0, -1, 1.5, true, null]) {
|
||||
const { routes, skipped } = routesFrom(
|
||||
contributed({ from: "registry", name: "images.example", port: 5001, limit }), defaults.machine);
|
||||
assert.deepEqual(routes, [], `a limit of ${JSON.stringify(limit)} was served`);
|
||||
assert.equal(skipped.length, 1);
|
||||
assert.match(skipped[0]!, /max-request-body/);
|
||||
}
|
||||
// And a limit the mesh's own proxy would accept is carried through, as a number.
|
||||
const { routes } = routesFrom(
|
||||
contributed({ from: "registry", name: "images.example", port: 5001, limit: 1024 }), defaults.machine);
|
||||
assert.equal(routes[0]?.maxRequestBody, 1024);
|
||||
});
|
||||
|
||||
+182
-59
@@ -2,72 +2,195 @@
|
||||
"module": "showcase",
|
||||
"version": "1",
|
||||
"slug": "show",
|
||||
|
||||
"capabilities": ["container-runtime"],
|
||||
|
||||
"provides": [{ "name": "greeting", "scope": "mesh" }],
|
||||
"serves": { "greeting": { "path": "/greeting" } },
|
||||
"requires": ["postgres-database"],
|
||||
"binds": { "postgres-database": "/var/lib/showcase/database.json" },
|
||||
"secrets": { "postgres-database": "/var/lib/showcase/database.secret" },
|
||||
"own-secrets": { "broker": "/var/lib/mesh/showcase/broker" },
|
||||
|
||||
"claims": [{ "name": "the-showcase", "scope": "node" }],
|
||||
|
||||
"emits": ["module.showcase.acknowledged"],
|
||||
"consumes": ["module.showcase.greeted"],
|
||||
|
||||
"capabilities": [
|
||||
"container-runtime"
|
||||
],
|
||||
"provides": [
|
||||
{
|
||||
"name": "greeting",
|
||||
"scope": "mesh"
|
||||
}
|
||||
],
|
||||
"serves": {
|
||||
"greeting": {
|
||||
"path": "/greeting"
|
||||
}
|
||||
},
|
||||
"requires": [
|
||||
"postgres-database"
|
||||
],
|
||||
"binds": {
|
||||
"postgres-database": "/var/lib/showcase/database.json"
|
||||
},
|
||||
"secrets": {
|
||||
"postgres-database": "/var/lib/showcase/database.secret"
|
||||
},
|
||||
"own-secrets": {
|
||||
"broker": "/var/lib/mesh/showcase/broker"
|
||||
},
|
||||
"claims": [
|
||||
{
|
||||
"name": "the-showcase",
|
||||
"scope": "node"
|
||||
}
|
||||
],
|
||||
"emits": [
|
||||
"module.showcase.acknowledged"
|
||||
],
|
||||
"consumes": [
|
||||
"module.showcase.greeted"
|
||||
],
|
||||
"listens": [
|
||||
{ "port": 8080, "protocol": "tcp", "from": "mesh",
|
||||
"why": "the port the daemon itself listens on. The mesh assigns the machine-side number and tells consumers that one (ADR 0038)" }
|
||||
{
|
||||
"port": 8080,
|
||||
"protocol": "tcp",
|
||||
"from": "mesh",
|
||||
"why": "the port the daemon itself listens on. The mesh assigns the machine-side number and tells consumers that one (ADR 0038)"
|
||||
}
|
||||
],
|
||||
|
||||
"build": {
|
||||
"artifacts": [
|
||||
{ "name": "code", "kind": "bundle", "language": "typescript",
|
||||
"entrypoints": ["index.js", "tools/index.js", "provisioner/index.js",
|
||||
"daemon/index.js", "step/index.js", "report/index.js"] },
|
||||
{ "name": "files", "kind": "archive", "from": "files" },
|
||||
{ "name": "helper", "kind": "upstream",
|
||||
"from": "alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b" }
|
||||
{
|
||||
"name": "code",
|
||||
"kind": "bundle",
|
||||
"language": "typescript",
|
||||
"entrypoints": [
|
||||
"index.js",
|
||||
"tools/index.js",
|
||||
"provisioner/index.js",
|
||||
"daemon/index.js",
|
||||
"step/index.js",
|
||||
"report/index.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "files",
|
||||
"kind": "archive",
|
||||
"from": "files"
|
||||
},
|
||||
{
|
||||
"name": "helper",
|
||||
"kind": "upstream",
|
||||
"from": "alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"resources": [
|
||||
{ "id": "account", "type": "user", "name": "showcase", "shell": "/usr/bin/nologin",
|
||||
"home": "/var/lib/showcase" },
|
||||
|
||||
{ "id": "logs", "type": "access", "path": "/var/log", "mode": "0755" },
|
||||
|
||||
{ "id": "mesh-state", "type": "directory", "path": "/var/lib/mesh/showcase", "mode": "0700" },
|
||||
{ "id": "state", "type": "directory", "path": "/var/lib/showcase", "mode": "0755" },
|
||||
|
||||
{ "id": "settings", "type": "file", "path": "/var/lib/showcase/showcase.env", "mode": "0600",
|
||||
"content": "SHOWCASE_GREETING=hello\nSHOWCASE_EVERY_SECONDS=30\nSHOWCASE_STATE=/var/lib/showcase\nSHOWCASE_DATABASE=${bound:postgres-database:at}\nSHOWCASE_LISTEN=${port:8080}\n" },
|
||||
|
||||
{ "id": "packed", "type": "archive", "path": "/opt/showcase", "artifact": "files" },
|
||||
|
||||
{ "id": "net", "type": "network", "name": "showcase" },
|
||||
|
||||
{ "id": "tooling", "type": "package", "package": "jq" },
|
||||
|
||||
{ "id": "migrate", "type": "process", "name": "showcase-migrate", "artifact": "code",
|
||||
"run": ["node", "step/index.js"], "run-once": true,
|
||||
"env-file": ["/var/lib/showcase/showcase.env"] },
|
||||
|
||||
{ "id": "server", "type": "process", "name": "showcase", "artifact": "code",
|
||||
"run": ["node", "daemon/index.js"], "user": "showcase",
|
||||
"env-file": ["/var/lib/showcase/showcase.env"],
|
||||
"restart-on": ["settings"] },
|
||||
|
||||
{ "id": "reporting", "type": "process", "name": "showcase-report", "artifact": "code",
|
||||
"run": ["node", "report/index.js"], "schedule": "0 3 * * *",
|
||||
"env-file": ["/var/lib/showcase/showcase.env"] },
|
||||
|
||||
{ "id": "tools", "type": "container", "name": "mesh-showcase", "artifact": "helper",
|
||||
{
|
||||
"id": "account",
|
||||
"type": "user",
|
||||
"name": "showcase",
|
||||
"shell": "/usr/bin/nologin",
|
||||
"home": "/var/lib/showcase"
|
||||
},
|
||||
{
|
||||
"id": "logs",
|
||||
"type": "access",
|
||||
"path": "/var/log",
|
||||
"mode": "0755"
|
||||
},
|
||||
{
|
||||
"id": "mesh-state",
|
||||
"type": "directory",
|
||||
"path": "/var/lib/mesh/showcase",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "state",
|
||||
"type": "directory",
|
||||
"path": "/var/lib/showcase",
|
||||
"mode": "0755"
|
||||
},
|
||||
{
|
||||
"id": "settings",
|
||||
"type": "file",
|
||||
"path": "/var/lib/showcase/showcase.env",
|
||||
"mode": "0600",
|
||||
"content": "SHOWCASE_GREETING=hello\nSHOWCASE_EVERY_SECONDS=30\nSHOWCASE_STATE=/var/lib/showcase\nSHOWCASE_DATABASE=${bound:postgres-database:at}\nSHOWCASE_LISTEN=${port:8080}\n"
|
||||
},
|
||||
{
|
||||
"id": "packed",
|
||||
"type": "archive",
|
||||
"path": "/opt/showcase",
|
||||
"artifact": "files"
|
||||
},
|
||||
{
|
||||
"id": "net",
|
||||
"type": "network",
|
||||
"name": "showcase"
|
||||
},
|
||||
{
|
||||
"id": "tooling",
|
||||
"type": "package",
|
||||
"package": "jq"
|
||||
},
|
||||
{
|
||||
"id": "migrate",
|
||||
"type": "process",
|
||||
"name": "showcase-migrate",
|
||||
"artifact": "code",
|
||||
"run": [
|
||||
"node",
|
||||
"step/index.js"
|
||||
],
|
||||
"run-once": true,
|
||||
"env-file": [
|
||||
"/var/lib/showcase/showcase.env"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "server",
|
||||
"type": "process",
|
||||
"name": "showcase",
|
||||
"artifact": "code",
|
||||
"run": [
|
||||
"node",
|
||||
"daemon/index.js"
|
||||
],
|
||||
"user": "showcase",
|
||||
"env-file": [
|
||||
"/var/lib/showcase/showcase.env"
|
||||
],
|
||||
"restart-on": [
|
||||
"settings"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "reporting",
|
||||
"type": "process",
|
||||
"name": "showcase-report",
|
||||
"artifact": "code",
|
||||
"run": [
|
||||
"node",
|
||||
"report/index.js"
|
||||
],
|
||||
"schedule": "0 3 * * *",
|
||||
"env-file": [
|
||||
"/var/lib/showcase/showcase.env"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "tools",
|
||||
"type": "container",
|
||||
"name": "mesh-showcase",
|
||||
"artifact": "helper",
|
||||
"network": "showcase",
|
||||
"volumes": ["/var/lib/mesh/showcase/broker:/run/secrets/broker:ro"],
|
||||
"env": { "MESH_BROKER_FILE": "/run/secrets/broker" },
|
||||
"args": ["sleep", "infinity"] }
|
||||
"volumes": [
|
||||
"/var/lib/mesh/showcase/broker:/run/secrets/broker:ro"
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker"
|
||||
},
|
||||
"args": [
|
||||
"sleep",
|
||||
"infinity"
|
||||
]
|
||||
}
|
||||
],
|
||||
"seats": [
|
||||
{
|
||||
"name": "the-showcase",
|
||||
"scope": "node"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"module": "ssh-client",
|
||||
"version": "1",
|
||||
"resources": [
|
||||
{
|
||||
"id": "openssh",
|
||||
"type": "package",
|
||||
"package": "openssh"
|
||||
},
|
||||
{
|
||||
"id": "ssh-dir",
|
||||
"type": "directory",
|
||||
"path": "${machine:account-home}/.ssh",
|
||||
"owner": "${machine:account}",
|
||||
"mode": "0700"
|
||||
}
|
||||
],
|
||||
"facts": {
|
||||
"ssh-config": {
|
||||
"path": ".ssh/config",
|
||||
"home": true,
|
||||
"shared": true,
|
||||
"template": "# The mesh's Host blocks — every other node, so `ssh <node>` reaches it as the\n# right account. This region is replaced whenever a node joins, leaves or is\n# renamed; the rest of this file is yours and is kept untouched.\n{{range .Machines}}{{if ne .Name $.Node}}\nHost {{.Name}} {{.FQDN}}\n HostName {{.FQDN}}\n{{if .Account}} User {{.Account}}\n{{end}}{{end}}{{end}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"module": "sshd",
|
||||
"version": "1",
|
||||
"capabilities": [
|
||||
"package-manager",
|
||||
"service-manager"
|
||||
],
|
||||
"listens": [
|
||||
{
|
||||
"port": 22,
|
||||
"protocol": "tcp",
|
||||
"from": "anywhere",
|
||||
"why": "the operator's own door. From anywhere because the machines that need it are exactly the ones not on the mesh yet \u2014 and locking the operator out is the one failure a firewall must never arrange"
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"id": "package",
|
||||
"type": "package",
|
||||
"package": "openssh"
|
||||
},
|
||||
{
|
||||
"id": "config",
|
||||
"type": "file",
|
||||
"path": "/etc/ssh/sshd_config.d/10-mesh.conf",
|
||||
"mode": "0644",
|
||||
"content": "# Managed by the mesh (module sshd). Replaced on every push; edit the catalogue instead.\nPort 22\nPermitRootLogin no\nPasswordAuthentication no\nPubkeyAuthentication yes\nKbdInteractiveAuthentication no\nUsePAM yes\nX11Forwarding no\nPrintMotd no\nAcceptEnv LANG LC_*\n"
|
||||
},
|
||||
{
|
||||
"id": "run",
|
||||
"type": "service",
|
||||
"unit": "sshd.service",
|
||||
"state": "running",
|
||||
"restart-on": [
|
||||
"config"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"module": "systemd-networkd",
|
||||
"version": "1",
|
||||
"capabilities": [
|
||||
"package-manager",
|
||||
"service-manager"
|
||||
],
|
||||
"claims": [
|
||||
{
|
||||
"name": "node-uplink",
|
||||
"scope": "node"
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"id": "package",
|
||||
"type": "package",
|
||||
"package": "systemd"
|
||||
},
|
||||
{
|
||||
"id": "config",
|
||||
"type": "file",
|
||||
"path": "/etc/systemd/network/00-mesh0.network",
|
||||
"mode": "0644",
|
||||
"content": "# Managed by the mesh (module systemd-networkd). Replaced on every push; edit\n# the catalogue instead.\n#\n# This machine's uplink is systemd-networkd's, and the mesh asks one thing of it\n# here (novox/hq ADR 0117): leave the private network's interface alone. mesh0\n# is the mesh's; the mesh brings it up and configures it itself. The mesh never\n# declares a link, an address, a route, a wireless network or its credentials,\n# nor a network file for any of this machine's own interfaces \u2014 those are\n# the operator's, and the link they make is the only channel the mesh reaches\n# this machine over.\n#\n# 00-: networkd applies the first .network file, in alphanumeric order across\n# every directory, that matches an interface, and ignores every later one even\n# if it matches too (systemd.network(5), [Match]). A catch-all of the operator's\n# \u2014 Name=*, Type=ether, a file with no [Match] at all \u2014 sorted before\n# this one would claim mesh0 first. 00 sorts before every numbered prefix the\n# man page recommends.\n#\n# Unmanaged=yes: \"no attempts are made to bring up or configure matching links,\n# equivalent to when there are no matching network files\" (systemd.network(5),\n# [Link], since 233). A match that ends the search, and does nothing else.\n#\n# No DNS setting, because none is needed: networkd never writes\n# /etc/resolv.conf. What it learns from a lease it hands only to\n# systemd-resolved, and the resolver file stays whatever resolv-conf wrote.\n# Whether resolved runs, and what it does with that, is the resolver\n# configuration's question, not the uplink's.\n#\n# The service is reloaded when this file changes, never restarted: a restart\n# drops the links networkd holds, this machine's channel to the mesh among them.\n[Match]\nName=mesh0\n\n[Link]\nUnmanaged=yes\n"
|
||||
},
|
||||
{
|
||||
"id": "service",
|
||||
"type": "service",
|
||||
"unit": "systemd-networkd.service",
|
||||
"reload-on": [
|
||||
"config"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
# verdaccio's runtime: the tool runtime, carrying this module's compiled code.
|
||||
#
|
||||
# **Built from this module's own directory and nothing else.** The sdk and the tool runtime are in
|
||||
# the base images, published like any other artifact — which is what makes this buildable by the
|
||||
# mesh from a repository and a path (novox/hq ADR 0069) rather than only on a workstation that
|
||||
# happens to have the siblings.
|
||||
#
|
||||
# Two bases, named rather than pinned (novox/hq issue 044): the image this is COMPILED in and the
|
||||
# image it RUNS in — the second must not carry a compiler. Declared in module.json's `build.on`.
|
||||
ARG BUILD_BASE
|
||||
ARG RUNTIME_BASE
|
||||
|
||||
FROM ${BUILD_BASE} AS build
|
||||
# Compiled under /app/modules so `@novox/mesh-sdk` resolves upward into the base's own
|
||||
# node_modules — the module is compiled against exactly the sdk it will run against. The compiler
|
||||
# is invoked by its real path: node_modules/.bin entries are launcher symlinks the base image
|
||||
# resolved away.
|
||||
WORKDIR /app/modules/verdaccio
|
||||
COPY . .
|
||||
RUN node /app/node_modules/typescript/bin/tsc client.ts index.ts tools/index.ts \
|
||||
--module NodeNext --moduleResolution NodeNext --target ES2022 --outDir dist
|
||||
|
||||
FROM ${RUNTIME_BASE}
|
||||
COPY --from=build /app/modules/verdaccio/dist /app/modules/verdaccio/dist
|
||||
# Every serve-time entrypoint, loaded by the runtime in serve mode: tools and events serve, and a
|
||||
# provider's provisioner runs its reconcile loop in the same process, with the broker connected —
|
||||
# the convention novox/hq issues 060/061 settled. A container that instead ran only its
|
||||
# provisioner (`run`) served no tools and emitted no events; a container that named no command
|
||||
# ran no provisioner at all.
|
||||
ENV MESH_TOOL_MODULES=/app/modules/verdaccio/dist/index.js,/app/modules/verdaccio/dist/tools/index.js
|
||||
@@ -1,91 +0,0 @@
|
||||
// The Verdaccio (npm registry) client — verdaccio's own code, living in the module (novox/hq
|
||||
// ADR 0039). Both this module's tools and its events entrypoint import it, and nothing outside
|
||||
// verdaccio does.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export interface VerdaccioPackage {
|
||||
name: string;
|
||||
version?: string;
|
||||
description?: string;
|
||||
time?: string;
|
||||
}
|
||||
|
||||
export interface PackageInfo {
|
||||
name: string;
|
||||
latest?: string;
|
||||
versions: string[];
|
||||
description?: string;
|
||||
modified?: string;
|
||||
}
|
||||
|
||||
/** The settings-merged config the mesh delivers (novox/hq ADR 0046): { url, apiKey, token, password, user, ... }. */
|
||||
function meshConfig(file?: string): Record<string, string> {
|
||||
if (!file) return {};
|
||||
try { return JSON.parse(readFileSync(file, "utf8")) as Record<string, string>; }
|
||||
catch { return {}; }
|
||||
}
|
||||
|
||||
export class VerdaccioClient {
|
||||
readonly baseUrl: string;
|
||||
|
||||
// A bearer token is optional: package listing and reading are public on most registries, so the
|
||||
// token is sent only when configured, for a registry that gates reads behind auth.
|
||||
constructor(
|
||||
url: string,
|
||||
private readonly token?: string,
|
||||
) {
|
||||
this.baseUrl = url.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build from the module's resolved environment. The URL is MESH_VERDACCIO_URL (or the local
|
||||
* port); an optional MESH_VERDACCIO_TOKEN authenticates. Throws when no URL is configured.
|
||||
*/
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): VerdaccioClient {
|
||||
const cfg = meshConfig(env.MESH_VERDACCIO_CONFIG_FILE);
|
||||
const url = cfg.url ?? (env.MESH_VERDACCIO_URL ?? `http://127.0.0.1:${env.VERDACCIO_PORT ?? "4873"}`);
|
||||
if (!url) throw new Error("no verdaccio URL — set MESH_VERDACCIO_URL");
|
||||
return new VerdaccioClient(url, cfg.token ?? env.MESH_VERDACCIO_TOKEN);
|
||||
}
|
||||
|
||||
private async getJson<T>(path: string): Promise<T> {
|
||||
const res = await fetch(`${this.baseUrl}${path}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),
|
||||
},
|
||||
});
|
||||
if (!res.ok) throw new Error(`Verdaccio ${path}: ${res.status} ${await res.text()}`);
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every package the registry hosts, from Verdaccio's own web API — the same list its UI shows.
|
||||
* Each entry carries the latest version and the time it was last published.
|
||||
*/
|
||||
async listPackages(): Promise<VerdaccioPackage[]> {
|
||||
const raw = await this.getJson<any[]>("/-/verdaccio/data/packages");
|
||||
return (raw ?? []).map((p) => ({
|
||||
name: p.name,
|
||||
version: p.version ?? p["dist-tags"]?.latest,
|
||||
description: p.description,
|
||||
time: p.time?.modified ?? p.time,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* The full detail of one package — its dist-tags, every published version, and timestamps —
|
||||
* from the standard npm packument endpoint (`GET /<name>`).
|
||||
*/
|
||||
async getPackageInfo(name: string): Promise<PackageInfo> {
|
||||
const doc = await this.getJson<any>(`/${encodeURIComponent(name).replace(/%2F/g, "/")}`);
|
||||
return {
|
||||
name: doc.name ?? name,
|
||||
latest: doc["dist-tags"]?.latest,
|
||||
versions: Object.keys(doc.versions ?? {}),
|
||||
description: doc.description,
|
||||
modified: doc.time?.modified,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
// verdaccio's events. The tool runtime imports this once the broker is bound.
|
||||
//
|
||||
// Emits (novox/hq ADR 0041/0042):
|
||||
// module.verdaccio.package.published — a new package version was published to the registry
|
||||
//
|
||||
// A genuinely useful signal: a package was just published, so anything on the mesh that pins,
|
||||
// mirrors or announces dependency releases can react without polling the registry. Verdaccio has
|
||||
// no publish webhook, so the module discovers it by diffing the package list's latest versions.
|
||||
//
|
||||
// The polling is deliberately unhurried: a publish a minute late is still the event, whereas
|
||||
// hammering the registry for immediacy nobody asked for is not.
|
||||
|
||||
import { emit } from "@novox/mesh-sdk/events";
|
||||
import { VerdaccioClient } from "./client.js";
|
||||
|
||||
const verdaccio = VerdaccioClient.fromEnv();
|
||||
|
||||
// The latest version we have seen per package name. Primed silently on the first look so a registry
|
||||
// that was already populated when this started does not announce its whole catalog as freshly
|
||||
// published.
|
||||
const latest = new Map<string, string>();
|
||||
let primed = false;
|
||||
|
||||
async function pollPackages(): Promise<void> {
|
||||
const packages = await verdaccio.listPackages();
|
||||
for (const pkg of packages) {
|
||||
if (!pkg.version) continue;
|
||||
const known = latest.get(pkg.name);
|
||||
if (known !== pkg.version) {
|
||||
// A name we have not seen, or a name whose latest version moved — both are a publish.
|
||||
if (primed) await emit("module.verdaccio.package.published", { name: pkg.name, version: pkg.version });
|
||||
latest.set(pkg.name, pkg.version);
|
||||
}
|
||||
}
|
||||
primed = true;
|
||||
}
|
||||
|
||||
const tick = (fn: () => Promise<void>, everyMs: number): void => {
|
||||
const run = (): void => void fn().catch((err) => console.error(`[verdaccio] ${err}`));
|
||||
setInterval(run, everyMs);
|
||||
run();
|
||||
};
|
||||
tick(pollPackages, 60_000);
|
||||
|
||||
console.log("[verdaccio] watching the registry for newly published packages");
|
||||
@@ -1,130 +0,0 @@
|
||||
{
|
||||
"module": "verdaccio",
|
||||
"version": "1",
|
||||
"slug": "verdacc",
|
||||
"capabilities": [
|
||||
"container-runtime"
|
||||
],
|
||||
"emits": [
|
||||
"module.verdaccio.package.published"
|
||||
],
|
||||
"own-secrets": {
|
||||
"broker": "/var/lib/mesh/verdaccio/broker"
|
||||
},
|
||||
"listens": [
|
||||
{
|
||||
"port": 4873,
|
||||
"protocol": "tcp",
|
||||
"from": "mesh",
|
||||
"why": "the package registry, for installs and publishes"
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"id": "mesh-state",
|
||||
"type": "directory",
|
||||
"path": "/var/lib/mesh/verdaccio",
|
||||
"mode": "0700"
|
||||
},
|
||||
{
|
||||
"id": "conf",
|
||||
"type": "directory",
|
||||
"path": "/services/verdaccio/conf",
|
||||
"mode": "0755",
|
||||
"owner": "10001:10001"
|
||||
},
|
||||
{
|
||||
"id": "storage",
|
||||
"type": "directory",
|
||||
"path": "/services/verdaccio/storage",
|
||||
"mode": "0700",
|
||||
"owner": "10001:10001"
|
||||
},
|
||||
{
|
||||
"id": "config",
|
||||
"type": "file",
|
||||
"path": "/services/verdaccio/conf/config.yaml",
|
||||
"mode": "0644",
|
||||
"content": "storage: /verdaccio/storage\nauth:\n htpasswd:\n file: /verdaccio/conf/htpasswd\n max_users: 10\nuplinks:\n npmjs:\n url: https://registry.npmjs.org/\npackages:\n \"**\":\n access: $all\n publish: $authenticated\n proxy: npmjs\nserver:\n keepAliveTimeout: 60\n maxBodySize: 10mb\nmiddlewares:\n audit:\n enabled: true\nlog:\n type: stdout\n format: pretty\n level: http\n"
|
||||
},
|
||||
{
|
||||
"id": "server",
|
||||
"type": "container",
|
||||
"name": "verdaccio",
|
||||
"image": "verdaccio/verdaccio@sha256:7b067a47ae51fb9dff3dcdce60ec0a2cbd7650c208cb4b9f6d37cb1b09b39d43",
|
||||
"ports": [
|
||||
"4873"
|
||||
],
|
||||
"volumes": [
|
||||
"/services/verdaccio/storage:/verdaccio/storage",
|
||||
"/services/verdaccio/conf:/verdaccio/conf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "runtime-config",
|
||||
"type": "file",
|
||||
"path": "/var/lib/mesh/verdaccio/config.json",
|
||||
"mode": "0600",
|
||||
"content": "{}\n",
|
||||
"merge": "json"
|
||||
},
|
||||
{
|
||||
"id": "runtime",
|
||||
"type": "container",
|
||||
"name": "mesh-verdaccio",
|
||||
"network": "host",
|
||||
"volumes": [
|
||||
"/var/lib/mesh/verdaccio/broker:/run/secrets/broker:ro",
|
||||
"/var/lib/mesh/verdaccio/config.json:/run/config/config.json:ro"
|
||||
],
|
||||
"env": {
|
||||
"MESH_BROKER_FILE": "/run/secrets/broker",
|
||||
"MESH_VERDACCIO_URL": "http://127.0.0.1:4873",
|
||||
"MESH_VERDACCIO_CONFIG_FILE": "/run/config/config.json"
|
||||
},
|
||||
"restart-on": [
|
||||
"runtime-config"
|
||||
],
|
||||
"artifact": "runtime"
|
||||
}
|
||||
],
|
||||
"requires": [
|
||||
"route"
|
||||
],
|
||||
"contributes": {
|
||||
"route": {
|
||||
"label": "npm",
|
||||
"port": 4873
|
||||
}
|
||||
},
|
||||
"binds": {
|
||||
"route": "/var/lib/mesh/verdaccio/route.json"
|
||||
},
|
||||
"provides": [
|
||||
{
|
||||
"name": "package-registry",
|
||||
"scope": "mesh"
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
"on": [
|
||||
{
|
||||
"arg": "BUILD_BASE",
|
||||
"module": "mesh-tools",
|
||||
"artifact": "build"
|
||||
},
|
||||
{
|
||||
"arg": "RUNTIME_BASE",
|
||||
"module": "mesh-tools",
|
||||
"artifact": "runtime"
|
||||
}
|
||||
],
|
||||
"artifacts": [
|
||||
{
|
||||
"name": "runtime",
|
||||
"kind": "image",
|
||||
"from": "Dockerfile"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"name": "@novox/module-verdaccio",
|
||||
"version": "0.1.0",
|
||||
"description": "verdaccio — private npm registry. Its API client, tools and events live here (novox/hq ADR 0039).",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@novox/mesh-sdk": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"typescript": "^5.6.0"
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// verdaccio's tools — its own code (novox/hq ADR 0039), importing verdaccio's own client. They
|
||||
// return structured data; the mesh serves them through the sdk's tool harness.
|
||||
|
||||
import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools";
|
||||
import { VerdaccioClient } from "../client.js";
|
||||
|
||||
export function getVerdaccioTools(verdaccio: VerdaccioClient): ToolDefinition[] {
|
||||
return [
|
||||
{
|
||||
name: "verdaccio_list_packages",
|
||||
description: "List every package hosted on the private npm registry, with each one's latest version.",
|
||||
input: {},
|
||||
run: async () => {
|
||||
const packages = await verdaccio.listPackages();
|
||||
return { count: packages.length, packages };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "verdaccio_package_info",
|
||||
description: "Details of one package on the registry: its latest tag, all published versions, and description.",
|
||||
input: { name: { type: "string", description: "the package name, e.g. '@novox/mesh-sdk'" } },
|
||||
run: async (args) => verdaccio.getPackageInfo(String(args.name)),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// The tools exist only when a registry URL is configured; otherwise verdaccio contributes none
|
||||
// rather than failing the whole runtime.
|
||||
registerModuleTools("verdaccio", (env) => {
|
||||
try {
|
||||
return getVerdaccioTools(VerdaccioClient.fromEnv(env));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["client.ts", "index.ts", "tools/index.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user