feat: Update 3 files
All checks were successful
Build-Test-Publish / build (linux/amd64) (push) Successful in 27s
Build-Test-Publish / build (linux/arm64) (push) Successful in 1m16s

This commit is contained in:
j
2026-01-15 10:22:05 +13:00
parent 8b1433c03b
commit 22e2397624
3 changed files with 252 additions and 15 deletions

View File

@@ -160,6 +160,17 @@ namespace dropshell
// copy the template config files to the service directory // copy the template config files to the service directory
recursive_copy(tinfo.local_template_path() / "config", service_dir); 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. // modify the SSH_USER to be nice.
// everything is created, so we can get the service info. // everything is created, so we can get the service info.
LocalServiceInfo service_info = get_service_info(server_name, service_name); LocalServiceInfo service_info = get_service_info(server_name, service_name);
@@ -210,17 +221,6 @@ namespace dropshell
service_env_file_out.close(); 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. // check docker.
if (service_info.requires_docker) if (service_info.requires_docker)
{ {

View File

@@ -4,6 +4,7 @@
#include "utils/utils.hpp" #include "utils/utils.hpp"
#include "utils/directories.hpp" #include "utils/directories.hpp"
#include "utils/execute.hpp" #include "utils/execute.hpp"
#include "utils/envmanager.hpp"
#include "shared_commands.hpp" #include "shared_commands.hpp"
#include <iostream> #include <iostream>
@@ -36,10 +37,13 @@ struct ValidateCommandRegister {
"Validate a template's structure, syntax, and common issues.", "Validate a template's structure, syntax, and common issues.",
R"( R"(
Validates a dropshell template by checking: 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) 2. Shell script linting via shellcheck (run in container)
3. Dropshell-specific checks: 3. Scripts source common.sh
- 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: Example:
ds validate my-template ds validate my-template
@@ -174,6 +178,174 @@ ValidationResult check_common_sh_sourced(const std::filesystem::path& script_pat
return result; 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 // Main validation handler
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@@ -248,6 +420,70 @@ int validate_handler(const CommandContext& ctx) {
} }
std::cout << std::endl; 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 // Summary
maketitle("Validation Summary"); maketitle("Validation Summary");
if (errors == 0 && warnings == 0) { if (errors == 0 && warnings == 0) {

View File

@@ -777,7 +777,8 @@ For full documentation, see: dropshell help templates
"config/" + filenames::service_env, "config/" + filenames::service_env,
filenames::template_info_env, filenames::template_info_env,
"install.sh", "install.sh",
"uninstall.sh" "uninstall.sh",
"status.sh"
}; };
for (const auto& file : required_files) { for (const auto& file : required_files) {