mesh/merge-gate pass: builds baserow, grafana, mailu, matrix, mongodb, mosquitto, nodered, postgres, redis, step-ca, supabase, website → ace, novox; no bus…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/health-the-first-declarations delivered: every member is delivered
Seven modules' images ship a check the mesh never read. Adopted by name where it says healthy on the live mesh today: nine of mail's containers (not its antivirus, whose six-minute start is past the five-minute bound, nor its cache, whose image ships none), the certificate authority, the spreadsheet app, four of the database suite's (the studio among them, with the address it binds fixed), the flow editor and the chat client. And the endpoints four services already declare, looked at from the machine: tcp on the database, the cache, the document store and the broker; http on the website and the dashboards. The count of undeclared falls from 93 to 70.
186 lines
6.7 KiB
JSON
186 lines
6.7 KiB
JSON
{
|
|
"module": "nodered",
|
|
"version": "1",
|
|
"emits": [
|
|
"flows.deployed"
|
|
],
|
|
"own-secrets": {
|
|
"admin": {
|
|
"path": "${dir:mesh-state}/admin",
|
|
"taken": "at-start"
|
|
},
|
|
"api-token": {
|
|
"path": "${dir:mesh-state}/api-token",
|
|
"taken": "at-start"
|
|
}
|
|
},
|
|
"capabilities": [
|
|
"container-runtime"
|
|
],
|
|
"listens": [
|
|
{
|
|
"name": "web",
|
|
"port": 1880,
|
|
"protocol": "tcp",
|
|
"from": "mesh",
|
|
"why": "the flow editor and the endpoints flows expose"
|
|
}
|
|
],
|
|
"data": {
|
|
"own": [
|
|
{
|
|
"id": "data",
|
|
"path": "${dir:data}",
|
|
"class": "valuable",
|
|
"why": "flows and their credentials"
|
|
}
|
|
]
|
|
},
|
|
"resources": [
|
|
{
|
|
"id": "mesh-state",
|
|
"type": "directory",
|
|
"mode": "0700",
|
|
"place": "mesh"
|
|
},
|
|
{
|
|
"id": "state",
|
|
"type": "directory",
|
|
"mode": "0700",
|
|
"place": "."
|
|
},
|
|
{
|
|
"id": "data",
|
|
"type": "directory",
|
|
"mode": "0700",
|
|
"owner": "1000:1000"
|
|
},
|
|
{
|
|
"id": "written",
|
|
"type": "directory",
|
|
"mode": "0700"
|
|
},
|
|
{
|
|
"id": "settings-code",
|
|
"type": "file",
|
|
"path": "${dir:state}/settings.js",
|
|
"mode": "0600",
|
|
"owner": "1000:1000",
|
|
"content": "// Node-RED's settings, written by the mesh from the nodered module. What an assignment may change\n// is settings.json beside this file (merged key by key); the credentials are the mesh's secrets and\n// reach Node-RED only through this file. The flows' own credentials stay encrypted in the user\n// directory under the key Node-RED keeps there (.config.runtime.json), which is data, not this.\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst crypto = require(\"crypto\");\n\nconst ADMIN_PASSWORD = \"${secret:admin}\";\nconst API_TOKEN = \"${secret:api-token}\";\nconst ADMIN = { username: \"admin\", permissions: \"*\" };\n\nconst settings = JSON.parse(fs.readFileSync(path.join(__dirname, \"settings.json\"), \"utf8\"));\n// The mesh's keys, not Node-RED's: endpoints lands in every merged file; timeZone is the\n// assignment's way to set the zone flows schedule and format in; mqtt names the broker nodes the\n// module's MQTT step keeps pointed at the mesh's broker; topics is what nodered asks the broker for.\nif (settings.timeZone) process.env.TZ = settings.timeZone;\ndelete settings.timeZone;\ndelete settings.endpoints;\ndelete settings.mqtt;\ndelete settings.topics;\n\nfunction same(a, b) {\n const x = crypto.createHash(\"sha256\").update(String(a)).digest();\n const y = crypto.createHash(\"sha256\").update(String(b)).digest();\n return crypto.timingSafeEqual(x, y);\n}\n\n// The admin secret is a password, or, accepted from an existing install, the bcrypt hash its\n// settings held, so the password people already use keeps working.\nfunction passwordMatches(given) {\n if (/^\\$2[aby]\\$\\d\\d\\$/.test(ADMIN_PASSWORD)) return require(\"bcryptjs\").compare(String(given), ADMIN_PASSWORD);\n return Promise.resolve(same(given, ADMIN_PASSWORD));\n}\n\nmodule.exports = Object.assign(settings, {\n uiPort: 1880,\n adminAuth: {\n type: \"credentials\",\n users: (username) => Promise.resolve(username === ADMIN.username ? ADMIN : null),\n authenticate: (username, password) =>\n username === ADMIN.username\n ? passwordMatches(password).then((ok) => (ok ? ADMIN : null))\n : Promise.resolve(null),\n // The module's own tools call the admin API with this bearer token.\n tokens: (token) => Promise.resolve(same(token, API_TOKEN) ? { username: \"mesh\", permissions: \"*\" } : null),\n },\n});\n"
|
|
},
|
|
{
|
|
"id": "settings",
|
|
"type": "file",
|
|
"path": "${dir:state}/settings.json",
|
|
"mode": "0600",
|
|
"owner": "1000:1000",
|
|
"merge": "json",
|
|
"content": "{\n \"flowFile\": \"flows.json\",\n \"flowFilePretty\": true,\n \"diagnostics\": { \"enabled\": true, \"ui\": true },\n \"runtimeState\": { \"enabled\": false, \"ui\": false },\n \"logging\": { \"console\": { \"level\": \"info\", \"metrics\": false, \"audit\": false } },\n \"exportGlobalContextKeys\": false,\n \"externalModules\": {},\n \"editorTheme\": { \"projects\": { \"enabled\": false } },\n \"functionExternalModules\": true,\n \"debugMaxLength\": 1000,\n \"mqttReconnectTime\": 15000,\n \"serialReconnectTime\": 15000\n}\n"
|
|
},
|
|
{
|
|
"id": "server",
|
|
"type": "container",
|
|
"name": "nodered",
|
|
"image": "nodered/node-red@sha256:a649dd711d55490151a2c39a8e48ad0c44325488fbc0e66315f2d2e19e5e1ace",
|
|
"health": {
|
|
"kind": "runtime"
|
|
},
|
|
"env": {
|
|
"TZ": "Etc/UTC"
|
|
},
|
|
"ports": [
|
|
"1880"
|
|
],
|
|
"args": [
|
|
"--settings",
|
|
"/data/settings.js"
|
|
],
|
|
"volumes": [
|
|
"${dir:data}:/data",
|
|
"${dir:state}/settings.js:/data/settings.js:ro",
|
|
"${dir:state}/settings.json:/data/settings.json:ro"
|
|
],
|
|
"restart-on": [
|
|
"settings-code",
|
|
"settings"
|
|
]
|
|
},
|
|
{
|
|
"id": "runtime-config",
|
|
"type": "file",
|
|
"path": "${dir:mesh-state}/config.json",
|
|
"mode": "0600",
|
|
"content": "{\n \"token\": \"${secret:api-token}\"\n}\n"
|
|
},
|
|
{
|
|
"id": "mqtt-env",
|
|
"type": "file",
|
|
"path": "${dir:state}/mqtt.env",
|
|
"mode": "0600",
|
|
"content": "MESH_NODERED_URL=http://127.0.0.1:${port:1880}\nMESH_NODERED_CONFIG_FILE=${dir:mesh-state}/config.json\nMESH_PROVISIONS_DIR=${dir:state}\nMESH_WRITTEN_DIR=${dir:written}\n"
|
|
},
|
|
{
|
|
"id": "mqtt",
|
|
"type": "process",
|
|
"name": "nodered-mqtt",
|
|
"artifact": "code",
|
|
"run": [
|
|
"node",
|
|
"mqtt/index.js"
|
|
],
|
|
"run-once": true,
|
|
"env-file": [
|
|
"${dir:state}/mqtt.env"
|
|
],
|
|
"restart-on": [
|
|
"mqtt-env",
|
|
"bound-mqtt-topic",
|
|
"secret-mqtt-topic",
|
|
"settings"
|
|
]
|
|
}
|
|
],
|
|
"requires": [
|
|
"mqtt-topic",
|
|
"route"
|
|
],
|
|
"contributes": {
|
|
"mqtt-topic": {
|
|
"topics": [
|
|
"#"
|
|
]
|
|
},
|
|
"route": {
|
|
"label": "nodered",
|
|
"endpoint": "web"
|
|
}
|
|
},
|
|
"binds": {
|
|
"route": "${dir:state}/route.json",
|
|
"mqtt-topic": "${dir:state}/mqtt-topic.json"
|
|
},
|
|
"secrets": {
|
|
"mqtt-topic": "${dir:state}/mqtt-topic.secret"
|
|
},
|
|
"build": {
|
|
"artifacts": [
|
|
{
|
|
"name": "code",
|
|
"kind": "bundle",
|
|
"language": "typescript",
|
|
"entrypoints": [
|
|
"tools/index.js",
|
|
"mqtt/index.js"
|
|
],
|
|
"loads": [
|
|
"tools/index.js"
|
|
],
|
|
"env": {
|
|
"MESH_NODERED_URL": "http://127.0.0.1:${port:1880}",
|
|
"MESH_NODERED_CONFIG_FILE": "${dir:mesh-state}/config.json"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|