feat: add extra Docker networks to container config
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m23s
CI / docker (push) Successful in 1m7s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 38s

Apps can now join additional Docker networks (e.g., monitoring,
prometheus) configured via containerConfig.extraNetworks. Flows through
the 3-layer config merge. Networks are created if absent and containers
are connected during deployment. UI adds a pill-list field on the
Resources tab (both create and edit views).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-12 16:53:01 +02:00
parent 5b6543b167
commit be96336974
4 changed files with 68 additions and 2 deletions

View File

@@ -147,6 +147,16 @@ public class DeploymentExecutor {
additionalNets.add(envNet);
}
// User-configured extra networks (e.g., monitoring)
if (networkManager != null && config.extraNetworks() != null) {
for (String net : config.extraNetworks()) {
if (!net.isBlank() && !additionalNets.contains(net)) {
networkManager.ensureNetwork(net);
additionalNets.add(net);
}
}
}
// === START REPLICAS ===
updateStage(deployment.id(), DeployStage.START_REPLICAS);
@@ -370,6 +380,7 @@ public class DeploymentExecutor {
map.put("deploymentStrategy", config.deploymentStrategy());
map.put("runtimeType", config.runtimeType());
map.put("customArgs", config.customArgs());
map.put("extraNetworks", config.extraNetworks());
return map;
}
}

View File

