From 6804ea3e60ac00064eb8634317da50b292e54128 Mon Sep 17 00:00:00 2001 From: j Date: Sun, 28 Dec 2025 10:59:51 +1300 Subject: [PATCH] Update source/src/templates.cpp --- source/src/templates.cpp | 270 ++++++++++++++++++++++++++++++--------- 1 file changed, 212 insertions(+), 58 deletions(-) diff --git a/source/src/templates.cpp b/source/src/templates.cpp index cf54210..2655181 100644 --- a/source/src/templates.cpp +++ b/source/src/templates.cpp @@ -493,6 +493,25 @@ return source->template_command_exists(template_name, command); } + // Helper function to write a file with content + static bool write_template_file(const std::string& path, const std::string& content, bool executable = false) { + std::ofstream file(path); + if (!file.is_open()) { + error << "Failed to create file: " << path << std::endl; + return false; + } + file << content; + file.close(); + + if (executable) { + std::filesystem::permissions(path, + std::filesystem::perms::owner_all | + std::filesystem::perms::group_read | std::filesystem::perms::group_exec | + std::filesystem::perms::others_read | std::filesystem::perms::others_exec); + } + return true; + } + bool template_manager::create_template(const std::string &template_name) const { if (!legal_service_name(template_name)) { @@ -500,21 +519,14 @@ return false; } - // 1. Create a new directory in the user templates directory - std::vector local_server_definition_paths = gConfig().get_local_server_definition_paths(); - - if (local_server_definition_paths.empty()) { - error << "No local server definition paths found" << std::endl; - info << "Run 'dropshell edit' to configure DropShell" << std::endl; - return false; - } - + // 1. Check template doesn't already exist auto tinfo = get_template_info(template_name, false); if (tinfo.is_set()) { error << "Template '" << template_name << "' already exists at " << tinfo.locationID() << std::endl; return false; } + // 2. Determine where to create the template auto local_template_paths = gConfig().get_local_template_paths(); if (local_template_paths.empty()) { error << "No local template paths found" << std::endl; @@ -522,58 +534,200 @@ return false; } std::string new_template_path = local_template_paths[0] + "/" + template_name; - - // Create the new template directory - std::filesystem::create_directories(new_template_path); - - // 2. Copy the example template from the system templates directory - auto example_info = gTemplateManager().get_template_info("example-nginx"); - if (!example_info.is_set()) { - error << "Example template not found" << std::endl; - return false; - } - std::string example_template_path = example_info.local_template_path(); - - // Copy all files from example template to new template - for (const auto& entry : std::filesystem::recursive_directory_iterator(example_template_path)) { - std::string relative_path = entry.path().string().substr(example_template_path.length()); - std::string target_path = new_template_path + relative_path; - - if (entry.is_directory()) { - std::filesystem::create_directory(target_path); - } else { - std::filesystem::copy_file(entry.path(), target_path); - } - } - // modify the TEMPLATE=example line in the .template_info.env file to TEMPLATE= - std::string search_string = "TEMPLATE="; - std::string replacement_line = "TEMPLATE=" + template_name; - std::string service_env_path = new_template_path + "/config/" + filenames::template_info_env; - if (!replace_line_in_file(service_env_path, search_string, replacement_line)) { - error << "Failed to replace TEMPLATE= line in the " << filenames::template_info_env <<" file" << std::endl; - return false; - } + // 3. Create directory structure + std::filesystem::create_directories(new_template_path + "/config"); - // 3. Print out the README.txt file and the path - std::string readme_path = new_template_path + "/README.txt"; - if (std::filesystem::exists(readme_path)) { - std::cout << "\nREADME contents:" << std::endl; - std::cout << std::string(60, '-') << std::endl; - - std::ifstream readme_file(readme_path); - if (readme_file.is_open()) { - std::string line; - while (std::getline(readme_file, line)) { - std::cout << line << std::endl; - } - readme_file.close(); - } - std::cout << std::string(60, '-') << std::endl; - } + // 4. Generate template files inline (self-contained, no external dependencies) + + // config/.template_info.env + std::string template_info_env = R"TMPL(# Template identifier - MUST match the directory name +TEMPLATE=)TMPL" + template_name + R"TMPL( + +# Requirements +REQUIRES_HOST_ROOT=false +REQUIRES_DOCKER=true +REQUIRES_DOCKER_ROOT=false + +# Docker image settings +IMAGE_REGISTRY="docker.io" +IMAGE_REPO="nginx" +IMAGE_TAG="alpine" + +# Volume definitions +DATA_VOLUME="${CONTAINER_NAME}_data" +)TMPL"; + if (!write_template_file(new_template_path + "/config/" + filenames::template_info_env, template_info_env)) return false; + + // config/service.env + std::string service_env = R"TMPL(# Service identification (REQUIRED) +CONTAINER_NAME=)TMPL" + template_name + R"TMPL( + +# Server settings (REQUIRED by dropshell) +SSH_USER="root" + +# Service-specific settings +HTTP_PORT=8080 +)TMPL"; + if (!write_template_file(new_template_path + "/config/" + filenames::service_env, service_env)) return false; + + // install.sh + std::string install_sh = R"BASH(#!/bin/bash +source "${AGENT_PATH}/common.sh" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Check required environment variables +_check_required_env_vars "CONTAINER_NAME" "IMAGE_REGISTRY" "IMAGE_REPO" "IMAGE_TAG" + +# Check Docker is available +_check_docker_installed || _die "Docker test failed" + +# Pull the Docker image +docker pull "$IMAGE_REGISTRY/$IMAGE_REPO:$IMAGE_TAG" || _die "Failed to pull image" + +# Stop any existing container +bash "$SCRIPT_DIR/stop.sh" 2>/dev/null || true + +# Remove old container +_remove_container "$CONTAINER_NAME" 2>/dev/null || true + +# Start the new container +bash "$SCRIPT_DIR/start.sh" || _die "Failed to start container" + +echo "Installation of ${CONTAINER_NAME} complete" +)BASH"; + if (!write_template_file(new_template_path + "/install.sh", install_sh, true)) return false; + + // uninstall.sh + std::string uninstall_sh = R"BASH(#!/bin/bash +source "${AGENT_PATH}/common.sh" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +_check_required_env_vars "CONTAINER_NAME" + +# Stop the container +bash "$SCRIPT_DIR/stop.sh" 2>/dev/null || true + +# Remove the container (but preserve data volumes!) +_remove_container "$CONTAINER_NAME" || _die "Failed to remove container" + +# CRITICAL: Never remove data volumes in uninstall.sh! +# Data volumes must be preserved for potential reinstallation +# Only destroy.sh should remove volumes + +echo "Uninstallation of ${CONTAINER_NAME} complete" +echo "Note: Data volumes have been preserved. To remove all data, use destroy.sh" +)BASH"; + if (!write_template_file(new_template_path + "/uninstall.sh", uninstall_sh, true)) return false; + + // start.sh + std::string start_sh = R"BASH(#!/bin/bash +source "${AGENT_PATH}/common.sh" + +_check_required_env_vars "CONTAINER_NAME" "IMAGE_REGISTRY" "IMAGE_REPO" "IMAGE_TAG" "HTTP_PORT" + +# Create data volume if it doesn't exist +docker volume create "$DATA_VOLUME" 2>/dev/null || true + +# Start the container +docker run -d \ + --name "$CONTAINER_NAME" \ + --restart unless-stopped \ + -p "${HTTP_PORT}:80" \ + -v "${DATA_VOLUME}:/usr/share/nginx/html" \ + "$IMAGE_REGISTRY/$IMAGE_REPO:$IMAGE_TAG" || _die "Failed to start container" + +echo "Container ${CONTAINER_NAME} started on port ${HTTP_PORT}" +)BASH"; + if (!write_template_file(new_template_path + "/start.sh", start_sh, true)) return false; + + // stop.sh + std::string stop_sh = R"BASH(#!/bin/bash +source "${AGENT_PATH}/common.sh" + +_check_required_env_vars "CONTAINER_NAME" + +docker stop "$CONTAINER_NAME" 2>/dev/null || true + +echo "Container ${CONTAINER_NAME} stopped" +)BASH"; + if (!write_template_file(new_template_path + "/stop.sh", stop_sh, true)) return false; + + // status.sh (REQUIRED for dropshell list command) + std::string status_sh = R"BASH(#!/bin/bash +source "${AGENT_PATH}/common.sh" + +_check_required_env_vars "CONTAINER_NAME" + +# Check if container is running +if docker ps --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then + echo "Running" +else + echo "Stopped" +fi +)BASH"; + if (!write_template_file(new_template_path + "/status.sh", status_sh, true)) return false; + + // logs.sh + std::string logs_sh = R"BASH(#!/bin/bash +source "${AGENT_PATH}/common.sh" + +_check_required_env_vars "CONTAINER_NAME" + +docker logs "$CONTAINER_NAME" "$@" +)BASH"; + if (!write_template_file(new_template_path + "/logs.sh", logs_sh, true)) return false; + + // README.txt + std::string readme = "Template: " + template_name + R"TMPL( + +This template was created by 'dropshell create-template'. + +QUICK START +----------- +1. Edit config/service.env to customize your deployment +2. Edit config/.template_info.env if you need different Docker settings +3. Modify the scripts as needed for your use case +4. Run 'dropshell validate ' to check for issues + +REQUIRED FILES +-------------- +- config/.template_info.env : Template metadata (don't change TEMPLATE=) +- config/service.env : Service configuration (edit this!) +- install.sh : Installation script +- uninstall.sh : Uninstallation script (preserves data) +- status.sh : Status check (required for 'dropshell list') + +OPTIONAL FILES +-------------- +- start.sh : Start the service +- stop.sh : Stop the service +- logs.sh : View logs +- backup.sh : Backup data +- restore.sh : Restore data +- destroy.sh : Remove service AND data (use with caution!) + +BEST PRACTICES +-------------- +1. Always source common.sh: source "${AGENT_PATH}/common.sh" +2. Check required vars: _check_required_env_vars "VAR1" "VAR2" +3. Handle errors: command || _die "Error message" +4. NEVER remove data volumes in uninstall.sh +5. Run 'dropshell validate' before publishing + +For full documentation, see: dropshell help templates +)TMPL"; + if (!write_template_file(new_template_path + "/README.txt", readme)) return false; + + // 5. Print summary + std::cout << "\nTemplate '" << template_name << "' created at " << new_template_path << std::endl; + std::cout << std::string(60, '-') << std::endl; + std::cout << "Next steps:" << std::endl; + std::cout << " 1. Edit " << new_template_path << "/config/service.env" << std::endl; + std::cout << " 2. Customize the scripts for your application" << std::endl; + std::cout << " 3. Run: dropshell validate " << new_template_path << std::endl; + std::cout << std::string(60, '-') << std::endl; - std::cout << std::endl; - std::cout << "Template '" << template_name << "' created at " << new_template_path << std::endl; return test_template(new_template_path); }