From 22e23976242ccff444bbda3dd9429793fec377d6 Mon Sep 17 00:00:00 2001 From: j Date: Thu, 15 Jan 2026 10:22:05 +1300 Subject: [PATCH] feat: Update 3 files --- source/src/commands/create-service.cpp | 22 +- source/src/commands/validate-template.cpp | 242 +++++++++++++++++++++- source/src/templates.cpp | 3 +- 3 files changed, 252 insertions(+), 15 deletions(-) diff --git a/source/src/commands/create-service.cpp b/source/src/commands/create-service.cpp index 17f1a4a..e68cbba 100644 --- a/source/src/commands/create-service.cpp +++ b/source/src/commands/create-service.cpp @@ -160,6 +160,17 @@ namespace dropshell // copy the template config files to the service directory recursive_copy(tinfo.local_template_path() / "config", service_dir); + // set TEMPLATE in service.env first (required before get_service_info) + if (!set_env_variable( + localfile::service_env(server_name, service_name), + "TEMPLATE", + template_name, + "TEMPLATE set by dropshell on creation. Important this does not change.")) + { + error << "Failed to set TEMPLATE in service.env" << std::endl; + return false; + } + // modify the SSH_USER to be nice. // everything is created, so we can get the service info. LocalServiceInfo service_info = get_service_info(server_name, service_name); @@ -210,17 +221,6 @@ namespace dropshell service_env_file_out.close(); } - // set TEMPLATE in service.env (removes any existing definition first) - if (!set_env_variable( - localfile::service_env(server_name, service_name), - "TEMPLATE", - template_name, - "TEMPLATE set by dropshell on creation. Important this does not change.")) - { - error << "Failed to set TEMPLATE in service.env" << std::endl; - return false; - } - // check docker. if (service_info.requires_docker) { diff --git a/source/src/commands/validate-template.cpp b/source/src/commands/validate-template.cpp index bc5066d..1aaf710 100644 --- a/source/src/commands/validate-template.cpp +++ b/source/src/commands/validate-template.cpp @@ -4,6 +4,7 @@ #include "utils/utils.hpp" #include "utils/directories.hpp" #include "utils/execute.hpp" +#include "utils/envmanager.hpp" #include "shared_commands.hpp" #include @@ -36,10 +37,13 @@ struct ValidateCommandRegister { "Validate a template's structure, syntax, and common issues.", R"( Validates a dropshell template by checking: - 1. Required files exist (install.sh, uninstall.sh, config files) + 1. Required files exist (install.sh, uninstall.sh, status.sh, config files) 2. Shell script linting via shellcheck (run in container) - 3. Dropshell-specific checks: - - Scripts source common.sh + 3. Scripts source common.sh + 4. Required variables in service.env (SSH_USER, CONTAINER_NAME) + 5. Backup/restore script pairing + 6. uninstall.sh preserves data volumes + 7. Scripts are non-interactive (no read -p, select, etc.) Example: ds validate my-template @@ -174,6 +178,174 @@ ValidationResult check_common_sh_sourced(const std::filesystem::path& script_pat return result; } +// Check if a required variable is defined in service.env +ValidationResult check_env_var_defined(const std::filesystem::path& service_env_path, const std::string& var_name) { + ValidationResult result; + result.passed = false; + + if (!std::filesystem::exists(service_env_path)) { + result.message = "service.env not found"; + return result; + } + + envmanager env(service_env_path.string()); + env.load(); + + std::string value = env.get_variable(var_name); + if (!value.empty()) { + result.passed = true; + result.message = var_name + " is defined in service.env"; + } else { + result.message = var_name + " is not defined in service.env"; + result.details = "This variable is required for proper service operation."; + } + return result; +} + +// Check if backup.sh and restore.sh are paired +ValidationResult check_backup_restore_pairing(const std::filesystem::path& template_path) { + ValidationResult result; + + bool has_backup = std::filesystem::exists(template_path / "backup.sh"); + bool has_restore = std::filesystem::exists(template_path / "restore.sh"); + + if (has_backup && !has_restore) { + result.passed = false; + result.message = "backup.sh exists but restore.sh is missing"; + result.details = "Templates with backup.sh must also have restore.sh for data recovery."; + } else if (!has_backup && has_restore) { + result.passed = false; + result.message = "restore.sh exists but backup.sh is missing"; + result.details = "restore.sh requires a corresponding backup.sh to create backups."; + } else { + result.passed = true; + if (has_backup && has_restore) { + result.message = "backup.sh and restore.sh are properly paired"; + } else { + result.message = "No backup/restore scripts (optional)"; + } + } + return result; +} + +// Check if uninstall.sh contains volume removal (which it shouldn't) +ValidationResult check_uninstall_preserves_volumes(const std::filesystem::path& template_path) { + ValidationResult result; + result.passed = true; + + std::filesystem::path uninstall_path = template_path / "uninstall.sh"; + if (!std::filesystem::exists(uninstall_path)) { + result.message = "uninstall.sh not found"; + result.passed = false; + return result; + } + + std::ifstream file(uninstall_path); + if (!file.is_open()) { + result.message = "Could not open uninstall.sh"; + result.passed = false; + return result; + } + + std::string line; + int line_num = 0; + std::vector violations; + + // Patterns that indicate volume removal + std::regex volume_rm_pattern(R"(docker\s+volume\s+rm)"); + std::regex remove_volume_pattern(R"(_remove_volume\s)"); + std::regex volume_prune_pattern(R"(docker\s+volume\s+prune)"); + + while (std::getline(file, line)) { + line_num++; + // Skip comments + std::string trimmed = line; + size_t hash_pos = trimmed.find('#'); + if (hash_pos != std::string::npos) { + trimmed = trimmed.substr(0, hash_pos); + } + + if (std::regex_search(trimmed, volume_rm_pattern) || + std::regex_search(trimmed, remove_volume_pattern) || + std::regex_search(trimmed, volume_prune_pattern)) { + violations.push_back("Line " + std::to_string(line_num) + ": " + line); + } + } + + if (!violations.empty()) { + result.passed = false; + result.message = "uninstall.sh may remove data volumes"; + result.details = "Found potential volume removal:\n"; + for (const auto& v : violations) { + result.details += " " + v + "\n"; + } + result.details += " Data volumes should only be removed in destroy.sh, not uninstall.sh."; + } else { + result.message = "uninstall.sh preserves data volumes"; + } + return result; +} + +// Check if scripts contain interactive commands +int check_interactive_commands(const std::filesystem::path& template_path, std::vector& scripts) { + int issues = 0; + + // Patterns that indicate interactive commands + std::regex read_p_pattern(R"(\bread\s+-[a-zA-Z]*p)"); // read -p, read -rp, etc. + std::regex read_prompt_pattern(R"(\bread\s+[^|&;#\n]*\s+\w+\s*$)"); // read var (simple read) + std::regex select_pattern(R"(\bselect\s+\w+\s+in\b)"); + + for (const auto& script : scripts) { + std::string filename = script.filename().string(); + + // Skip destroy.sh - it's allowed to have interactive prompts per TEMPLATES.md + if (filename == "destroy.sh") { + continue; + } + + std::ifstream file(script); + if (!file.is_open()) continue; + + std::string line; + int line_num = 0; + bool script_header_printed = false; + + while (std::getline(file, line)) { + line_num++; + + // Skip comments + std::string trimmed = line; + size_t hash_pos = trimmed.find('#'); + if (hash_pos != std::string::npos) { + trimmed = trimmed.substr(0, hash_pos); + } + if (trimmed.empty()) continue; + + bool found_issue = false; + std::string issue_type; + + if (std::regex_search(trimmed, read_p_pattern)) { + found_issue = true; + issue_type = "interactive read"; + } else if (std::regex_search(trimmed, select_pattern)) { + found_issue = true; + issue_type = "select menu"; + } + + if (found_issue) { + if (!script_header_printed) { + warning << " " << filename << ":" << std::endl; + script_header_printed = true; + } + warning << " Line " << line_num << ": " << issue_type << ": " << line << std::endl; + issues++; + } + } + } + + return issues; +} + // ---------------------------------------------------------------------------- // Main validation handler // ---------------------------------------------------------------------------- @@ -248,6 +420,70 @@ int validate_handler(const CommandContext& ctx) { } std::cout << std::endl; + // Step 5: Check required environment variables in service.env + info << "=== Required Variables in service.env ===" << std::endl; + std::filesystem::path service_env_path = template_path / "config" / filenames::service_env; + std::vector required_service_vars = {"SSH_USER", "CONTAINER_NAME"}; + for (const auto& var_name : required_service_vars) { + ValidationResult result = check_env_var_defined(service_env_path, var_name); + if (result.passed) { + info << " ✓ " << result.message << std::endl; + } else { + error << " ✗ " << result.message << std::endl; + if (!result.details.empty()) { + error << " " << result.details << std::endl; + } + errors++; + } + } + std::cout << std::endl; + + // Step 6: Check backup/restore pairing + info << "=== Backup/Restore Pairing ===" << std::endl; + { + ValidationResult result = check_backup_restore_pairing(template_path); + if (result.passed) { + info << " ✓ " << result.message << std::endl; + } else { + error << " ✗ " << result.message << std::endl; + if (!result.details.empty()) { + error << " " << result.details << std::endl; + } + errors++; + } + } + std::cout << std::endl; + + // Step 7: Check that uninstall.sh preserves volumes + info << "=== Data Volume Preservation ===" << std::endl; + { + ValidationResult result = check_uninstall_preserves_volumes(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; + + // Step 8: Check for interactive commands + info << "=== Non-Interactive Script Check ===" << std::endl; + { + int interactive_issues = check_interactive_commands(template_path, shell_scripts); + if (interactive_issues == 0) { + info << " ✓ No interactive commands found" << std::endl; + } else { + warning << " Found " << interactive_issues << " potential interactive command(s)" << std::endl; + warning << " Scripts must be non-interactive for automated deployments." << std::endl; + warnings += interactive_issues; + } + } + std::cout << std::endl; + // Summary maketitle("Validation Summary"); if (errors == 0 && warnings == 0) { diff --git a/source/src/templates.cpp b/source/src/templates.cpp index 9df0150..e8dbb83 100644 --- a/source/src/templates.cpp +++ b/source/src/templates.cpp @@ -777,7 +777,8 @@ For full documentation, see: dropshell help templates "config/" + filenames::service_env, filenames::template_info_env, "install.sh", - "uninstall.sh" + "uninstall.sh", + "status.sh" }; for (const auto& file : required_files) {