diff --git a/source/src/commands/validate-template.cpp b/source/src/commands/validate-template.cpp index 1aaf710..3971dbc 100644 --- a/source/src/commands/validate-template.cpp +++ b/source/src/commands/validate-template.cpp @@ -286,6 +286,83 @@ ValidationResult check_uninstall_preserves_volumes(const std::filesystem::path& return result; } +// Check if template exposes ports but lacks ports.sh +ValidationResult check_ports_script(const std::filesystem::path& template_path) { + ValidationResult result; + result.passed = true; + + bool has_ports_sh = std::filesystem::exists(template_path / "ports.sh"); + bool exposes_ports = false; + std::string detection_reason; + + // Check service.env for *_PORT variables + std::filesystem::path service_env_path = template_path / "config" / "service.env"; + if (std::filesystem::exists(service_env_path)) { + std::ifstream env_file(service_env_path); + std::string line; + std::regex port_var_pattern(R"(^[A-Z_]*PORT\s*=)"); + while (std::getline(env_file, line)) { + if (std::regex_search(line, port_var_pattern)) { + exposes_ports = true; + detection_reason = "Found PORT variable in service.env"; + break; + } + } + } + + // Check start.sh for -p flag + if (!exposes_ports) { + std::filesystem::path start_sh = template_path / "start.sh"; + if (std::filesystem::exists(start_sh)) { + std::ifstream file(start_sh); + std::string line; + std::regex port_flag_pattern(R"(-p\s+[\"']?\$?\{?[A-Za-z_]*\}?:\d+)"); + std::regex port_flag_pattern2(R"(-p\s+\d+:\d+)"); + while (std::getline(file, line)) { + if (std::regex_search(line, port_flag_pattern) || std::regex_search(line, port_flag_pattern2)) { + exposes_ports = true; + detection_reason = "Found -p port mapping in start.sh"; + break; + } + } + } + } + + // Check docker-compose files for ports: section + if (!exposes_ports) { + for (const auto& compose_name : {"docker-compose.yml", "docker-compose.yml.template"}) { + std::filesystem::path compose_path = template_path / compose_name; + if (std::filesystem::exists(compose_path)) { + std::ifstream file(compose_path); + std::string line; + std::regex ports_pattern(R"(^\s*ports:\s*$)"); + while (std::getline(file, line)) { + if (std::regex_search(line, ports_pattern)) { + exposes_ports = true; + detection_reason = "Found ports: section in " + std::string(compose_name); + break; + } + } + if (exposes_ports) break; + } + } + } + + if (exposes_ports && !has_ports_sh) { + result.passed = false; + result.message = "Template exposes ports but ports.sh is missing"; + result.details = detection_reason + ". Add ports.sh to output exposed port numbers."; + } else if (exposes_ports && has_ports_sh) { + result.message = "ports.sh exists for port-exposing template"; + } else if (!exposes_ports && has_ports_sh) { + result.message = "ports.sh exists (no port exposure detected)"; + } else { + result.message = "No ports exposed (ports.sh not required)"; + } + + return result; +} + // Check if scripts contain interactive commands int check_interactive_commands(const std::filesystem::path& template_path, std::vector& scripts) { int issues = 0; @@ -484,6 +561,22 @@ int validate_handler(const CommandContext& ctx) { } std::cout << std::endl; + // Step 9: Check for ports.sh when ports are exposed + info << "=== Port Exposure Check ===" << std::endl; + { + ValidationResult result = check_ports_script(template_path); + if (result.passed) { + info << " ✓ " << result.message << std::endl; + } else { + warning << " ⚠ " << result.message << std::endl; + if (!result.details.empty()) { + warning << " " << result.details << std::endl; + } + warnings++; + } + } + std::cout << std::endl; + // Summary maketitle("Validation Summary"); if (errors == 0 && warnings == 0) {