From bba4f9a41953464c136c0544e6a03043f2e67224 Mon Sep 17 00:00:00 2001 From: "openai-code-agent[bot]" <242516109+Codex@users.noreply.github.com> Date: Wed, 6 May 2026 10:07:44 +0000 Subject: [PATCH 1/2] Initial plan From 05269489d7d2407ff04baf89bd3693760d6cd062 Mon Sep 17 00:00:00 2001 From: "openai-code-agent[bot]" <242516109+Codex@users.noreply.github.com> Date: Wed, 6 May 2026 10:25:51 +0000 Subject: [PATCH 2/2] Prevent duplicate nginx-proxy-manager deployment when proxy.type is set Co-authored-by: vsilent <42473+vsilent@users.noreply.github.com> --- src/cli/stacker_client.rs | 90 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/src/cli/stacker_client.rs b/src/cli/stacker_client.rs index 7fd0b30f..ea7f6663 100644 --- a/src/cli/stacker_client.rs +++ b/src/cli/stacker_client.rs @@ -2207,8 +2207,32 @@ pub fn build_project_body(config: &StackerConfig) -> serde_json::Value { web_apps.push(main_app); } + fn is_nginx_proxy_manager_service(svc: &crate::cli::config_parser::ServiceDefinition) -> bool { + let name = svc.name.to_lowercase(); + let image = svc.image.to_lowercase(); + + name == "nginx_proxy_manager" + || name == "npm" + || name == "proxy-manager" + || name == "nginx-proxy-manager" + || image.contains("nginx-proxy-manager") + || image.contains("jc21/nginx-proxy-manager") + } + // Include additional services for svc in &config.services { + // `proxy.type: nginx|nginx-proxy-manager` is deployed via the dedicated + // `nginx_proxy_manager` feature/role. If a configure step accidentally + // injects an NPM service into `services:`, deploying it here would start + // a second NPM container and collide on ports 80/81/443. + if matches!( + config.proxy.proxy_type, + crate::cli::config_parser::ProxyType::Nginx + | crate::cli::config_parser::ProxyType::NginxProxyManager + ) && is_nginx_proxy_manager_service(svc) + { + continue; + } web_apps.push(service_to_app_json(svc, &network_ids)); } @@ -2392,7 +2416,7 @@ pub fn build_deploy_form(config: &StackerConfig) -> serde_json::Value { if let Some(stack_obj) = form.get_mut("stack").and_then(|v| v.as_object_mut()) { let features = stack_obj .entry("extended_features") - .or_insert_with(|| serde_json::json!([])); + .or_insert_with(|| serde_json::json!([])); if let Some(arr) = features.as_array_mut() { let npm = serde_json::Value::String("nginx_proxy_manager".to_string()); if !arr.contains(&npm) { @@ -2455,6 +2479,7 @@ pub fn build_deploy_form(config: &StackerConfig) -> serde_json::Value { #[cfg(test)] mod tests { use super::*; + use std::collections::HashMap; #[test] fn test_build_deploy_form_defaults() { @@ -2639,6 +2664,69 @@ mod tests { assert_eq!(ports.len(), 3); } + #[test] + fn bdd_reconfigure_existing_nginx_proxy_manager_proxy_does_not_duplicate_features() { + use crate::cli::config_parser::{DeployTarget, ProxyConfig, ProxyType, ServiceDefinition}; + + let given_existing_npm_service = ServiceDefinition { + name: "nginx_proxy_manager".to_string(), + image: "jc21/nginx-proxy-manager:latest".to_string(), + ports: vec!["80:80".to_string(), "443:443".to_string(), "81:81".to_string()], + environment: HashMap::new(), + volumes: vec![], + depends_on: vec![], + }; + + let given_config = crate::cli::config_parser::ConfigBuilder::new() + .name("myproject") + .deploy_target(DeployTarget::Cloud) + .proxy(ProxyConfig { + proxy_type: ProxyType::NginxProxyManager, + auto_detect: true, + domains: vec![], + config: None, + }) + .add_service(given_existing_npm_service) + .build() + .unwrap(); + + let when_project_body = build_project_body(&given_config); + let when_deploy_form = build_deploy_form(&given_config); + + let then_project_web = when_project_body["custom"]["web"] + .as_array() + .expect("custom.web must be an array"); + assert!( + then_project_web + .iter() + .all(|app| app["code"] != "nginx_proxy_manager"), + "should not deploy nginx_proxy_manager as a web service when proxy.type already provisions it as a feature: {:?}", + then_project_web + ); + + let then_project_features = when_project_body["custom"]["feature"] + .as_array() + .expect("custom.feature must be an array"); + assert!( + then_project_features + .iter() + .filter(|f| f["code"] == "nginx_proxy_manager") + .count() + == 1, + "should include exactly one nginx_proxy_manager feature: {:?}", + then_project_features + ); + + let then_extended_features = when_deploy_form["stack"]["extended_features"] + .as_array() + .expect("stack.extended_features must be an array"); + assert!( + then_extended_features.contains(&serde_json::json!("nginx_proxy_manager")), + "should inject nginx_proxy_manager into stack.extended_features when proxy.type requires it: {:?}", + then_extended_features + ); + } + #[test] fn test_build_project_body_with_status_panel() { let config = crate::cli::config_parser::ConfigBuilder::new()