Skip to content

Commit 6b09bc9

Browse files
Fix bind IP array format (#2202)
* fix ip array * fix testing * add unit testing * remove only
1 parent 702501c commit 6b09bc9

File tree

2 files changed

+185
-10
lines changed

2 files changed

+185
-10
lines changed

packages/dockerCompose/src/userSettings.ts

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import path from "path";
2-
import { mapValues, pick, omitBy, isObject, mergeWith, union } from "lodash-es";
2+
import { mapValues, pick, omitBy, isObject } from "lodash-es";
33
import {
44
parsePortMappings,
55
stringifyPortMappings,
@@ -10,7 +10,15 @@ import {
1010
parseServiceNetworks
1111
} from "./index.js";
1212
import { parseEnvironment } from "@dappnode/utils";
13-
import { Compose, ComposeServiceNetworks, PortMapping, UserSettings, VolumeMapping } from "@dappnode/types";
13+
import {
14+
Compose,
15+
ComposeServiceNetwork,
16+
ComposeServiceNetworks,
17+
ComposeServiceNetworksObj,
18+
PortMapping,
19+
UserSettings,
20+
VolumeMapping
21+
} from "@dappnode/types";
1422
import { cleanCompose, isOmitable } from "./clean.js";
1523
import { stringifyVolumeMappings } from "./volumes.js";
1624
import { readContainerLabels, writeDefaultsToLabels } from "./labelsDb.js";
@@ -145,7 +153,7 @@ export function applyUserSettings(
145153
const userSetEnvironment = (userSettings.environment || {})[serviceName] || {};
146154
const userSetPortMappings = (userSettings.portMappings || {})[serviceName] || {};
147155
const userSetLegacyBindVolumes = (userSettings.legacyBindVolumes || {})[serviceName] || {};
148-
const userSetNetworks = (userSettings.networks?.serviceNetworks || {})[serviceName] || {};
156+
const userSetNetworks = parseServiceNetworks((userSettings.networks?.serviceNetworks || {})[serviceName] || {});
149157

150158
// New values
151159
const nextEnvironment = mapValues(environment, (envValue, envName) => userSetEnvironment[envName] || envValue);
@@ -159,13 +167,64 @@ export function applyUserSettings(
159167
})
160168
);
161169

162-
// docker aliases must be unique
163-
// merge base and user networks, then remove any empty/nullish props (e.g. empty ipv4_address)
164-
const mergedNetworks = mergeWith(networks, userSetNetworks, (value1, value2) =>
165-
mergeWith(value1, value2, (subvalue1, subvalue2) => union(subvalue1, subvalue2))
166-
);
167-
const nextNetworks = mapValues(mergedNetworks, (config) =>
168-
omitBy(config, (v) => v == null || (Array.isArray(v) && v.length === 0))
170+
// Function to ensure types are correct and enforce Compose file compatibility
171+
const normalizeNetwork = (network: ComposeServiceNetwork): ComposeServiceNetwork => {
172+
const normalized: ComposeServiceNetwork = {};
173+
// Only add aliases if the array is non-empty
174+
if (Array.isArray(network.aliases) && network.aliases.length > 0) {
175+
normalized.aliases = network.aliases;
176+
}
177+
// Only add ipv4_address if it is defined (not undefined)
178+
if (network.ipv4_address) {
179+
normalized.ipv4_address = network.ipv4_address;
180+
}
181+
return normalized;
182+
};
183+
184+
// Function to merge base and user networks, ensuring proper type for ipv4_address and aliases
185+
const mergeNetworks = (
186+
base: ComposeServiceNetworksObj,
187+
user: ComposeServiceNetworksObj
188+
): ComposeServiceNetworksObj => {
189+
const merged: ComposeServiceNetworksObj = {};
190+
191+
// Iterate over the keys of the base networks
192+
for (const networkName in base) {
193+
merged[networkName] = { ...base[networkName] }; // Start with a shallow copy of the base network
194+
195+
if (user[networkName]) {
196+
// If the user has provided overrides for this network, merge them
197+
for (const subKey in user[networkName]) {
198+
if (subKey === "ipv4_address") {
199+
merged[networkName].ipv4_address = base[networkName].ipv4_address; // Always take base ipv4_address
200+
} else if (subKey === "aliases") {
201+
// Merge and deduplicate aliases
202+
const baseAliases = base[networkName].aliases || [];
203+
const userAliases = user[networkName].aliases || [];
204+
merged[networkName].aliases = Array.from(new Set([...baseAliases, ...userAliases]));
205+
}
206+
}
207+
} else {
208+
// If user doesn't have settings for the network, just copy the base network
209+
if (!merged[networkName].ipv4_address && base[networkName].ipv4_address) {
210+
merged[networkName].ipv4_address = base[networkName].ipv4_address;
211+
}
212+
}
213+
}
214+
215+
// Ensure that networks that don't have user settings but exist in base are still included
216+
for (const key in user) {
217+
if (!(key in base)) {
218+
merged[key] = { ...user[key] }; // Add user networks that don't exist in base
219+
}
220+
}
221+
222+
return merged;
223+
};
224+
225+
// Normalize the merged networks to ensure types are correct and compatible with Compose
226+
const nextNetworks = Object.fromEntries(
227+
Object.entries(mergeNetworks(networks, userSetNetworks)).map(([key, config]) => [key, normalizeNetwork(config)])
169228
);
170229

171230
// ##### <DEPRECATED> Kept for legacy compatibility

packages/dockerCompose/test/unit/userSettings.test.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,3 +584,119 @@ describe("applyUserSettings", () => {
584584
);
585585
});
586586
});
587+
588+
describe("applyUserSettings - network merging logic", () => {
589+
const serviceName = "test.dnp.dappnode.eth";
590+
const baseCompose = (baseIp?: string, baseAliases?: string[]) => ({
591+
version: "3.5",
592+
services: {
593+
[serviceName]: {
594+
image: "test:latest",
595+
container_name: "DAppNodePackage-test",
596+
networks: {
597+
net1: {
598+
...(baseIp !== undefined ? { ipv4_address: baseIp } : {}),
599+
...(baseAliases !== undefined ? { aliases: baseAliases } : {})
600+
}
601+
}
602+
}
603+
},
604+
networks: { net1: {} }
605+
});
606+
607+
const userSettings = (userIp?: string, userAliases?: string[]): UserSettings => ({
608+
networks: {
609+
rootNetworks: { net1: {} },
610+
serviceNetworks: {
611+
[serviceName]: {
612+
net1: {
613+
...(userIp !== undefined ? { ipv4_address: userIp } : {}),
614+
...(userAliases !== undefined ? { aliases: userAliases } : {})
615+
}
616+
}
617+
}
618+
}
619+
});
620+
621+
const applyUserSettingsTest: typeof applyUserSettings = (...args) => {
622+
const nextCompose = applyUserSettings(...args);
623+
for (const sName in nextCompose.services) delete nextCompose.services[sName].labels;
624+
return nextCompose;
625+
};
626+
627+
it("should merge aliases from both base and user networks, always resulting in an array", () => {
628+
const compose = baseCompose(undefined, ["base1", "base2"]);
629+
const settings = userSettings(undefined, ["user1", "base2"]);
630+
const result = applyUserSettingsTest(compose, settings, { dnpName: serviceName });
631+
expect(result.services[serviceName].networks).to.deep.equal({ net1: { aliases: ["base1", "base2", "user1"] } });
632+
});
633+
634+
it("should always use the ip from the base network, not from user network", () => {
635+
// base has ip, user has different ip
636+
const compose = baseCompose("10.0.0.1");
637+
const settings = userSettings("10.0.0.2");
638+
const result = applyUserSettingsTest(compose, settings, { dnpName: serviceName });
639+
expect(result.services[serviceName].networks).to.deep.equal({ net1: { ipv4_address: "10.0.0.1" } });
640+
});
641+
642+
it("should not have ip if base network does not define it, even if user network does", () => {
643+
const compose = baseCompose();
644+
const settings = userSettings("10.0.0.2");
645+
const result = applyUserSettingsTest(compose, settings, { dnpName: serviceName });
646+
expect(result.services[serviceName].networks).to.deep.equal({ net1: {} });
647+
});
648+
649+
it("should not have ip if neither base nor user network define it", () => {
650+
const compose = baseCompose();
651+
const settings = userSettings();
652+
const result = applyUserSettingsTest(compose, settings, { dnpName: serviceName });
653+
expect(result.services[serviceName].networks).to.deep.equal({ net1: {} });
654+
});
655+
656+
it("should include user network and base network if both are defined in each", () => {
657+
const serviceName = "test.dnp.dappnode.eth";
658+
const baseCompose = () => ({
659+
version: "3.5",
660+
services: {
661+
[serviceName]: {
662+
image: "test:latest",
663+
container_name: "DAppNodePackage-test",
664+
networks: {
665+
net1: {
666+
ipv4_address: "10.0.0.6",
667+
aliases: ["alias3", "alias4"]
668+
}
669+
}
670+
}
671+
},
672+
networks: { net1: {} }
673+
});
674+
675+
const userSettings = {
676+
networks: {
677+
rootNetworks: { net1: {}, net2: {} },
678+
serviceNetworks: {
679+
[serviceName]: {
680+
net2: {
681+
ipv4_address: "10.0.0.5",
682+
aliases: ["alias1", "alias2"]
683+
}
684+
}
685+
}
686+
}
687+
};
688+
689+
const compose = baseCompose();
690+
const result = applyUserSettingsTest(compose, userSettings, { dnpName: serviceName });
691+
expect(result.services[serviceName].networks).to.deep.equal({
692+
net1: {
693+
ipv4_address: "10.0.0.6",
694+
aliases: ["alias3", "alias4"]
695+
},
696+
net2: {
697+
ipv4_address: "10.0.0.5",
698+
aliases: ["alias1", "alias2"]
699+
}
700+
});
701+
});
702+
});

0 commit comments

Comments
 (0)