modified: default-environment.env

modified:   monitoring/node-red/Dockerfile
	modified:   monitoring/node-red/data/.flows.json.backup
	modified:   monitoring/node-red/data/context/00b02bbd01c91485/flow.json
	modified:   monitoring/node-red/data/flows.json
	modified:   monitoring/node-red/data/test-container.sh
	modified:   monitoring/node-red/docker-compose.yml
	modified:   services-up.sh
	monitoring/node-red/data/update-events.ndjson
This commit is contained in:
git
2026-04-13 09:41:16 +10:00
parent d6325494c7
commit 86fba4f43f
8 changed files with 212 additions and 85 deletions
+1
View File
@@ -62,3 +62,4 @@ PORTAINER_GODEBUG=netdns=cgo
# Node-red # Node-red
DOCKER_SOCKET_PROXY_HOST=tcp://docker-socket-proxy:2375 DOCKER_SOCKET_PROXY_HOST=tcp://docker-socket-proxy:2375
DOCKER_SOCKET_PROXY_LOG_LEVEL=debug DOCKER_SOCKET_PROXY_LOG_LEVEL=debug
NODE_COMPOSE_ROOT=/compose
+1 -1
View File
@@ -2,6 +2,6 @@ FROM nodered/node-red:latest
USER root USER root
RUN apk add --no-cache docker-cli docker-cli-compose RUN apk add --no-cache docker-cli docker-cli-compose
RUN addgroup -g 131 -S docker && addgroup node-red docker #RUN addgroup -g 131 -S docker && addgroup node-red docker
USER node-red USER node-red
+179 -16
View File
@@ -60,7 +60,7 @@
"type": "function", "type": "function",
"z": "00b02bbd01c91485", "z": "00b02bbd01c91485",
"name": "Create Pull Command", "name": "Create Pull Command",
"func": "const labels = msg.payload.alerts.labels || {};\nconst container = labels.container;\nconst image = labels.compose_image || labels.running_image || labels.image;\nconst project = labels.com_docker_compose_project\n\nif (project == \"core\") {\n var host = \"docker\"\n}\nelse {\n host = \"raspi\"\n}\n\nif (!container) {\n node.warn(\"No container found in alert labels\");\n return null; // skip this alert\n}\n\nmsg.payload = `/compose/${host}/services-up.sh --profile all pull -q ${container}`;\nmsg.container = container;\nmsg.image = image;\nmsg.host = host;\nnode.log(`New docker update available\n Container: ${container}\n Image: ${image}\n Host: ${host}`)\nreturn msg;", "func": "const labels = msg.payload.alerts.labels || {};\nconst container = labels.container;\nconst image = labels.compose_image || labels.running_image || labels.image;\nconst project = labels.com_docker_compose_project;\n\nconst host = project === \"core\" ? \"docker\" : \"raspi\";\n\nif (!container) {\n node.warn(\"No container found in alert labels\");\n return null;\n}\n\nmsg.payload = `PROJECT_ROOT=\"/compose/${host}\" /compose/${host}/services-up.sh --profile all pull -q ${container}`;\nmsg.container = container;\nmsg.image = image;\nmsg.host = host;\n\nnode.log(`New docker update available\n Container: ${container}\n Image: ${image}\n Host: ${host}`);\nreturn msg;",
"outputs": 1, "outputs": 1,
"timeout": "", "timeout": "",
"noerr": 0, "noerr": 0,
@@ -276,7 +276,7 @@
"type": "function", "type": "function",
"z": "00b02bbd01c91485", "z": "00b02bbd01c91485",
"name": "Build Deploy Command", "name": "Build Deploy Command",
"func": "const container = msg.container;\nconst image = msg.image;\nconst host = msg.host;\nmsg.payload = `/compose/${host}/services-up.sh --profile all up -d ${container}`;\nmsg.image = image;\nmsg.container = container;\nmsg.host = host;\nnode.log(`Test Successful\n Container: ${container}\n Image: ${msg.image}\n Host: ${host}`\n )\nreturn msg;", "func": "const container = msg.container;\nconst image = msg.image;\nconst host = msg.host;\nmsg.payload = `PROJECT_ROOT=\"/compose/${host} /compose/${host}/services-up.sh --profile all up -d ${container}`;\nmsg.image = image;\nmsg.container = container;\nmsg.host = host;\nnode.log(`Test Successful\n Container: ${container}\n Image: ${msg.image}\n Host: ${host}`\n )\nreturn msg;",
"outputs": 1, "outputs": 1,
"timeout": "", "timeout": "",
"noerr": 0, "noerr": 0,
@@ -375,7 +375,7 @@
"type": "function", "type": "function",
"z": "00b02bbd01c91485", "z": "00b02bbd01c91485",
"name": "Pull Image Failed", "name": "Pull Image Failed",
"func": "const labels = msg.payload.alerts.labels || {};\nconst container = labels.container;\nconst host = msg.host\nconst image = labels.compose_image || labels.running_image || labels.image;\n\nlet attempts = flow.get(\"dockerUpdateAttempts\") || {};\n\nif (msg.updateKey && attempts[msg.updateKey]) {\n const firstFailure = !attempts[msg.updateKey].notified;\n\n attempts[msg.updateKey].status = \"test_failed\";\n attempts[msg.updateKey].failedAt = Date.now();\n\n if (firstFailure) {\n attempts[msg.updateKey].notified = true;\n\n msg.notification = {\n title: `Docker update locked out: ${msg.container}`,\n message:\n `Automatic update for ${msg.container} failed.\\n\n Image: ${msg.image}\n Host: ${msg.host}\n Result: ${attempts[msg.updateKey].status}\\n\n Further Grafana alerts for this update will be ignored until manual intervention.`\n };\n\n flow.set(\"dockerUpdateAttempts\", attempts);\n\n // send to 2 outputs:\n // output 1 = existing failure handling\n // output 2 = notification flow\n return [msg, msg];\n }\n\n flow.set(\"dockerUpdateAttempts\", attempts);\n}\n\nnode.log(`Pull image failed\n Command: ${msg.payload}\n Container: ${container}\n Image: ${image}\n Host: ${host}`)\nreturn [msg, null];", "func": "let attempts = flow.get(\"dockerUpdateAttempts\") || {};\n\nif (msg.updateKey && attempts[msg.updateKey]) {\n const firstFailure = !attempts[msg.updateKey].notified;\n\n attempts[msg.updateKey].status = \"pull_failed\";\n attempts[msg.updateKey].failedAt = Date.now();\n\n if (firstFailure) {\n attempts[msg.updateKey].notified = true;\n\n msg.notification = {\n title: `Docker update locked out: ${msg.container}`,\n message:\n `Automatic update for ${msg.container} failed.\n\n` +\n `Image: ${msg.image}\n` +\n `Host: ${msg.host}\n` +\n `Result: ${attempts[msg.updateKey].status}\n\n` +\n `Further Grafana alerts for this update will be ignored until manual intervention.`\n };\n\n flow.set(\"dockerUpdateAttempts\", attempts);\n return [msg, msg];\n }\n\n flow.set(\"dockerUpdateAttempts\", attempts);\n}\n\nnode.log(`Pull image failed\n Command: ${msg.payload}\n Container: ${msg.container}\n Image: ${msg.image}\n Host: ${msg.host}`);\nreturn [msg, null];",
"outputs": 2, "outputs": 2,
"timeout": "", "timeout": "",
"noerr": 0, "noerr": 0,
@@ -398,7 +398,7 @@
"type": "function", "type": "function",
"z": "00b02bbd01c91485", "z": "00b02bbd01c91485",
"name": "Test Image Failed", "name": "Test Image Failed",
"func": "//const labels = msg.payload.alerts.labels || {};\n//const container = labels.container;\nconst host = msg.host;\n//const image = labels.compose_image || labels.running_image || labels.image;\n\nlet attempts = flow.get(\"dockerUpdateAttempts\") || {};\n\nif (msg.updateKey && attempts[msg.updateKey]) {\n const firstFailure = !attempts[msg.updateKey].notified;\n\n attempts[msg.updateKey].status = \"test_failed\";\n attempts[msg.updateKey].failedAt = Date.now();\n\n if (firstFailure) {\n attempts[msg.updateKey].notified = true;\n\n msg.notification = {\n title: `Docker update locked out: ${msg.container}`,\n message:\n `Automatic update for ${msg.container} failed.\\n\n Image: ${msg.image}\n Host: ${msg.host}\n Result: ${attempts[msg.updateKey].status}\\n\n Further Grafana alerts for this update will be ignored until manual intervention.`\n };\n\n flow.set(\"dockerUpdateAttempts\", attempts);\n\n // send to 2 outputs:\n // output 1 = existing failure handling\n // output 2 = notification flow\n return [msg, msg];\n }\n\n flow.set(\"dockerUpdateAttempts\", attempts);\n}\n\nnode.log(`Test image failed\\n\n Command: ${msg.payload}\\n\n Container: ${msg.container}\\n\n Image: ${msg.image}\\n\n Host: ${host}`)\nreturn [msg, null];", "func": "let attempts = flow.get(\"dockerUpdateAttempts\") || {};\n\nif (msg.updateKey && attempts[msg.updateKey]) {\n const firstFailure = !attempts[msg.updateKey].notified;\n\n attempts[msg.updateKey].status = \"test_failed\";\n attempts[msg.updateKey].failedAt = Date.now();\n\n if (firstFailure) {\n attempts[msg.updateKey].notified = true;\n\n msg.notification = {\n title: `Docker update locked out: ${msg.container}`,\n message:\n `Automatic update for ${msg.container} failed.\n\n` +\n `Image: ${msg.image}\n` +\n `Host: ${msg.host}\n` +\n `Result: ${attempts[msg.updateKey].status}\n\n` +\n `Further Grafana alerts for this update will be ignored until manual intervention.`\n };\n\n flow.set(\"dockerUpdateAttempts\", attempts);\n return [msg, msg];\n }\n\n flow.set(\"dockerUpdateAttempts\", attempts);\n}\n\nnode.log(`Test image failed\n Command: ${msg.payload}\n Container: ${msg.container}\n Image: ${msg.image}\n Host: ${msg.host}`);\nreturn [msg, null];",
"outputs": 2, "outputs": 2,
"timeout": "", "timeout": "",
"noerr": 0, "noerr": 0,
@@ -460,7 +460,7 @@
"type": "function", "type": "function",
"z": "00b02bbd01c91485", "z": "00b02bbd01c91485",
"name": "Unknown Project", "name": "Unknown Project",
"func": "const labels = msg.payload.alerts.labels || {};\nconst container = labels.container;\nconst image = labels.compose_image || labels.running_image || labels.image;\nconst project = labels.com_docker_compose_project\n\nnode.warn(`Unable to map project name ${project} to host.\\n\n Updates for ${container} failed`)\nreturn msg;", "func": "const payload = (msg.payload && typeof msg.payload === \"object\") ? msg.payload : {};\nconst labels = (payload.labels && typeof payload.labels === \"object\") ? payload.labels : {};\n\nconst container = labels.container || \"unknown container\";\nconst image = labels.compose_image || labels.running_image || labels.image || \"unknown image\";\nconst project = labels.com_docker_compose_project || \"unknown project\";\n\nnode.warn(`Unable to map project name ${project} to host.\n\nUpdates for ${container} (${image}) failed`);\nreturn msg;",
"outputs": 1, "outputs": 1,
"timeout": "", "timeout": "",
"noerr": 0, "noerr": 0,
@@ -493,7 +493,7 @@
"type": "function", "type": "function",
"z": "00b02bbd01c91485", "z": "00b02bbd01c91485",
"name": "Repeat update Suppresed", "name": "Repeat update Suppresed",
"func": "node.warn(\n `${msg.payload.alerts.labels.container} ` +\n `(${msg.payload.alerts.labels.compose_image})`\n);\nreturn msg;", "func": "const labels = msg.payload.alerts.labels || {};\n\nnode.warn(\n `${labels.container || \"unknown\"} ` +\n `(${labels.compose_image || labels.running_image || \"unknown image\"})`\n);\nreturn msg;",
"outputs": 1, "outputs": 1,
"timeout": "", "timeout": "",
"noerr": 0, "noerr": 0,
@@ -565,7 +565,7 @@
"type": "function", "type": "function",
"z": "00b02bbd01c91485", "z": "00b02bbd01c91485",
"name": "Check Already Attempted", "name": "Check Already Attempted",
"func": "const labels = msg.payload.alerts.labels || {};\n\nconst container = labels.container;\nconst image = labels.compose_image || labels.running_image || labels.image;\nconst project = labels.com_docker_compose_project\n\n// Load persistent attempt registry\nlet attempts = flow.get(\"dockerUpdateAttempts\") || {};\n\nif (project == \"core\") {\n var host = \"docker\"\n}\nelse {\n host = \"raspi\"\n}\n\n// Unique key for this exact update attempt\nconst key = `${container}|${image}|${host}`;\n\n// If we've already tried this image for this container, suppress it\nif (attempts[key]) {\n node.warn(\n `Ignoring repeated update alert for ${container} -> ${image}. ` +\n `Already attempted at ${new Date(attempts[key].time).toISOString()}`\n );\n\n msg.suppressed = true;\n msg.updateKey = key;\n msg.attemptInfo = attempts[key];\n\n // output 1 = continue flow\n // output 2 = suppressed repeat\n return [null, msg];\n}\n\n// First time we've seen this update, record it immediately\nattempts[key] = {\n time: Date.now(),\n status: \"started\"\n};\n\nflow.set(\"dockerUpdateAttempts\", attempts);\n\nmsg.updateKey = key;\n\nnode.log(`First attempt for ${container} -> ${image}`);\n\n// continue normal flow\nreturn [msg, null];", "func": "const labels = msg.payload.alerts.labels || {};\n\nconst container = labels.container;\nconst image = labels.compose_image || labels.running_image || labels.image;\nconst project = labels.com_docker_compose_project;\n\nif (!container || !image) {\n node.warn(\"Missing container/image labels; skipping update attempt tracking\");\n return [null, null];\n}\n\n// Load persistent attempt registry\nlet attempts = flow.get(\"dockerUpdateAttempts\") || {};\n\nconst host = project === \"core\" ? \"docker\" : \"raspi\";\n\n// Unique key for this exact update attempt\nconst key = `${container}|${image}|${host}`;\n\n// If we've already tried this image for this container, suppress it\nif (attempts[key]) {\n node.warn(\n `Ignoring repeated update alert for ${container} -> ${image}. ` +\n `Already attempted at ${new Date(attempts[key].time).toISOString()}`\n );\n\n msg.suppressed = true;\n msg.updateKey = key;\n msg.attemptInfo = attempts[key];\n\n return [null, msg];\n}\n\n// First time we've seen this update, record it immediately\nattempts[key] = {\n time: Date.now(),\n status: \"started\"\n};\n\nflow.set(\"dockerUpdateAttempts\", attempts);\n\nmsg.updateKey = key;\n\nnode.log(`First attempt for ${container} -> ${image}`);\n\nreturn [msg, null];",
"outputs": 2, "outputs": 2,
"timeout": 0, "timeout": 0,
"noerr": 0, "noerr": 0,
@@ -936,7 +936,7 @@
"y": 200, "y": 200,
"wires": [ "wires": [
[ [
"0135d283b9edfb01" "c1aa11bb22cc33dd"
] ]
] ]
}, },
@@ -972,7 +972,7 @@
"y": 300, "y": 300,
"wires": [ "wires": [
[ [
"4eafade32c867e40" "d2aa11bb22cc33dd"
] ]
] ]
}, },
@@ -981,7 +981,7 @@
"type": "function", "type": "function",
"z": "c5240b64a962ea54", "z": "c5240b64a962ea54",
"name": "Docker update success", "name": "Docker update success",
"func": "const container = msg.container || \"unknown container\";\nconst code = msg.payload.code;\nconst stderr = flow.get(\"pull_stderr\") || \"Unknown error\";\n\nmsg.payload = {\n title: \"Container Updated\",\n message: `The ${container} container has been succesfully updated`,\n priority: 8\n};\n\nreturn msg;", "func": "const container = msg.container || \"unknown container\";\nconst code = msg.payload.code;\nconst stderr = flow.get(\"pull_stderr\") || \"Unknown error\";\n\nmsg.payload = {\n title: \"Container Updated\",\n message: `Container: ${container}\n Host: ${msg.host}`,\n priority: 8\n};\n\nreturn msg;",
"outputs": 1, "outputs": 1,
"timeout": 0, "timeout": 0,
"noerr": 0, "noerr": 0,
@@ -1008,7 +1008,7 @@
"y": 380, "y": 380,
"wires": [ "wires": [
[ [
"7d8200040f9b1e83" "e3aa11bb22cc33dd"
] ]
] ]
}, },
@@ -1017,7 +1017,7 @@
"type": "function", "type": "function",
"z": "c5240b64a962ea54", "z": "c5240b64a962ea54",
"name": "Docker updates Unknown Project", "name": "Docker updates Unknown Project",
"func": "const container = msg.container || \"unknown container\";\nconst code = msg.payload.code;\nconst stderr = flow.get(\"pull_stderr\") || \"Unknown error\";\nconst project = msg.payload.labels.com_docker_compose_project\nmsg.payload = {\n title: \"Container Updates Failed\",\n message: `The ${container} container has failed.\\n\n Unknown project ${project}`,\n priority: 8\n};\n\nreturn msg;", "func": "const payload = (msg.payload && typeof msg.payload === \"object\") ? msg.payload : {};\nconst labels = (payload.labels && typeof payload.labels === \"object\") ? payload.labels : {};\n\nconst container = msg.container || labels.container || \"unknown container\";\nconst project = labels.com_docker_compose_project || msg.project || \"unknown project\";\n\nmsg.payload = {\n title: \"Container Updates Failed\",\n message: `The ${container} container has failed.\n\nUnknown project ${project}`,\n priority: 8\n};\n\nreturn msg;",
"outputs": 1, "outputs": 1,
"timeout": 0, "timeout": 0,
"noerr": 0, "noerr": 0,
@@ -1044,7 +1044,7 @@
"y": 460, "y": 460,
"wires": [ "wires": [
[ [
"c3d07241f4a570af" "f4aa11bb22cc33dd"
] ]
] ]
}, },
@@ -1078,7 +1078,7 @@
"y": 520, "y": 520,
"wires": [ "wires": [
[ [
"d1346f7151103832" "a5aa11bb22cc33dd"
] ]
] ]
}, },
@@ -1095,7 +1095,7 @@
"y": 580, "y": 580,
"wires": [ "wires": [
[ [
"1a9798d5c081240a" "b6aa11bb22cc33dd"
] ]
] ]
}, },
@@ -1104,7 +1104,7 @@
"type": "function", "type": "function",
"z": "c5240b64a962ea54", "z": "c5240b64a962ea54",
"name": "Docker updates locked", "name": "Docker updates locked",
"func": "const container = msg.container || \"unknown container\";\nconst code = msg.payload.code;\nconst stderr = flow.get(\"pull_stderr\") || \"Unknown error\";\n//const project = msg.payload.labels.com_docker_compose_project\nmsg.payload = {\n title: \"Update Locked out\",\n message: `Container: ${container}\n Host: ${msg.host}\\n\n Manual Intervention required.`,\n priority: 8\n};\n\nreturn msg;", "func": "const container = msg.container || \"unknown container\";\nconst defaultPayload = {\n title: \"Update Locked out\",\n message: `Container: ${container}\nHost: ${msg.host}\n\nManual intervention required.`,\n priority: 8\n};\n\nif (msg.notification) {\n msg.payload = {\n title: msg.notification.title || defaultPayload.title,\n message: msg.notification.message || defaultPayload.message,\n priority: 8\n };\n return msg;\n}\n\nmsg.payload = defaultPayload;\nreturn msg;",
"outputs": 1, "outputs": 1,
"timeout": 0, "timeout": 0,
"noerr": 0, "noerr": 0,
@@ -1118,5 +1118,168 @@
"8630c7dfcdbcce50" "8630c7dfcdbcce50"
] ]
] ]
},
{
"id": "a1f8e9b2c3d4e5f6",
"type": "function",
"z": "c5240b64a962ea54",
"name": "Build update log event",
"func": "const nowIso = new Date().toISOString();\nconst startedAt = msg.update_started_at || Date.now();\nconst durationMs = Math.max(0, Date.now() - startedAt);\n\nconst payload = (msg.payload && typeof msg.payload === \"object\") ? msg.payload : {};\nconst labels = payload.labels || {};\n\nconst status = (msg.update_status || payload.status || \"unknown\").toString().toLowerCase();\n\nmsg.payload = JSON.stringify({\n ts: nowIso,\n flow: \"docker-updates\",\n event: msg.update_event || \"attempt\",\n container: msg.container || labels.container || \"unknown\",\n project: labels.com_docker_compose_project || msg.project || \"unknown\",\n host: msg.host || \"unknown\",\n status,\n success: status === \"success\" ? 1 : 0,\n failed: [\"failed\", \"locked\"].includes(status) ? 1 : 0,\n duration_ms: durationMs,\n code: Number.isFinite(Number(payload.code)) ? Number(payload.code) : 0,\n error: (msg.update_error || payload.error || \"\").toString().slice(0, 300)\n});\n\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 690,
"y": 660,
"wires": [
[
"b1c2d3e4f5a69788"
]
]
},
{
"id": "b1c2d3e4f5a69788",
"type": "file",
"z": "c5240b64a962ea54",
"name": "Write update event log",
"filename": "/data/update-events.ndjson",
"filenameType": "str",
"appendNewline": true,
"createDir": false,
"overwriteFile": "false",
"encoding": "none",
"x": 930,
"y": 660,
"wires": [
[]
]
},
{
"id": "c1aa11bb22cc33dd",
"type": "function",
"z": "c5240b64a962ea54",
"name": "Mark Docker Pull Failed",
"func": "msg.update_status = \"failed\";\nmsg.update_event = \"completed\";\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 470,
"y": 200,
"wires": [
[
"0135d283b9edfb01",
"a1f8e9b2c3d4e5f6"
]
]
},
{
"id": "d2aa11bb22cc33dd",
"type": "function",
"z": "c5240b64a962ea54",
"name": "Mark Docker Test Failed",
"func": "msg.update_status = \"failed\";\nmsg.update_event = \"completed\";\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 470,
"y": 300,
"wires": [
[
"4eafade32c867e40",
"a1f8e9b2c3d4e5f6"
]
]
},
{
"id": "e3aa11bb22cc33dd",
"type": "function",
"z": "c5240b64a962ea54",
"name": "Mark Docker Update Success",
"func": "msg.update_status = \"success\";\nmsg.update_event = \"completed\";\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 470,
"y": 380,
"wires": [
[
"7d8200040f9b1e83",
"a1f8e9b2c3d4e5f6"
]
]
},
{
"id": "f4aa11bb22cc33dd",
"type": "function",
"z": "c5240b64a962ea54",
"name": "Mark Docker Unknown Project",
"func": "msg.update_status = \"failed\";\nmsg.update_event = \"completed\";\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 470,
"y": 460,
"wires": [
[
"c3d07241f4a570af",
"a1f8e9b2c3d4e5f6"
]
]
},
{
"id": "a5aa11bb22cc33dd",
"type": "function",
"z": "c5240b64a962ea54",
"name": "Mark Docker Update Attempt",
"func": "msg.update_status = \"attempt\";\nmsg.update_event = \"attempt\";\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 470,
"y": 520,
"wires": [
[
"d1346f7151103832",
"a1f8e9b2c3d4e5f6"
]
]
},
{
"id": "b6aa11bb22cc33dd",
"type": "function",
"z": "c5240b64a962ea54",
"name": "Mark Docker Update Locked",
"func": "msg.update_status = \"locked\";\nmsg.update_event = \"completed\";\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 470,
"y": 580,
"wires": [
[
"1a9798d5c081240a",
"a1f8e9b2c3d4e5f6"
]
]
} }
] ]
@@ -1,44 +1,3 @@
{ {
"dockerUpdateAttempts": { "dockerUpdateAttempts": {}
"node-exporter|prom/node-exporter:latest|docker": {
"time": 1775631942178,
"status": "started"
},
"prometheus|prom/prometheus:latest|docker": {
"time": 1775631942180,
"status": "started"
},
"nextcloud-redis|redis:latest|docker": {
"time": 1775631942186,
"status": "started"
},
"searxng-webapp|searxng/searxng:latest|docker": {
"time": 1775631942187,
"status": "started"
},
"traefik|traefik:3|docker": {
"time": 1775631942188,
"status": "started"
},
"traefik|unknown|raspi": {
"time": 1775631942191,
"status": "started"
},
"telegraf|unknown|raspi": {
"time": 1775763342447,
"status": "started"
},
"authelia|authelia/authelia:latest|docker": {
"time": 1775804742531,
"status": "started"
},
"passbolt-webapp|passbolt/passbolt:latest-ce|docker": {
"time": 1775804742539,
"status": "started"
},
"gramps-web|ghcr.io/gramps-project/grampsweb:latest|docker": {
"time": 1775891142715,
"status": "started"
}
}
} }
+7 -7
View File
@@ -60,7 +60,7 @@
"type": "function", "type": "function",
"z": "00b02bbd01c91485", "z": "00b02bbd01c91485",
"name": "Create Pull Command", "name": "Create Pull Command",
"func": "const labels = msg.payload.labels || {};\nconst container = labels.container;\nconst image = labels.compose_image || labels.running_image || labels.image;\nconst project = labels.com_docker_compose_project;\n\nconst host = project === \"core\" ? \"docker\" : \"raspi\";\n\nif (!container) {\n node.warn(\"No container found in alert labels\");\n return null;\n}\n\nmsg.payload = `/compose/${host}/services-up.sh --profile all pull -q ${container}`;\nmsg.container = container;\nmsg.image = image;\nmsg.host = host;\n\nnode.log(`New docker update available\n Container: ${container}\n Image: ${image}\n Host: ${host}`);\nreturn msg;", "func": "const labels = msg.payload.alerts.labels || {};\nconst container = labels.container;\nconst image = labels.compose_image || labels.running_image || labels.image;\nconst project = labels.com_docker_compose_project;\n\nconst host = project === \"core\" ? \"docker\" : \"raspi\";\n\nif (!container) {\n node.warn(\"No container found in alert labels\");\n return null;\n}\n\nmsg.payload = `PROJECT_ROOT=\"/compose/${host}\" /compose/${host}/services-up.sh --profile all pull -q ${container}`;\nmsg.container = container;\nmsg.image = image;\nmsg.host = host;\n\nnode.log(`New docker update available\n Container: ${container}\n Image: ${image}\n Host: ${host}`);\nreturn msg;",
"outputs": 1, "outputs": 1,
"timeout": "", "timeout": "",
"noerr": 0, "noerr": 0,
@@ -276,7 +276,7 @@
"type": "function", "type": "function",
"z": "00b02bbd01c91485", "z": "00b02bbd01c91485",
"name": "Build Deploy Command", "name": "Build Deploy Command",
"func": "const container = msg.container;\nconst image = msg.image;\nconst host = msg.host;\nmsg.payload = `/compose/${host}/services-up.sh --profile all up -d ${container}`;\nmsg.image = image;\nmsg.container = container;\nmsg.host = host;\nnode.log(`Test Successful\n Container: ${container}\n Image: ${msg.image}\n Host: ${host}`\n )\nreturn msg;", "func": "const container = msg.container;\nconst image = msg.image;\nconst host = msg.host;\nmsg.payload = `PROJECT_ROOT=\"/compose/${host}\" /compose/${host}/services-up.sh --profile all up -d ${container}`;\nmsg.image = image;\nmsg.container = container;\nmsg.host = host;\nnode.log(`Test Successful\n Container: ${container}\n Image: ${msg.image}\n Host: ${host}`\n )\nreturn msg;",
"outputs": 1, "outputs": 1,
"timeout": "", "timeout": "",
"noerr": 0, "noerr": 0,
@@ -421,7 +421,7 @@
"type": "switch", "type": "switch",
"z": "00b02bbd01c91485", "z": "00b02bbd01c91485",
"name": "docker project name", "name": "docker project name",
"property": "payload.labels.com_docker_compose_project", "property": "payload.alerts.labels.com_docker_compose_project",
"propertyType": "msg", "propertyType": "msg",
"rules": [ "rules": [
{ {
@@ -493,7 +493,7 @@
"type": "function", "type": "function",
"z": "00b02bbd01c91485", "z": "00b02bbd01c91485",
"name": "Repeat update Suppresed", "name": "Repeat update Suppresed",
"func": "const labels = msg.payload.labels || {};\n\nnode.warn(\n `${labels.container || \"unknown\"} ` +\n `(${labels.compose_image || labels.running_image || \"unknown image\"})`\n);\nreturn msg;", "func": "const labels = msg.payload.alerts.labels || {};\n\nnode.warn(\n `${labels.container || \"unknown\"} ` +\n `(${labels.compose_image || labels.running_image || \"unknown image\"})`\n);\nreturn msg;",
"outputs": 1, "outputs": 1,
"timeout": "", "timeout": "",
"noerr": 0, "noerr": 0,
@@ -565,7 +565,7 @@
"type": "function", "type": "function",
"z": "00b02bbd01c91485", "z": "00b02bbd01c91485",
"name": "Check Already Attempted", "name": "Check Already Attempted",
"func": "const labels = msg.payload.labels || {};\n\nconst container = labels.container;\nconst image = labels.compose_image || labels.running_image || labels.image;\nconst project = labels.com_docker_compose_project;\n\nif (!container || !image) {\n node.warn(\"Missing container/image labels; skipping update attempt tracking\");\n return [null, null];\n}\n\n// Load persistent attempt registry\nlet attempts = flow.get(\"dockerUpdateAttempts\") || {};\n\nconst host = project === \"core\" ? \"docker\" : \"raspi\";\n\n// Unique key for this exact update attempt\nconst key = `${container}|${image}|${host}`;\n\n// If we've already tried this image for this container, suppress it\nif (attempts[key]) {\n node.warn(\n `Ignoring repeated update alert for ${container} -> ${image}. ` +\n `Already attempted at ${new Date(attempts[key].time).toISOString()}`\n );\n\n msg.suppressed = true;\n msg.updateKey = key;\n msg.attemptInfo = attempts[key];\n\n return [null, msg];\n}\n\n// First time we've seen this update, record it immediately\nattempts[key] = {\n time: Date.now(),\n status: \"started\"\n};\n\nflow.set(\"dockerUpdateAttempts\", attempts);\n\nmsg.updateKey = key;\n\nnode.log(`First attempt for ${container} -> ${image}`);\n\nreturn [msg, null];", "func": "const labels = msg.payload.alerts.labels || {};\n\nconst container = labels.container;\nconst image = labels.compose_image || labels.running_image || labels.image;\nconst project = labels.com_docker_compose_project;\n\nif (!container || !image) {\n node.warn(\"Missing container/image labels; skipping update attempt tracking\");\n return [null, null];\n}\n\n// Load persistent attempt registry\nlet attempts = flow.get(\"dockerUpdateAttempts\") || {};\n\nconst host = project === \"core\" ? \"docker\" : \"raspi\";\n\n// Unique key for this exact update attempt\nconst key = `${container}|${image}|${host}`;\n\n// If we've already tried this image for this container, suppress it\nif (attempts[key]) {\n node.warn(\n `Ignoring repeated update alert for ${container} -> ${image}. ` +\n `Already attempted at ${new Date(attempts[key].time).toISOString()}`\n );\n\n msg.suppressed = true;\n msg.updateKey = key;\n msg.attemptInfo = attempts[key];\n\n return [null, msg];\n}\n\n// First time we've seen this update, record it immediately\nattempts[key] = {\n time: Date.now(),\n status: \"started\"\n};\n\nflow.set(\"dockerUpdateAttempts\", attempts);\n\nmsg.updateKey = key;\n\nnode.log(`First attempt for ${container} -> ${image}`);\n\nreturn [msg, null];",
"outputs": 2, "outputs": 2,
"timeout": 0, "timeout": 0,
"noerr": 0, "noerr": 0,
@@ -869,7 +869,7 @@
"type": "function", "type": "function",
"z": "c5240b64a962ea54", "z": "c5240b64a962ea54",
"name": "Domain name alerts", "name": "Domain name alerts",
"func": "const name = msg.payload.monitor.name;\nconst hb = msg.payload.heartbeat;\n\nconst isUp = hb.status === 1;\n\nconst icon = isUp ? \"\u2705\" : \"\ud83d\udd34\";\nconst state = isUp ? \"UP\" : \"DOWN\";\n\n// Build a short title for the Gotify notification\nconst title = `${icon} ${name} is ${state}`;\nconst time = hb.localDateTime.split(\" \")[1].split(\":\").slice(0, 2).join(\":\");\n// Build a more detailed message\nlet message = `${icon} Monitor: ${name}\\n`;\nmessage += `Status: ${state}\\n`;\nmessage += `Time: ${time}\\n`;\nmessage += `Details: ${hb.msg}`;\n\nif (hb.ping !== undefined) {\n message += `\\nPing: ${hb.ping} ms`;\n}\n\nif (hb.duration !== undefined) {\n message += `\\nCheck Interval: ${hb.duration} sec`;\n}\n\n// Higher priority for down alerts\nmsg.payload = {\n title: title,\n message: message,\n priority: isUp ? 5 : 8\n};\n\nreturn msg;", "func": "const name = msg.payload.monitor.name;\nconst hb = msg.payload.heartbeat;\n\nconst isUp = hb.status === 1;\n\nconst icon = isUp ? \"\" : \"🔴\";\nconst state = isUp ? \"UP\" : \"DOWN\";\n\n// Build a short title for the Gotify notification\nconst title = `${icon} ${name} is ${state}`;\nconst time = hb.localDateTime.split(\" \")[1].split(\":\").slice(0, 2).join(\":\");\n// Build a more detailed message\nlet message = `${icon} Monitor: ${name}\\n`;\nmessage += `Status: ${state}\\n`;\nmessage += `Time: ${time}\\n`;\nmessage += `Details: ${hb.msg}`;\n\nif (hb.ping !== undefined) {\n message += `\\nPing: ${hb.ping} ms`;\n}\n\nif (hb.duration !== undefined) {\n message += `\\nCheck Interval: ${hb.duration} sec`;\n}\n\n// Higher priority for down alerts\nmsg.payload = {\n title: title,\n message: message,\n priority: isUp ? 5 : 8\n};\n\nreturn msg;",
"outputs": 1, "outputs": 1,
"timeout": 0, "timeout": 0,
"noerr": 0, "noerr": 0,
@@ -1282,4 +1282,4 @@
] ]
] ]
} }
] ]
+1 -1
View File
@@ -9,7 +9,7 @@ test_name="testing-${container}"
compose_script="/compose/${host}/services-up.sh" compose_script="/compose/${host}/services-up.sh"
# Run container in detached mode # Run container in detached mode
$compose_script --profile all run -d --name "$test_name" --build "$container" >/dev/null 2>&1 $compose_script --profile all run -d --name "$test_name" --build "$container"
# Poll health status # Poll health status
timeout=60 # seconds timeout=60 # seconds
+21 -17
View File
@@ -11,6 +11,7 @@ services:
environment: environment:
DOCKER_HOST: ${DOCKER_SOCKET_PROXY_HOST} DOCKER_HOST: ${DOCKER_SOCKET_PROXY_HOST}
TZ: ${TZ} TZ: ${TZ}
PROJECT_ROOT: ${NODE_COMPOSE_ROOT}
cap_drop: cap_drop:
- ALL - ALL
security_opt: security_opt:
@@ -21,23 +22,26 @@ services:
- ${PROJECT_ROOT}/monitoring/node-red/data:/data - ${PROJECT_ROOT}/monitoring/node-red/data:/data
- ${PROJECT_ROOT}:/compose/docker:ro - ${PROJECT_ROOT}:/compose/docker:ro
- /home/nixos/raspi:/compose/raspi:ro - /home/nixos/raspi:/compose/raspi:ro
- ${PROJECT_ROOT}/default-environment.env:/usr/src/node-red/default-environment.env:ro # - ${PROJECT_ROOT}:/usr/src/node-red:ro
- ${PROJECT_ROOT}/default-network.yml:/usr/src/node-red/default-network.yml:ro
- ${PROJECT_ROOT}/core/docker-compose.yml:/usr/src/node-red/core/docker-compose.yml:ro # - ${PROJECT_ROOT}/default-environment.env:/usr/src/node-red/default-environment.env:ro
- ${PROJECT_ROOT}/monitoring/prometheus/docker-compose.yml:/usr/src/node-red/monitoring/prometheus/docker-compose.yml:ro # - ${PROJECT_ROOT}/default-network.yml:/usr/src/node-red/default-network.yml:ro
- ${PROJECT_ROOT}/monitoring/gotify/docker-compose.yml:/usr/src/node-red/monitoring/gotify/docker-compose.yml:ro # - ${PROJECT_ROOT}/core/docker-compose.yml:/usr/src/node-red/core/docker-compose.yml:ro
- ${PROJECT_ROOT}/monitoring/grafana/docker-compose.yml:/usr/src/node-red/monitoring/grafana/docker-compose.yml:ro # - ${PROJECT_ROOT}/monitoring/prometheus/docker-compose.yml:/usr/src/node-red/monitoring/prometheus/docker-compose.yml:ro
- ${PROJECT_ROOT}/monitoring/portainer/docker-compose.yml:/usr/src/node-red/monitoring/portainer/docker-compose.yml:ro # - ${PROJECT_ROOT}/monitoring/gotify/docker-compose.yml:/usr/src/node-red/monitoring/gotify/docker-compose.yml:ro
- ${PROJECT_ROOT}/monitoring/uptime-kuma/docker-compose.yml:/usr/src/node-red/monitoring/uptime-kuma/docker-compose.yml:ro # - ${PROJECT_ROOT}/monitoring/grafana/docker-compose.yml:/usr/src/node-red/monitoring/grafana/docker-compose.yml:ro
- ${PROJECT_ROOT}/apps/gitea/docker-compose.yml:/usr/src/node-red/apps/gitea/docker-compose.yml:ro # - ${PROJECT_ROOT}/monitoring/portainer/docker-compose.yml:/usr/src/node-red/monitoring/portainer/docker-compose.yml:ro
- ${PROJECT_ROOT}/apps/gramps/docker-compose.yml:/usr/src/node-red/apps/gramps/docker-compose.yml:ro # - ${PROJECT_ROOT}/monitoring/uptime-kuma/docker-compose.yml:/usr/src/node-red/monitoring/uptime-kuma/docker-compose.yml:ro
- ${PROJECT_ROOT}/apps/nextcloud/docker-compose.yml:/usr/src/node-red/apps/nextcloud/docker-compose.yml:ro # - ${PROJECT_ROOT}/apps/gitea/docker-compose.yml:/usr/src/node-red/apps/gitea/docker-compose.yml:ro
- ${PROJECT_ROOT}/apps/passbolt/docker-compose.yml:/usr/src/node-red/apps/passbolt/docker-compose.yml:ro # - ${PROJECT_ROOT}/apps/gramps/docker-compose.yml:/usr/src/node-red/apps/gramps/docker-compose.yml:ro
- ${PROJECT_ROOT}/apps/searxng/docker-compose.yml:/usr/src/node-red/apps/searxng/docker-compose.yml:ro # - ${PROJECT_ROOT}/apps/nextcloud/docker-compose.yml:/usr/src/node-red/apps/nextcloud/docker-compose.yml:ro
- ${PROJECT_ROOT}/apps/shift-recorder/docker-compose.yml:/usr/src/node-red/apps/shift-recorder/docker-compose.yml:ro # - ${PROJECT_ROOT}/apps/passbolt/docker-compose.yml:/usr/src/node-red/apps/passbolt/docker-compose.yml:ro
- ${PROJECT_ROOT}/apps/stockfill/docker-compose.yml:/usr/src/node-red/apps/stockfill/docker-compose.yml:ro # - ${PROJECT_ROOT}/apps/searxng/docker-compose.yml:/usr/src/node-red/apps/searxng/docker-compose.yml:ro
- ${PROJECT_ROOT}/monitoring/node-red/docker-compose.yml:/usr/src/node-red/monitoring/node-red/docker-compose.yml:ro # - ${PROJECT_ROOT}/apps/shift-recorder/docker-compose.yml:/usr/src/node-red/apps/shift-recorder/docker-compose.yml:ro
- ${PROJECT_ROOT}/core/test/docker-compose.yml:/usr/src/node-red/core/test/docker-compose.yml:ro # - ${PROJECT_ROOT}/apps/stockfill/docker-compose.yml:/usr/src/node-red/apps/stockfill/docker-compose.yml:ro
# - ${PROJECT_ROOT}/monitoring/node-red/docker-compose.yml:/usr/src/node-red/monitoring/node-red/docker-compose.yml:ro
# - ${PROJECT_ROOT}/core/test/docker-compose.yml:/usr/src/node-red/core/test/docker-compose.yml:ro
# - ${PROJECT_ROOT}/secrets/stack-secrets.env:/usr/src/node-red/secrets/stack-secrets.env:ro
# - /run/current-system/sw/bin/docker:/usr/bin/docker:ro # - /run/current-system/sw/bin/docker:/usr/bin/docker:ro
# depends_on: # depends_on:
+1 -1
View File
@@ -3,7 +3,7 @@
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="${PROJECT_ROOT:-$SCRIPT_DIR}" PROJECT_ROOT="${SCRIPT_DIR}"
ENV="$PROJECT_ROOT/default-environment.env" ENV="$PROJECT_ROOT/default-environment.env"
SECRETS="$PROJECT_ROOT/secrets/stack-secrets.env" SECRETS="$PROJECT_ROOT/secrets/stack-secrets.env"
PROJECT="core" PROJECT="core"