diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DeploymentExecutor.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DeploymentExecutor.java index f56e91aa..c35bbb60 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DeploymentExecutor.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DeploymentExecutor.java @@ -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; } } diff --git a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/ConfigMerger.java b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/ConfigMerger.java index cf7aa7d0..edd8b7b3 100644 --- a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/ConfigMerger.java +++ b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/ConfigMerger.java @@ -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 stringList(Map app, Map 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 stringMap(Map app, Map env, String key) { Object val = app.containsKey(key) ? app.get(key) : env.get(key); diff --git a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/ResolvedContainerConfig.java b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/ResolvedContainerConfig.java index 8d174951..b1674745 100644 --- a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/ResolvedContainerConfig.java +++ b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/ResolvedContainerConfig.java @@ -21,7 +21,8 @@ public record ResolvedContainerConfig( boolean routeControlEnabled, boolean replayEnabled, String runtimeType, - String customArgs + String customArgs, + List extraNetworks ) { public long memoryLimitBytes() { return (long) memoryLimitMb * 1024 * 1024; diff --git a/ui/src/pages/AppsTab/AppsTab.tsx b/ui/src/pages/AppsTab/AppsTab.tsx index da9d08e2..004cd8d1 100644 --- a/ui/src/pages/AppsTab/AppsTab.tsx +++ b/ui/src/pages/AppsTab/AppsTab.tsx @@ -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(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 !busy && setSslOffloading(!sslOffloading)} disabled={busy} /> {sslOffloading ? 'Enabled' : 'Disabled'} + + Extra Networks +
+
+ {extraNetworks.map((n) => ( + + {n} + + + ))} + 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(''); } } }} /> +
+ Additional Docker networks to join (e.g., monitoring, prometheus) +
)} @@ -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(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 editing && setSslOffloading(!sslOffloading)} disabled={!editing} /> {sslOffloading ? 'Enabled' : 'Disabled'} + + Extra Networks +
+
+ {extraNetworks.map((n) => ( + + {n} + + + ))} + 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(''); } } }} /> +
+ Additional Docker networks to join (e.g., monitoring, prometheus) +
)}