feat: Update 3 files
This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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 <iostream>
|
||||
@@ -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<std::string> 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<std::filesystem::path>& 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<std::string> 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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user