#include #include #include #include #include #include #include #include #include "utils/envmanager.hpp" #include "utils/directories.hpp" #include "utils/utils.hpp" #include "utils/output.hpp" #include "utils/execute.hpp" #include "templates.hpp" #include "config.hpp" #include "utils/hash.hpp" namespace dropshell { // ------------------------------------------------------------------------------------------------ // template_manager — loading and resolution // ------------------------------------------------------------------------------------------------ void template_manager::load_sources() { ASSERT(mSources.empty(), "Template manager already loaded (sources are not empty)."); ASSERT(gConfig().is_config_set(), "Config not set."); ASSERT(!mLoaded, "Template manager already loaded."); // Read template_paths.json from each server_definition_path auto server_def_paths = gConfig().get_local_server_definition_paths(); for (const auto& sdp : server_def_paths) { std::filesystem::path json_path = std::filesystem::path(sdp) / filenames::template_paths_json; if (!std::filesystem::exists(json_path)) continue; std::ifstream f(json_path); if (!f.is_open()) { warning << "Could not open " << json_path << std::endl; continue; } nlohmann::json j; try { j = nlohmann::json::parse(f); } catch (nlohmann::json::parse_error& ex) { error << "Failed to parse " << json_path << ": " << ex.what() << std::endl; continue; } if (!j.is_array()) { error << json_path << " must be a JSON array" << std::endl; continue; } for (const auto& entry : j) { if (!entry.is_string() || entry.get().empty()) continue; std::string s = entry.get(); TemplateSource src; // Split on first ':' — Linux paths never contain colons size_t colon = s.find(':'); if (colon != std::string::npos) { src.local_path = s.substr(0, colon); src.git_url = s.substr(colon + 1); } else { src.local_path = s; } mSources.push_back(src); } } // Resolve templates from all sources resolve_templates(); mLoaded = true; } void template_manager::resolve_templates() { mTemplateMap.clear(); for (size_t i = 0; i < mSources.size(); ++i) resolve_source(i); } void template_manager::resolve_source(size_t source_index) { const auto& src = mSources[source_index]; if (!std::filesystem::exists(src.local_path) || !std::filesystem::is_directory(src.local_path)) { if (!src.git_url.empty()) debug << "Source path does not exist (run 'dropshell pull' to clone): " << src.local_path << std::endl; else warning << "Source path does not exist: " << src.local_path << std::endl; return; } // Try to read dropshell-templates.list std::filesystem::path list_file = std::filesystem::path(src.local_path) / filenames::dropshell_templates_list; std::vector template_dirs; if (std::filesystem::exists(list_file)) { template_dirs = parse_list_file(src.local_path); } else { // Discover templates by searching for template_info.env template_dirs = discover_templates(src.local_path); // Write the discovered list so next time is fast if (!template_dirs.empty()) write_list_file(src.local_path, template_dirs); } // Remove stale entries for this source from mTemplateMap before re-adding for (auto it = mTemplateMap.begin(); it != mTemplateMap.end(); ) { if (it->second.source_index == source_index) it = mTemplateMap.erase(it); else ++it; } // Add to map (first-wins across sources) for (const auto& tdir : template_dirs) { std::filesystem::path full_path = std::filesystem::path(src.local_path) / tdir; if (!std::filesystem::exists(full_path) || !std::filesystem::is_directory(full_path)) { debug << "Template directory does not exist: " << full_path << std::endl; continue; } std::string name = full_path.filename().string(); if (mTemplateMap.find(name) == mTemplateMap.end()) { mTemplateMap[name] = ResolvedTemplate{full_path, source_index}; } } } std::vector template_manager::parse_list_file(const std::string& local_path) const { std::vector result; std::filesystem::path list_file = std::filesystem::path(local_path) / filenames::dropshell_templates_list; std::ifstream f(list_file); if (!f.is_open()) return result; std::string line; while (std::getline(f, line)) { // Trim whitespace size_t start = line.find_first_not_of(" \t\r\n"); if (start == std::string::npos) continue; size_t end = line.find_last_not_of(" \t\r\n"); line = line.substr(start, end - start + 1); // Skip comments and blank lines if (line.empty() || line[0] == '#') continue; result.push_back(line); } return result; } std::vector template_manager::discover_templates(const std::string& local_path) const { std::vector result; std::filesystem::path root(local_path); for (auto it = std::filesystem::recursive_directory_iterator(root, std::filesystem::directory_options::skip_permission_denied); it != std::filesystem::recursive_directory_iterator(); ++it) { if (!it->is_directory()) continue; // Skip .git directories if (it->path().filename() == ".git") { it.disable_recursion_pending(); continue; } // Check if this directory contains template_info.env std::error_code ec; if (std::filesystem::exists(it->path() / filenames::template_info_env, ec) && !ec) { // Get relative path from root std::string rel = std::filesystem::relative(it->path(), root).string(); result.push_back(rel); // Don't recurse into template directories (templates don't nest) it.disable_recursion_pending(); } } return result; } void template_manager::write_list_file(const std::string& local_path, const std::vector& template_dirs) const { std::filesystem::path list_file = std::filesystem::path(local_path) / filenames::dropshell_templates_list; std::ofstream f(list_file); if (!f.is_open()) { debug << "Could not write " << list_file << std::endl; return; } f << "# Auto-generated by dropshell — template directories relative to this file" << std::endl; for (const auto& dir : template_dirs) f << dir << std::endl; info << "Wrote " << list_file << " (" << template_dirs.size() << " templates)" << std::endl; } // ------------------------------------------------------------------------------------------------ // template_manager — git operations // ------------------------------------------------------------------------------------------------ bool template_manager::git_clone_source(const TemplateSource& source) { if (source.git_url.empty()) return false; info << "Cloning " << source.git_url << " → " << source.local_path << std::endl; // Create parent directory if needed std::filesystem::path parent = std::filesystem::path(source.local_path).parent_path(); if (!std::filesystem::exists(parent)) std::filesystem::create_directories(parent); std::string cmd = "git clone " + quote(source.git_url) + " " + quote(source.local_path) + " 2>&1"; std::string output; ordered_env_vars empty_env; bool ok = execute_local_command(".", cmd, empty_env, &output, cMode::Silent); if (!ok) error << "git clone failed: " << output << std::endl; else info << "Cloned successfully." << std::endl; return ok; } bool template_manager::git_pull_source(const TemplateSource& source) { if (!std::filesystem::exists(std::filesystem::path(source.local_path) / ".git")) return false; // Verify remote URL matches if git_url is set if (!source.git_url.empty()) { std::string remote_output; ordered_env_vars empty_env; std::string check_cmd = "git -C " + quote(source.local_path) + " remote get-url origin 2>/dev/null"; execute_local_command(".", check_cmd, empty_env, &remote_output, cMode::Silent); // Trim while (!remote_output.empty() && (remote_output.back() == '\n' || remote_output.back() == '\r')) remote_output.pop_back(); if (!remote_output.empty() && remote_output != source.git_url) { warning << "Remote URL mismatch for " << source.local_path << std::endl; warning << " Expected: " << source.git_url << std::endl; warning << " Actual: " << remote_output << std::endl; return false; } } debug << "Pulling " << source.local_path << std::endl; std::string cmd = "git -C " + quote(source.local_path) + " pull --ff-only 2>&1"; std::string output; ordered_env_vars empty_env; bool ok = execute_local_command(".", cmd, empty_env, &output, cMode::Silent); if (!ok) warning << "git pull failed for " << source.local_path << ": " << output << std::endl; return ok; } bool template_manager::pull_all() { bool any_changes = false; for (const auto& src : mSources) { if (!std::filesystem::exists(src.local_path)) { if (!src.git_url.empty()) { if (git_clone_source(src)) any_changes = true; } else { warning << "Source path does not exist and no git URL configured: " << src.local_path << std::endl; } } else if (std::filesystem::exists(std::filesystem::path(src.local_path) / ".git")) { if (git_pull_source(src)) any_changes = true; } // else: local-only, no .git — skip } // Re-resolve all templates after pulling resolve_templates(); return true; } bool template_manager::pull_for_template(const std::string& template_name) { auto it = mTemplateMap.find(template_name); if (it == mTemplateMap.end()) return false; const auto& src = mSources[it->second.source_index]; if (src.git_url.empty()) return true; // local-only, nothing to pull if (!std::filesystem::exists(std::filesystem::path(src.local_path) / ".git")) return true; // no .git dir, nothing to pull git_pull_source(src); // Re-resolve this source's templates resolve_source(it->second.source_index); return true; } // ------------------------------------------------------------------------------------------------ // template_manager — queries // ------------------------------------------------------------------------------------------------ void template_manager::list_templates() const { ASSERT(mLoaded, "Template manager not loaded."); auto templates = get_template_list(); if (templates.empty()) { std::cout << "No templates found." << std::endl; return; } std::cout << "Available templates:" << std::endl; std::cout << std::string(60, '-') << std::endl; bool first = true; for (const auto& t : templates) { std::cout << (first ? "" : ", ") << t; first = false; } std::cout << std::endl; std::cout << std::string(60, '-') << std::endl; } std::set template_manager::get_template_list() const { ASSERT(mLoaded, "Template manager not loaded."); std::set templates; for (const auto& [name, _] : mTemplateMap) templates.insert(name); return templates; } std::vector> template_manager::get_template_list_with_source() const { ASSERT(mLoaded, "Template manager not loaded."); std::vector> result; for (const auto& [name, resolved] : mTemplateMap) { // Only include templates that have install.sh if (!std::filesystem::exists(resolved.template_dir / "install.sh")) continue; const auto& src = mSources[resolved.source_index]; std::string desc = src.git_url.empty() ? "Local: " + src.local_path : "Git: " + src.local_path; result.push_back({name, desc}); } return result; } bool template_manager::has_template(const std::string &template_name) const { ASSERT(mLoaded, "Template manager not loaded."); return mTemplateMap.count(template_name) > 0; } template_info template_manager::get_template_info(const std::string &template_name) const { ASSERT(mLoaded, "Template manager not loaded."); auto it = mTemplateMap.find(template_name); if (it == mTemplateMap.end()) return template_info(); const auto& src = mSources[it->second.source_index]; return template_info(template_name, src.local_path, it->second.template_dir); } bool template_manager::template_command_exists(const std::string &template_name, const std::string &command) const { ASSERT(mLoaded, "Template manager not loaded."); auto it = mTemplateMap.find(template_name); if (it == mTemplateMap.end()) { error << "Template '" << template_name << "' not found" << std::endl; return false; } return std::filesystem::exists(it->second.template_dir / (command + ".sh")); } // ------------------------------------------------------------------------------------------------ // template_manager — create template // ------------------------------------------------------------------------------------------------ // 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)) { error << "Template name contains illegal characters: " << template_name << std::endl; return false; } // 1. Check template doesn't already exist auto tinfo = get_template_info(template_name); if (tinfo.is_set()) { error << "Template '" << template_name << "' already exists at " << tinfo.locationID() << std::endl; return false; } // 2. Determine where to create the template if (mSources.empty()) { error << "No template sources configured" << std::endl; info << "Create a template_paths.json in your server definition path" << std::endl; return false; } std::string base_path = mSources[0].local_path; if (!std::filesystem::exists(base_path)) { error << "Template source path does not exist: " << base_path << std::endl; return false; } std::string new_template_path = base_path + "/" + template_name; // 3. Create directory structure std::filesystem::create_directories(new_template_path + "/config"); // 4. Generate template files std::string template_info_env = R"TMPL(# Template metadata - DO NOT EDIT # This file is replaced when the template is updated. # 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 + "/" + filenames::template_info_env, template_info_env)) return false; 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; std::string install_sh = R"BASH(#!/bin/bash source "${AGENT_PATH}/common.sh" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" _check_required_env_vars "CONTAINER_NAME" "IMAGE_REGISTRY" "IMAGE_REPO" "IMAGE_TAG" _check_docker_installed || _die "Docker test failed" docker pull -q "$IMAGE_REGISTRY/$IMAGE_REPO:$IMAGE_TAG" || _die "Failed to pull image" bash "$SCRIPT_DIR/stop.sh" 2>/dev/null || true _remove_container "$CONTAINER_NAME" 2>/dev/null || true 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; 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" bash "$SCRIPT_DIR/stop.sh" 2>/dev/null || true _remove_container "$CONTAINER_NAME" || _die "Failed to remove container" 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; 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" docker volume create "$DATA_VOLUME" 2>/dev/null || true 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; 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; std::string status_sh = R"BASH(#!/bin/bash source "${AGENT_PATH}/common.sh" _check_required_env_vars "CONTAINER_NAME" 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; 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; // 5. Update dropshell-templates.list std::filesystem::path list_file = std::filesystem::path(base_path) / filenames::dropshell_templates_list; std::ofstream lf(list_file, std::ios::app); if (lf.is_open()) { lf << template_name << std::endl; lf.close(); } // 6. 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; return test_template(new_template_path); } // ------------------------------------------------------------------------------------------------ // template_manager — sources display // ------------------------------------------------------------------------------------------------ void template_manager::print_sources() const { std::cout << "Template sources:" << std::endl; for (const auto& src : mSources) { std::cout << " [" << src.local_path; if (!src.git_url.empty()) std::cout << " ← " << src.git_url; std::cout << "]" << std::endl; } } // ------------------------------------------------------------------------------------------------ // template_manager — validation (static) // ------------------------------------------------------------------------------------------------ bool template_manager::required_file(std::string path, std::string template_name) { if (!std::filesystem::exists(path)) { error << path << " file not found in template - REQUIRED." << template_name << std::endl; return false; } return true; } bool template_manager::test_template(const std::string &template_path) { if (template_path.empty()) return false; if (!std::filesystem::exists(template_path)) return false; std::string template_name = std::filesystem::path(template_path).filename().string(); std::vector required_files = { "config/" + filenames::service_env, filenames::template_info_env, "install.sh", "uninstall.sh", "status.sh" }; for (const auto& file : required_files) { if (!required_file(template_path + "/" + file, template_name)) return false; std::string suffix=".sh"; if (file.find(suffix) == file.size() - suffix.size()) { std::filesystem::path path = template_path + "/" + file; auto perms = std::filesystem::status(path).permissions(); if ((perms & std::filesystem::perms::owner_exec) == std::filesystem::perms::none) error << file << " is not executable" << std::endl; } } // check required variables in template_info.env ordered_env_vars all_env_vars; std::vector env_files = { "config/" + filenames::service_env, filenames::template_info_env }; for (const auto& file : env_files) { ordered_env_vars env_vars; envmanager env_manager(template_path + "/" + file); env_manager.load(); env_manager.get_all_variables(env_vars); merge_vars(all_env_vars, env_vars); } std::vector required_vars = { "REQUIRES_HOST_ROOT", "REQUIRES_DOCKER", "REQUIRES_DOCKER_ROOT" }; for (const auto & required_var : required_vars) { auto it = find_var(all_env_vars, required_var); if (it == all_env_vars.end()) { error << "Required variable "<< required_var<<" not defined in " << template_path << std::endl; return false; } } return true; } bool template_manager::check_template_shell_scripts_syntax(const std::string &template_path) { if (template_path.empty() || !std::filesystem::exists(template_path)) return false; bool all_passed = true; for (const auto& entry : std::filesystem::recursive_directory_iterator(template_path)) { if (entry.is_regular_file() && entry.path().extension() == ".sh") { std::string script_path = entry.path().string(); std::string command = "bash -n " + script_path + " 2>&1"; FILE* pipe = popen(command.c_str(), "r"); if (!pipe) { error << "Failed to run bash -n on " << entry.path().filename() << std::endl; all_passed = false; continue; } char buffer[256]; std::string output; while (fgets(buffer, sizeof(buffer), pipe) != nullptr) output += buffer; int result = pclose(pipe); if (result != 0 || !output.empty()) { error << "Syntax error in " << entry.path().filename() << ":" << std::endl; error << output << std::endl; all_passed = false; } } } return all_passed; } // ------------------------------------------------------------------------------------------------ // Singletons and template_info // ------------------------------------------------------------------------------------------------ template_manager & gTemplateManager() { static template_manager instance; return instance; } template_info::template_info(const std::string &template_name, const std::string &location_id, const std::filesystem::path &local_template_path) : mTemplateName(template_name), mLocationID(location_id), mTemplateLocalPath(local_template_path), mTemplateValid(template_manager::test_template(local_template_path.string())), mIsSet(!template_name.empty() && !location_id.empty() && !local_template_path.empty()) { if (!std::filesystem::exists(local_template_path)) { error << "Template path does not exist: " << local_template_path << std::endl; return; } } std::filesystem::path template_info::local_template_service_env_path() { return mTemplateLocalPath / "config" / filenames::service_env; } std::filesystem::path template_info::local_template_info_env_path() { return mTemplateLocalPath / filenames::template_info_env; } } // namespace dropshell