feat: add extra Docker networks to container config
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:
@@ -147,6 +147,16 @@ public class DeploymentExecutor {
|
|||||||
additionalNets.add(envNet);
|
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 ===
|
// === START REPLICAS ===
|
||||||
updateStage(deployment.id(), DeployStage.START_REPLICAS);
|
updateStage(deployment.id(), DeployStage.START_REPLICAS);
|
||||||
|
|
||||||
@@ -370,6 +380,7 @@ public class DeploymentExecutor {
|
|||||||
map.put("deploymentStrategy", config.deploymentStrategy());
|
map.put("deploymentStrategy", config.deploymentStrategy());
|
||||||
map.put("runtimeType", config.runtimeType());
|
map.put("runtimeType", config.runtimeType());
|
||||||
map.put("customArgs", config.customArgs());
|
map.put("customArgs", config.customArgs());
|
||||||
|
map.put("extraNetworks", config.extraNetworks());
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ public final class ConfigMerger {
|
|||||||
boolVal(appConfig, envConfig, "routeControlEnabled", true),
|
boolVal(appConfig, envConfig, "routeControlEnabled", true),
|
||||||
boolVal(appConfig, envConfig, "replayEnabled", true),
|
boolVal(appConfig, envConfig, "replayEnabled", true),
|
||||||
stringVal(appConfig, envConfig, "runtimeType", "auto"),
|
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();
|
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")
|
@SuppressWarnings("unchecked")
|
||||||
private static Map<String, String> stringMap(Map<String, Object> app, Map<String, Object> env, String key) {
|
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);
|
Object val = app.containsKey(key) ? app.get(key) : env.get(key);
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ public record ResolvedContainerConfig(
|
|||||||
boolean routeControlEnabled,
|
boolean routeControlEnabled,
|
||||||
boolean replayEnabled,
|
boolean replayEnabled,
|
||||||
String runtimeType,
|
String runtimeType,
|
||||||
String customArgs
|
String customArgs,
|
||||||
|
List<String> extraNetworks
|
||||||
) {
|
) {
|
||||||
public long memoryLimitBytes() {
|
public long memoryLimitBytes() {
|
||||||
return (long) memoryLimitMb * 1024 * 1024;
|
return (long) memoryLimitMb * 1024 * 1024;
|
||||||
|
|||||||
@@ -211,6 +211,8 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
|
|||||||
const [sslOffloading, setSslOffloading] = useState(true);
|
const [sslOffloading, setSslOffloading] = useState(true);
|
||||||
const [runtimeType, setRuntimeType] = useState(String(defaults.runtimeType ?? 'auto'));
|
const [runtimeType, setRuntimeType] = useState(String(defaults.runtimeType ?? 'auto'));
|
||||||
const [customArgs, setCustomArgs] = useState(String(defaults.customArgs ?? ''));
|
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 [configTab, setConfigTab] = useState<'monitoring' | 'resources' | 'variables'>('monitoring');
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
@@ -226,6 +228,7 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
|
|||||||
setPorts(Array.isArray(d.exposedPorts) ? d.exposedPorts as number[] : []);
|
setPorts(Array.isArray(d.exposedPorts) ? d.exposedPorts as number[] : []);
|
||||||
setRuntimeType(String(d.runtimeType ?? 'auto'));
|
setRuntimeType(String(d.runtimeType ?? 'auto'));
|
||||||
setCustomArgs(String(d.customArgs ?? ''));
|
setCustomArgs(String(d.customArgs ?? ''));
|
||||||
|
setExtraNetworks(Array.isArray(d.extraNetworks) ? d.extraNetworks as string[] : []);
|
||||||
}, [envId, environments]);
|
}, [envId, environments]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -267,6 +270,7 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
|
|||||||
sslOffloading: sslOffloading,
|
sslOffloading: sslOffloading,
|
||||||
runtimeType: runtimeType,
|
runtimeType: runtimeType,
|
||||||
customArgs: customArgs || null,
|
customArgs: customArgs || null,
|
||||||
|
extraNetworks: extraNetworks,
|
||||||
};
|
};
|
||||||
await updateContainerConfig.mutateAsync({ appId: app.slug, config: containerConfig });
|
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} />
|
<Toggle checked={sslOffloading} onChange={() => !busy && setSslOffloading(!sslOffloading)} disabled={busy} />
|
||||||
<span className={sslOffloading ? styles.toggleEnabled : styles.toggleDisabled}>{sslOffloading ? 'Enabled' : 'Disabled'}</span>
|
<span className={sslOffloading ? styles.toggleEnabled : styles.toggleDisabled}>{sslOffloading ? 'Enabled' : 'Disabled'}</span>
|
||||||
</div>
|
</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))}>×</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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -829,6 +850,8 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
|
|||||||
const [sslOffloading, setSslOffloading] = useState(true);
|
const [sslOffloading, setSslOffloading] = useState(true);
|
||||||
const [runtimeType, setRuntimeType] = useState(String(merged.runtimeType ?? 'auto'));
|
const [runtimeType, setRuntimeType] = useState(String(merged.runtimeType ?? 'auto'));
|
||||||
const [customArgs, setCustomArgs] = useState(String(merged.customArgs ?? ''));
|
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
|
// Versions query for runtime detection hints
|
||||||
const { data: versions = [] } = useAppVersions(app.slug);
|
const { data: versions = [] } = useAppVersions(app.slug);
|
||||||
@@ -862,6 +885,7 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
|
|||||||
setSslOffloading(merged.sslOffloading !== false);
|
setSslOffloading(merged.sslOffloading !== false);
|
||||||
setRuntimeType(String(merged.runtimeType ?? 'auto'));
|
setRuntimeType(String(merged.runtimeType ?? 'auto'));
|
||||||
setCustomArgs(String(merged.customArgs ?? ''));
|
setCustomArgs(String(merged.customArgs ?? ''));
|
||||||
|
setExtraNetworks(Array.isArray(merged.extraNetworks) ? merged.extraNetworks as string[] : []);
|
||||||
}, [agentConfig, merged]);
|
}, [agentConfig, merged]);
|
||||||
|
|
||||||
useEffect(() => { syncFromServer(); }, [syncFromServer]);
|
useEffect(() => { syncFromServer(); }, [syncFromServer]);
|
||||||
@@ -913,6 +937,7 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
|
|||||||
sslOffloading: sslOffloading,
|
sslOffloading: sslOffloading,
|
||||||
runtimeType: runtimeType,
|
runtimeType: runtimeType,
|
||||||
customArgs: customArgs || null,
|
customArgs: customArgs || null,
|
||||||
|
extraNetworks: extraNetworks,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await updateContainerConfig.mutateAsync({ appId: app.slug, config: containerConfig });
|
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} />
|
<Toggle checked={sslOffloading} onChange={() => editing && setSslOffloading(!sslOffloading)} disabled={!editing} />
|
||||||
<span className={sslOffloading ? styles.toggleEnabled : styles.toggleDisabled}>{sslOffloading ? 'Enabled' : 'Disabled'}</span>
|
<span className={sslOffloading ? styles.toggleEnabled : styles.toggleDisabled}>{sslOffloading ? 'Enabled' : 'Disabled'}</span>
|
||||||
</div>
|
</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))}>×</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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user