fix(apps): place n8n sandbox in app directory (#1)
Test / test (push) Canceled after 0s

Preserve the modern Runtipi YAML compose format and validate both modern and legacy app definitions.
This commit was merged in pull request #1.
This commit is contained in:
2026-09-02 15:31:38 +00:00
10 changed files with 103 additions and 236 deletions
+34 -6
View File
@@ -3,6 +3,7 @@ import { appInfoSchema, dynamicComposeSchema } from '@runtipi/common/schemas'
import { fromError } from 'zod-validation-error'; import { fromError } from 'zod-validation-error';
import fs from 'node:fs' import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import YAML from 'yaml'
const getApps = async () => { const getApps = async () => {
const appsDir = await fs.promises.readdir(path.join(process.cwd(), 'apps')) const appsDir = await fs.promises.readdir(path.join(process.cwd(), 'apps'))
@@ -29,7 +30,7 @@ describe("each app should have the required files", async () => {
const apps = await getApps() const apps = await getApps()
for (const app of apps) { for (const app of apps) {
const files = ['config.json', 'docker-compose.json', 'metadata/logo.jpg', 'metadata/description.md'] const files = ['config.json', 'metadata/logo.jpg', 'metadata/description.md']
for (const file of files) { for (const file of files) {
test(`app ${app} should have ${file}`, async () => { test(`app ${app} should have ${file}`, async () => {
@@ -37,6 +38,12 @@ describe("each app should have the required files", async () => {
expect(fileContent).not.toBeNull() expect(fileContent).not.toBeNull()
}) })
} }
test(`app ${app} should have a compose file`, async () => {
const legacyCompose = await getFile(app, 'docker-compose.json')
const modernCompose = await getFile(app, 'docker-compose.yml')
expect(legacyCompose || modernCompose).not.toBeNull()
})
} }
}) })
@@ -58,17 +65,38 @@ describe("each app should have a valid config.json", async () => {
} }
}) })
describe("each app should have a valid docker-compose.json", async () => { describe("modern compose files preserve runtime semantics", () => {
test("n8n-sandbox keeps its one-shot certificate service", async () => {
const fileContent = await getFile('n8n-sandbox', 'docker-compose.yml')
expect(fileContent).not.toBeNull()
const parsed = YAML.parse(fileContent || '')
expect(parsed['x-runtipi']?.schema_version).toBe(2)
expect(parsed.services?.['sandbox-certs']?.restart).toBe('no')
expect(parsed.services?.['sandbox-api']?.['x-runtipi']?.is_main).toBe(true)
})
})
describe("each app should have a valid compose file", async () => {
const apps = await getApps() const apps = await getApps()
for (const app of apps) { for (const app of apps) {
test(`app ${app} should have a valid docker-compose.json`, async () => { test(`app ${app} should have a valid compose file`, async () => {
const fileContent = await getFile(app, 'docker-compose.json') const legacyCompose = await getFile(app, 'docker-compose.json')
const parsed = dynamicComposeSchema.safeParse(JSON.parse(fileContent || '{}')) const modernCompose = await getFile(app, 'docker-compose.yml')
if (modernCompose) {
const parsed = YAML.parse(modernCompose)
expect(parsed['x-runtipi']?.schema_version).toBeTypeOf('number')
expect(parsed.services).toBeTypeOf('object')
return
}
const parsed = dynamicComposeSchema.safeParse(JSON.parse(legacyCompose || '{}'))
if (!parsed.success) { if (!parsed.success) {
const validationError = fromError(parsed.error); const validationError = fromError(parsed.error);
console.error(`Error parsing docker-compose.json for app ${app}:`, validationError.toString()); console.error(`Error parsing compose file for app ${app}:`, validationError.toString());
} }
expect(parsed.success).toBe(true) expect(parsed.success).toBe(true)
+16 -58
View File
@@ -7,64 +7,22 @@
"hostname": "mediamtx", "hostname": "mediamtx",
"isMain": true, "isMain": true,
"internalPort": "8889", "internalPort": "8889",
"environment": [ "environment": {
{ "MTX_PROTOCOLS": "tcp",
"key": "MTX_PROTOCOLS", "MTX_LOGLEVEL": "info",
"value": "tcp" "MTX_LOGDESTINATIONS": "stdout",
}, "MTX_RTSPADDRESS": ":8554",
{ "MTX_RTMPADDRESS": ":1935",
"key": "MTX_LOGLEVEL", "MTX_HLSADDRESS": ":8888",
"value": "info" "MTX_WEBRTCADDRESS": ":8889",
}, "MTX_SRTADDRESS": ":8890",
{ "MTX_APIADDRESS": ":9997",
"key": "MTX_LOGDESTINATIONS", "MTX_METRICSADDRESS": ":9998",
"value": "stdout" "MTX_AUTHINTERNALUSERS": "${MTX_API_USERNAME:+${MTX_API_USERNAME}:${MTX_API_PASSWORD}}",
}, "MTX_PATHDEFAULTS_RECORD": "${MTX_RECORD_ENABLED:-false}",
{ "MTX_PATHDEFAULTS_RECORDPATH": "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f",
"key": "MTX_RTSPADDRESS", "MTX_PATHDEFAULTS_RECORDFORMAT": "fmp4"
"value": ":8554" },
},
{
"key": "MTX_RTMPADDRESS",
"value": ":1935"
},
{
"key": "MTX_HLSADDRESS",
"value": ":8888"
},
{
"key": "MTX_WEBRTCADDRESS",
"value": ":8889"
},
{
"key": "MTX_SRTADDRESS",
"value": ":8890"
},
{
"key": "MTX_APIADDRESS",
"value": ":9997"
},
{
"key": "MTX_METRICSADDRESS",
"value": ":9998"
},
{
"key": "MTX_AUTHINTERNALUSERS",
"value": "${MTX_API_USERNAME:+${MTX_API_USERNAME}:${MTX_API_PASSWORD}}"
},
{
"key": "MTX_PATHDEFAULTS_RECORD",
"value": "${MTX_RECORD_ENABLED:-false}"
},
{
"key": "MTX_PATHDEFAULTS_RECORDPATH",
"value": "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f"
},
{
"key": "MTX_PATHDEFAULTS_RECORDFORMAT",
"value": "fmp4"
}
],
"addPorts": [ "addPorts": [
{ {
"containerPort": 8554, "containerPort": 8554,

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

+4 -10
View File
@@ -29,16 +29,10 @@
"readOnly": true "readOnly": true
} }
], ],
"environment": [ "environment": {
{ "NGINX_HOST": "${NGINX_SERVER_NAME:-localhost}",
"key": "NGINX_HOST", "TZ": "${TZ:-Europe/Paris}"
"value": "${NGINX_SERVER_NAME:-localhost}" },
},
{
"key": "TZ",
"value": "${TZ:-Europe/Paris}"
}
],
"healthCheck": { "healthCheck": {
"test": "curl --fail http://localhost:80 || exit 1", "test": "curl --fail http://localhost:80 || exit 1",
"interval": "30s", "interval": "30s",
+47 -161
View File
@@ -9,12 +9,9 @@
"sh", "sh",
"/scripts/init-certs.sh" "/scripts/init-certs.sh"
], ],
"environment": [ "environment": {
{ "CERT_TOOL_VERSION": "4.14"
"key": "CERT_TOOL_VERSION", },
"value": "4.14"
}
],
"volumes": [ "volumes": [
{ {
"hostPath": "${APP_DATA_DIR}/data/config", "hostPath": "${APP_DATA_DIR}/data/config",
@@ -74,48 +71,18 @@
"condition": "service_healthy" "condition": "service_healthy"
} }
}, },
"environment": [ "environment": {
{ "OPENSEARCH_JAVA_OPTS": "-Xms1g -Xmx1g -Dlog4j2.formatMsgNoLookups=true",
"key": "OPENSEARCH_JAVA_OPTS", "DISABLE_INSTALL_DEMO_CONFIG": "true",
"value": "-Xms1g -Xmx1g -Dlog4j2.formatMsgNoLookups=true" "bootstrap.memory_lock": "true",
}, "network.host": "wazuh.indexer",
{ "node.name": "wazuh.indexer",
"key": "DISABLE_INSTALL_DEMO_CONFIG", "cluster.initial_cluster_manager_nodes": "wazuh.indexer",
"value": "true" "node.max_local_storage_nodes": "1",
}, "plugins.security.allow_default_init_securityindex": "true",
{ "NODES_DN": "CN=wazuh.indexer,OU=Wazuh,O=Wazuh,L=California,C=US",
"key": "bootstrap.memory_lock", "plugins.security.ssl.http.clientauth_mode": "OPTIONAL"
"value": "true" },
},
{
"key": "network.host",
"value": "wazuh.indexer"
},
{
"key": "node.name",
"value": "wazuh.indexer"
},
{
"key": "cluster.initial_cluster_manager_nodes",
"value": "wazuh.indexer"
},
{
"key": "node.max_local_storage_nodes",
"value": "1"
},
{
"key": "plugins.security.allow_default_init_securityindex",
"value": "true"
},
{
"key": "NODES_DN",
"value": "CN=wazuh.indexer,OU=Wazuh,O=Wazuh,L=California,C=US"
},
{
"key": "plugins.security.ssl.http.clientauth_mode",
"value": "OPTIONAL"
}
],
"ulimits": { "ulimits": {
"memlock": { "memlock": {
"soft": -1, "soft": -1,
@@ -209,64 +176,22 @@
"condition": "service_healthy" "condition": "service_healthy"
} }
}, },
"environment": [ "environment": {
{ "WAZUH_INDEXER_HOSTS": "wazuh.indexer:9200",
"key": "WAZUH_INDEXER_HOSTS", "WAZUH_NODE_NAME": "manager",
"value": "wazuh.indexer:9200" "WAZUH_NODE_TYPE": "master",
}, "WAZUH_CLUSTER_NODES": "wazuh.manager",
{ "WAZUH_CLUSTER_BIND_ADDR": "wazuh.manager",
"key": "WAZUH_NODE_NAME", "INDEXER_URL": "https://wazuh.indexer:9200",
"value": "manager" "INDEXER_USERNAME": "${INDEXER_USERNAME:-admin}",
}, "INDEXER_PASSWORD": "${INDEXER_PASSWORD:-admin}",
{ "FILEBEAT_SSL_VERIFICATION_MODE": "full",
"key": "WAZUH_NODE_TYPE", "SSL_CERTIFICATE_AUTHORITIES": "/var/ossec/etc/certs/root-ca.pem",
"value": "master" "SSL_CERTIFICATE": "/var/ossec/etc/certs/server.pem",
}, "SSL_KEY": "/var/ossec/etc/certs/server-key.pem",
{ "API_USERNAME": "wazuh-wui",
"key": "WAZUH_CLUSTER_NODES", "API_PASSWORD": "${API_PASSWORD:-MyS3cr37P450r.*-}"
"value": "wazuh.manager" },
},
{
"key": "WAZUH_CLUSTER_BIND_ADDR",
"value": "wazuh.manager"
},
{
"key": "INDEXER_URL",
"value": "https://wazuh.indexer:9200"
},
{
"key": "INDEXER_USERNAME",
"value": "${INDEXER_USERNAME:-admin}"
},
{
"key": "INDEXER_PASSWORD",
"value": "${INDEXER_PASSWORD:-admin}"
},
{
"key": "FILEBEAT_SSL_VERIFICATION_MODE",
"value": "full"
},
{
"key": "SSL_CERTIFICATE_AUTHORITIES",
"value": "/var/ossec/etc/certs/root-ca.pem"
},
{
"key": "SSL_CERTIFICATE",
"value": "/var/ossec/etc/certs/server.pem"
},
{
"key": "SSL_KEY",
"value": "/var/ossec/etc/certs/server-key.pem"
},
{
"key": "API_USERNAME",
"value": "wazuh-wui"
},
{
"key": "API_PASSWORD",
"value": "${API_PASSWORD:-MyS3cr37P450r.*-}"
}
],
"ulimits": { "ulimits": {
"memlock": { "memlock": {
"soft": -1, "soft": -1,
@@ -368,60 +293,21 @@
"condition": "service_healthy" "condition": "service_healthy"
} }
}, },
"environment": [ "environment": {
{ "SERVER_HOST": "0.0.0.0",
"key": "SERVER_HOST", "OPENSEARCH_HOSTS": "https://wazuh.indexer:9200",
"value": "0.0.0.0" "SERVER_SSL_ENABLED": "true",
}, "INDEXER_USERNAME": "${INDEXER_USERNAME:-admin}",
{ "INDEXER_PASSWORD": "${INDEXER_PASSWORD:-admin}",
"key": "OPENSEARCH_HOSTS", "WAZUH_API_URL": "https://wazuh.manager",
"value": "https://wazuh.indexer:9200" "DASHBOARD_USERNAME": "${DASHBOARD_USERNAME:-kibanaserver}",
}, "DASHBOARD_PASSWORD": "${DASHBOARD_PASSWORD:-kibanaserver}",
{ "API_USERNAME": "wazuh-wui",
"key": "SERVER_SSL_ENABLED", "API_PASSWORD": "${API_PASSWORD:-MyS3cr37P450r.*-}",
"value": "true" "SERVER_SSL_CERTIFICATE": "/usr/share/wazuh-dashboard/config/certs/dashboard.pem",
}, "SERVER_SSL_KEY": "/usr/share/wazuh-dashboard/config/certs/dashboard-key.pem",
{ "OPENSEARCH_SSL_CERTIFICATE_AUTHORITIES": "/usr/share/wazuh-dashboard/config/certs/root-ca.pem"
"key": "INDEXER_USERNAME", },
"value": "${INDEXER_USERNAME:-admin}"
},
{
"key": "INDEXER_PASSWORD",
"value": "${INDEXER_PASSWORD:-admin}"
},
{
"key": "WAZUH_API_URL",
"value": "https://wazuh.manager"
},
{
"key": "DASHBOARD_USERNAME",
"value": "${DASHBOARD_USERNAME:-kibanaserver}"
},
{
"key": "DASHBOARD_PASSWORD",
"value": "${DASHBOARD_PASSWORD:-kibanaserver}"
},
{
"key": "API_USERNAME",
"value": "wazuh-wui"
},
{
"key": "API_PASSWORD",
"value": "${API_PASSWORD:-MyS3cr37P450r.*-}"
},
{
"key": "SERVER_SSL_CERTIFICATE",
"value": "/usr/share/wazuh-dashboard/config/certs/dashboard.pem"
},
{
"key": "SERVER_SSL_KEY",
"value": "/usr/share/wazuh-dashboard/config/certs/dashboard-key.pem"
},
{
"key": "OPENSEARCH_SSL_CERTIFICATE_AUTHORITIES",
"value": "/usr/share/wazuh-dashboard/config/certs/root-ca.pem"
}
],
"volumes": [ "volumes": [
{ {
"hostPath": "${APP_DATA_DIR}/data/config/wazuh_ssl_certs", "hostPath": "${APP_DATA_DIR}/data/config/wazuh_ssl_certs",
@@ -462,4 +348,4 @@
} }
} }
] ]
} }
BIN
View File
Binary file not shown.
+2 -1
View File
@@ -12,7 +12,8 @@
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
"@types/node": "^22.14.1" "@types/node": "^22.14.1",
"yaml": "2"
}, },
"dependencies": { "dependencies": {
"@runtipi/common": "^0.8.0", "@runtipi/common": "^0.8.0",