Skip to content

Commit de0d6c9

Browse files
Export persistDappmanagerSettings function and add unit tests for environment variable persistence
1 parent e5850b2 commit de0d6c9

File tree

2 files changed

+294
-1
lines changed

2 files changed

+294
-1
lines changed

packages/installer/src/installer/getInstallerPackageData.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ function getInstallerPackageData(
133133
* - DAPPNODE_CORE_DIR is not lost during upgrades
134134
* - The DNCORE volume bind mount uses the correct host path from DAPPNODE_CORE_DIR
135135
*/
136-
function persistDappmanagerSettings(compose: ComposeEditor, dnpName: string, isCore: boolean): void {
136+
export function persistDappmanagerSettings(compose: ComposeEditor, dnpName: string, isCore: boolean): void {
137137
if (dnpName !== params.dappmanagerDnpName) return;
138138

139139
// Read the currently installed compose to get persisted env values
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
import "mocha";
2+
import { expect } from "chai";
3+
import fs from "fs";
4+
import path from "path";
5+
import { ComposeEditor } from "@dappnode/dockercompose";
6+
import { Compose } from "@dappnode/types";
7+
import { yamlDump, getDockerComposePath, parseEnvironment } from "@dappnode/utils";
8+
import { params } from "@dappnode/params";
9+
import { persistDappmanagerSettings } from "../../../src/installer/getInstallerPackageData.js";
10+
11+
const dnpName = params.dappmanagerDnpName;
12+
const isCore = true;
13+
14+
/**
15+
* Helper to write a compose file to the path ComposeFileEditor expects
16+
*/
17+
function writeInstalledCompose(compose: Compose): string {
18+
const composePath = getDockerComposePath(dnpName, isCore);
19+
const dir = path.dirname(composePath);
20+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
21+
fs.writeFileSync(composePath, yamlDump(compose));
22+
return composePath;
23+
}
24+
25+
/**
26+
* Cleanup the installed compose file
27+
*/
28+
function removeInstalledCompose(): void {
29+
const composePath = getDockerComposePath(dnpName, isCore);
30+
if (fs.existsSync(composePath)) fs.unlinkSync(composePath);
31+
}
32+
33+
/**
34+
* Build a minimal dappmanager compose
35+
*/
36+
function buildDappmanagerCompose(overrides?: {
37+
environment?: Record<string, string> | string[];
38+
volumes?: string[];
39+
}): Compose {
40+
return {
41+
version: "3.5",
42+
services: {
43+
"dappmanager.dnp.dappnode.eth": {
44+
image: "dappmanager.dnp.dappnode.eth:0.2.71",
45+
container_name: "DAppNodeCore-dappmanager.dnp.dappnode.eth",
46+
volumes: overrides?.volumes ?? [
47+
"/run/dbus/system_bus_socket:/run/dbus/system_bus_socket",
48+
"dappmanagerdnpdappnodeeth_data:/usr/src/app/dnp_repo/",
49+
"/usr/src/dappnode/DNCORE:/usr/src/app/DNCORE",
50+
"/var/run/docker.sock:/var/run/docker.sock"
51+
],
52+
environment: overrides?.environment ?? {
53+
LOG_LEVEL: "info"
54+
}
55+
}
56+
},
57+
networks: {
58+
dncore_network: { external: true }
59+
}
60+
};
61+
}
62+
63+
describe("persistDappmanagerSettings", () => {
64+
afterEach(() => {
65+
removeInstalledCompose();
66+
});
67+
68+
it("Should be a no-op for non-dappmanager packages", () => {
69+
const compose = new ComposeEditor(buildDappmanagerCompose(), { dnpName });
70+
const before = JSON.stringify(compose.compose);
71+
72+
// Call with a different dnpName
73+
persistDappmanagerSettings(compose, "other.dnp.dappnode.eth", isCore);
74+
75+
expect(JSON.stringify(compose.compose)).to.equal(before);
76+
});
77+
78+
it("Should be a no-op when no installed compose exists (fresh install)", () => {
79+
// Don't write any compose to disk
80+
removeInstalledCompose();
81+
82+
const newCompose = buildDappmanagerCompose();
83+
const compose = new ComposeEditor(newCompose, { dnpName });
84+
const before = JSON.stringify(compose.compose);
85+
86+
// Should not throw
87+
persistDappmanagerSettings(compose, dnpName, isCore);
88+
89+
expect(JSON.stringify(compose.compose)).to.equal(before);
90+
});
91+
92+
it("Should persist DISABLE_HOST_SCRIPTS from installed compose", () => {
93+
// Write installed compose with DISABLE_HOST_SCRIPTS=true
94+
writeInstalledCompose(
95+
buildDappmanagerCompose({
96+
environment: {
97+
LOG_LEVEL: "info",
98+
DISABLE_HOST_SCRIPTS: "true"
99+
}
100+
})
101+
);
102+
103+
// New compose does NOT have DISABLE_HOST_SCRIPTS
104+
const newCompose = buildDappmanagerCompose({
105+
environment: { LOG_LEVEL: "info" }
106+
});
107+
const compose = new ComposeEditor(newCompose, { dnpName });
108+
109+
persistDappmanagerSettings(compose, dnpName, isCore);
110+
111+
const service = compose.compose.services["dappmanager.dnp.dappnode.eth"];
112+
const envs = parseEnvironment(service.environment || []);
113+
expect(envs["DISABLE_HOST_SCRIPTS"]).to.equal("true");
114+
// Original env preserved
115+
expect(envs["LOG_LEVEL"]).to.equal("info");
116+
});
117+
118+
it("Should persist DAPPNODE_CORE_DIR from installed compose", () => {
119+
writeInstalledCompose(
120+
buildDappmanagerCompose({
121+
environment: {
122+
LOG_LEVEL: "info",
123+
DAPPNODE_CORE_DIR: "/custom/path/DNCORE"
124+
}
125+
})
126+
);
127+
128+
const newCompose = buildDappmanagerCompose({
129+
environment: { LOG_LEVEL: "info" }
130+
});
131+
const compose = new ComposeEditor(newCompose, { dnpName });
132+
133+
persistDappmanagerSettings(compose, dnpName, isCore);
134+
135+
const service = compose.compose.services["dappmanager.dnp.dappnode.eth"];
136+
const envs = parseEnvironment(service.environment || []);
137+
expect(envs["DAPPNODE_CORE_DIR"]).to.equal("/custom/path/DNCORE");
138+
});
139+
140+
it("Should persist both DISABLE_HOST_SCRIPTS and DAPPNODE_CORE_DIR", () => {
141+
writeInstalledCompose(
142+
buildDappmanagerCompose({
143+
environment: {
144+
LOG_LEVEL: "info",
145+
DISABLE_HOST_SCRIPTS: "true",
146+
DAPPNODE_CORE_DIR: "/custom/path/DNCORE"
147+
}
148+
})
149+
);
150+
151+
const newCompose = buildDappmanagerCompose({
152+
environment: { LOG_LEVEL: "debug" }
153+
});
154+
const compose = new ComposeEditor(newCompose, { dnpName });
155+
156+
persistDappmanagerSettings(compose, dnpName, isCore);
157+
158+
const service = compose.compose.services["dappmanager.dnp.dappnode.eth"];
159+
const envs = parseEnvironment(service.environment || []);
160+
expect(envs["DISABLE_HOST_SCRIPTS"]).to.equal("true");
161+
expect(envs["DAPPNODE_CORE_DIR"]).to.equal("/custom/path/DNCORE");
162+
// Original env kept
163+
expect(envs["LOG_LEVEL"]).to.equal("debug");
164+
});
165+
166+
it("Should update DNCORE volume host path to match DAPPNODE_CORE_DIR", () => {
167+
const customDir = "/custom/path/DNCORE";
168+
writeInstalledCompose(
169+
buildDappmanagerCompose({
170+
environment: {
171+
DAPPNODE_CORE_DIR: customDir
172+
}
173+
})
174+
);
175+
176+
// New compose has default volume path
177+
const newCompose = buildDappmanagerCompose({
178+
environment: { LOG_LEVEL: "info" },
179+
volumes: [
180+
"dappmanagerdnpdappnodeeth_data:/usr/src/app/dnp_repo/",
181+
"/usr/src/dappnode/DNCORE:/usr/src/app/DNCORE",
182+
"/var/run/docker.sock:/var/run/docker.sock"
183+
]
184+
});
185+
const compose = new ComposeEditor(newCompose, { dnpName });
186+
187+
persistDappmanagerSettings(compose, dnpName, isCore);
188+
189+
const service = compose.compose.services["dappmanager.dnp.dappnode.eth"];
190+
// The DNCORE volume should now point to the custom dir
191+
expect(service.volumes).to.include(`${customDir}:/usr/src/app/DNCORE`);
192+
// Other volumes untouched
193+
expect(service.volumes).to.include("dappmanagerdnpdappnodeeth_data:/usr/src/app/dnp_repo");
194+
expect(service.volumes).to.include("/var/run/docker.sock:/var/run/docker.sock");
195+
});
196+
197+
it("Should not modify volumes when DAPPNODE_CORE_DIR is not set in installed compose", () => {
198+
// Installed compose with DISABLE_HOST_SCRIPTS but no DAPPNODE_CORE_DIR
199+
writeInstalledCompose(
200+
buildDappmanagerCompose({
201+
environment: {
202+
DISABLE_HOST_SCRIPTS: "true"
203+
}
204+
})
205+
);
206+
207+
const volumes = [
208+
"dappmanagerdnpdappnodeeth_data:/usr/src/app/dnp_repo/",
209+
"/usr/src/dappnode/DNCORE:/usr/src/app/DNCORE",
210+
"/var/run/docker.sock:/var/run/docker.sock"
211+
];
212+
const newCompose = buildDappmanagerCompose({
213+
environment: { LOG_LEVEL: "info" },
214+
volumes
215+
});
216+
const compose = new ComposeEditor(newCompose, { dnpName });
217+
218+
persistDappmanagerSettings(compose, dnpName, isCore);
219+
220+
const service = compose.compose.services["dappmanager.dnp.dappnode.eth"];
221+
// Default DNCORE path preserved
222+
expect(service.volumes).to.include("/usr/src/dappnode/DNCORE:/usr/src/app/DNCORE");
223+
});
224+
225+
it("Should be a no-op when installed compose has no relevant envs", () => {
226+
// Installed compose with no special envs
227+
writeInstalledCompose(
228+
buildDappmanagerCompose({
229+
environment: {
230+
LOG_LEVEL: "info",
231+
SOME_OTHER_VAR: "value"
232+
}
233+
})
234+
);
235+
236+
const newCompose = buildDappmanagerCompose({
237+
environment: { LOG_LEVEL: "debug" }
238+
});
239+
const compose = new ComposeEditor(newCompose, { dnpName });
240+
const envsBefore = parseEnvironment(compose.compose.services["dappmanager.dnp.dappnode.eth"].environment || []);
241+
242+
persistDappmanagerSettings(compose, dnpName, isCore);
243+
244+
const envsAfter = parseEnvironment(compose.compose.services["dappmanager.dnp.dappnode.eth"].environment || []);
245+
expect(envsAfter).to.deep.equal(envsBefore);
246+
});
247+
248+
it("Should handle installed compose with environment as array format", () => {
249+
writeInstalledCompose(
250+
buildDappmanagerCompose({
251+
environment: ["LOG_LEVEL=info", "DISABLE_HOST_SCRIPTS=true", "DAPPNODE_CORE_DIR=/custom/dir"]
252+
})
253+
);
254+
255+
const newCompose = buildDappmanagerCompose({
256+
environment: { LOG_LEVEL: "info" }
257+
});
258+
const compose = new ComposeEditor(newCompose, { dnpName });
259+
260+
persistDappmanagerSettings(compose, dnpName, isCore);
261+
262+
const service = compose.compose.services["dappmanager.dnp.dappnode.eth"];
263+
const envs = parseEnvironment(service.environment || []);
264+
expect(envs["DISABLE_HOST_SCRIPTS"]).to.equal("true");
265+
expect(envs["DAPPNODE_CORE_DIR"]).to.equal("/custom/dir");
266+
});
267+
268+
it("Should not overwrite DNCORE volume if no volume matches the container path", () => {
269+
const customDir = "/custom/path/DNCORE";
270+
writeInstalledCompose(
271+
buildDappmanagerCompose({
272+
environment: {
273+
DAPPNODE_CORE_DIR: customDir
274+
}
275+
})
276+
);
277+
278+
// New compose with no DNCORE volume
279+
const newCompose = buildDappmanagerCompose({
280+
environment: { LOG_LEVEL: "info" },
281+
volumes: ["dappmanagerdnpdappnodeeth_data:/usr/src/app/dnp_repo/", "/var/run/docker.sock:/var/run/docker.sock"]
282+
});
283+
const compose = new ComposeEditor(newCompose, { dnpName });
284+
285+
persistDappmanagerSettings(compose, dnpName, isCore);
286+
287+
const service = compose.compose.services["dappmanager.dnp.dappnode.eth"];
288+
// No DNCORE volume should be added — only existing volumes modified
289+
expect(service.volumes).to.have.lengthOf(2);
290+
expect(service.volumes).to.include("dappmanagerdnpdappnodeeth_data:/usr/src/app/dnp_repo");
291+
expect(service.volumes).to.include("/var/run/docker.sock:/var/run/docker.sock");
292+
});
293+
});

0 commit comments

Comments
 (0)