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
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)
{

View File

@@ -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) {

View File

@@ -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) {