@@ -32,7 +32,8 @@ public final class ConfigMerger {
boolVal(appConfig, envConfig, "routeControlEnabled", true),
boolVal(appConfig, envConfig, "replayEnabled", true),
stringVal(appConfig, envConfig, "runtimeType", "auto"),
stringVal(appConfig, envConfig, "customArgs", "")
stringVal(appConfig, envConfig, "customArgs", ""),
stringList(appConfig, envConfig, "extraNetworks")
);
}
@@ -78,6 +79,17 @@ public final class ConfigMerger {
return List.of();
}
private static List<String> stringList(Map<String, Object> app, Map<String, Object> env, String key) {
Object val = app.containsKey(key) ? app.get(key) : env.get(key);
if (val instanceof List<?> list) {
return list.stream()
.filter(String.class::isInstance)
.map(String.class::cast)
.toList();
}
return List.of();
}
@SuppressWarnings("unchecked")
private static Map<String, String> stringMap(Map<String, Object> app, Map<String, Object> env, String key) {
Object val = app.containsKey(key) ? app.get(key) : env.get(key);

View File

@@ -21,7 +21,8 @@ public record ResolvedContainerConfig(
boolean routeControlEnabled,
boolean replayEnabled,
String runtimeType,
String customArgs
String customArgs,
List<String> extraNetworks
) {
public long memoryLimitBytes() {
return (long) memoryLimitMb * 1024 * 1024;

View File

@@ -211,6 +211,8 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
const [sslOffloading, setSslOffloading] = useState(true);
const [runtimeType, setRuntimeType] = useState(String(defaults.runtimeType ?? 'auto'));
const [customArgs, setCustomArgs] = useState(String(defaults.customArgs ?? ''));
const [extraNetworks, setExtraNetworks] = useState<string[]>(Array.isArray(defaults.extraNetworks) ? defaults.extraNetworks as string[] : []);
const [newNetwork, setNewNetwork] = useState('');
const [configTab, setConfigTab] = useState<'monitoring' | 'resources' | 'variables'>('monitoring');
const [busy, setBusy] = useState(false);
@@ -226,6 +228,7 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
setPorts(Array.isArray(d.exposedPorts) ? d.exposedPorts as number[] : []);
setRuntimeType(String(d.runtimeType ?? 'auto'));
setCustomArgs(String(d.customArgs ?? ''));
setExtraNetworks(Array.isArray(d.extraNetworks) ? d.extraNetworks as string[] : []);
}, [envId, environments]);
useEffect(() => {
@@ -267,6 +270,7 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
sslOffloading: sslOffloading,
runtimeType: runtimeType,
customArgs: customArgs || null,
extraNetworks: extraNetworks,
};
await updateContainerConfig.mutateAsync({ appId: app.slug, config: containerConfig });
@@ -500,6 +504,23 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
<Toggle checked={sslOffloading} onChange={() => !busy && setSslOffloading(!sslOffloading)} disabled={busy} />
<span className={sslOffloading ? styles.toggleEnabled : styles.toggleDisabled}>{sslOffloading ? 'Enabled' : 'Disabled'}</span>
</div>
<span className={styles.configLabel}>Extra Networks</span>
<div>
<div className={styles.portPills}>
{extraNetworks.map((n) => (
<span key={n} className={styles.portPill}>
{n}
<button className={styles.portPillDelete} disabled={busy}
onClick={() => !busy && setExtraNetworks(extraNetworks.filter((x) => x !== n))}>&times;</button>
</span>
))}
<input className={styles.portAddInput} disabled={busy} placeholder="+ network" value={newNetwork}
onChange={(e) => setNewNetwork(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); const v = newNetwork.trim(); if (v && !extraNetworks.includes(v)) { setExtraNetworks([...extraNetworks, v]); setNewNetwork(''); } } }} />
</div>
<span className={styles.configHint}>Additional Docker networks to join (e.g., monitoring, prometheus)</span>
</div>
</div>
</div>
)}
@@ -829,6 +850,8 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
const [sslOffloading, setSslOffloading] = useState(true);
const [runtimeType, setRuntimeType] = useState(String(merged.runtimeType ?? 'auto'));
const [customArgs, setCustomArgs] = useState(String(merged.customArgs ?? ''));
const [extraNetworks, setExtraNetworks] = useState<string[]>(Array.isArray(merged.extraNetworks) ? merged.extraNetworks as string[] : []);
const [newNetwork, setNewNetwork] = useState('');
// Versions query for runtime detection hints
const { data: versions = [] } = useAppVersions(app.slug);
@@ -862,6 +885,7 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
setSslOffloading(merged.sslOffloading !== false);
setRuntimeType(String(merged.runtimeType ?? 'auto'));
setCustomArgs(String(merged.customArgs ?? ''));
setExtraNetworks(Array.isArray(merged.extraNetworks) ? merged.extraNetworks as string[] : []);
}, [agentConfig, merged]);
useEffect(() => { syncFromServer(); }, [syncFromServer]);
@@ -913,6 +937,7 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
sslOffloading: sslOffloading,
runtimeType: runtimeType,
customArgs: customArgs || null,
extraNetworks: extraNetworks,
};
try {
await updateContainerConfig.mutateAsync({ appId: app.slug, config: containerConfig });
@@ -1198,6 +1223,23 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
<Toggle checked={sslOffloading} onChange={() => editing && setSslOffloading(!sslOffloading)} disabled={!editing} />
<span className={sslOffloading ? styles.toggleEnabled : styles.toggleDisabled}>{sslOffloading ? 'Enabled' : 'Disabled'}</span>
</div>
<span className={styles.configLabel}>Extra Networks</span>
<div>
<div className={styles.portPills}>
{extraNetworks.map((n) => (
<span key={n} className={styles.portPill}>
{n}
<button className={styles.portPillDelete} disabled={!editing}
onClick={() => editing && setExtraNetworks(extraNetworks.filter((x) => x !== n))}>&times;</button>
</span>
))}
<input className={styles.portAddInput} disabled={!editing} placeholder="+ network" value={newNetwork}
onChange={(e) => setNewNetwork(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); const v = newNetwork.trim(); if (v && !extraNetworks.includes(v)) { setExtraNetworks([...extraNetworks, v]); setNewNetwork(''); } } }} />
</div>
<span className={styles.configHint}>Additional Docker networks to join (e.g., monitoring, prometheus)</span>
</div>
</div>
</div>
)}