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