From 117af635a395f59e5046232db049d29550e14420 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 13 Sep 2025 07:28:00 +1200 Subject: [PATCH] exec command, and remote execution improvements! --- TEMPLATES.md | 95 +++++++++++++++++++++++- source/src/commands/exec.cpp | 137 +++++++++++++++++++++++++++++++++++ source/src/utils/execute.cpp | 46 +++++++++++- 3 files changed, 276 insertions(+), 2 deletions(-) create mode 100644 source/src/commands/exec.cpp diff --git a/TEMPLATES.md b/TEMPLATES.md index 84eaa88..dad73ba 100644 --- a/TEMPLATES.md +++ b/TEMPLATES.md @@ -164,7 +164,16 @@ docker stop "$CONTAINER_NAME" 2>/dev/null || true ``` ### status.sh -Reports the service status: +Reports the service status. + +**Expected Output Format:** +- Must output a single line with one of these exact status values: + - `Running` - Service is active and operational + - `Stopped` - Service is stopped but configured + - `Error` - Service is in an error state + - `Unknown` - Status cannot be determined + +The output is parsed by Dropshell for the `list` command, so it must be exactly one of these values with no additional text. ```bash #!/bin/bash @@ -178,6 +187,42 @@ else fi ``` +For more complex status checks: + +```bash +#!/bin/bash +source "${AGENT_PATH}/common.sh" +_check_required_env_vars "CONTAINER_NAME" + +# Check if container exists +if ! docker ps -a --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then + echo "Unknown" + exit 0 +fi + +# Check container state +STATE=$(docker inspect -f '{{.State.Status}}' "$CONTAINER_NAME" 2>/dev/null) +case "$STATE" in + running) + # Additional health check if needed + if docker inspect -f '{{.State.Health.Status}}' "$CONTAINER_NAME" 2>/dev/null | grep -q "unhealthy"; then + echo "Error" + else + echo "Running" + fi + ;; + exited|stopped) + echo "Stopped" + ;; + restarting|paused) + echo "Error" + ;; + *) + echo "Unknown" + ;; +esac +``` + ### logs.sh Shows container logs: @@ -365,6 +410,54 @@ else fi ``` +## Docker Compose Templates + +When creating templates that use Docker Compose: + +### Important: Always Build on Install + +When using `docker compose` with custom images (defined with `build:` in your compose file), **always use the `--build` flag** in your `install.sh`: + +```bash +# CORRECT - Forces rebuild of images during installation +docker compose up -d --build + +# WRONG - May use stale cached images +docker compose up -d +``` + +This ensures that: +- Custom images are rebuilt with the latest code changes +- No stale images are used from previous installations +- The service always starts with the most recent image version + +### Example Docker Compose install.sh + +```bash +#!/bin/bash +source "${AGENT_PATH}/common.sh" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +_check_required_env_vars "CONTAINER_NAME" +_check_docker_installed || _die "Docker test failed" + +# Stop any existing containers +docker compose down || true + +# Start with rebuild to ensure fresh images +docker compose up -d --build || _die "Failed to start services" + +echo "Installation of ${CONTAINER_NAME} complete" +``` + +### Docker Compose Best Practices + +1. **Always include `--build`** in install.sh when using custom images +2. **Use `.env` files** for configuration that Docker Compose can read +3. **Define service names** consistently with `CONTAINER_NAME` +4. **Handle cleanup** properly in stop.sh and uninstall.sh using `docker compose down` +5. **Use named volumes** for persistent data that matches Dropshell conventions + ## Testing Templates After creating a template, validate it: diff --git a/source/src/commands/exec.cpp b/source/src/commands/exec.cpp new file mode 100644 index 0000000..a6552d3 --- /dev/null +++ b/source/src/commands/exec.cpp @@ -0,0 +1,137 @@ +#include "command_registry.hpp" +#include "config.hpp" +#include "utils/utils.hpp" +#include "utils/directories.hpp" +#include "utils/execute.hpp" +#include "shared_commands.hpp" +#include "servers.hpp" +#include "services.hpp" +#include "templates.hpp" +#include "utils/output.hpp" +#include + +namespace dropshell +{ + + int exec_handler(const CommandContext &ctx); + + static std::vector exec_name_list = {"exec"}; + + // Static registration + struct ExecCommandRegister + { + ExecCommandRegister() + { + CommandRegistry::instance().register_command({exec_name_list, + exec_handler, + shared_commands::std_autocomplete, + false, // hidden + true, // requires_config + true, // requires_install + 3, // min_args (server, service, command) + -1, // max_args (unlimited for command with args) + "exec SERVER SERVICE COMMAND [ARGS...]", + "Execute a command on a server within a service environment.", + R"( + + exec SERVER SERVICE COMMAND [ARGS...] Execute a command in the service's environment. + + This command runs a command on the remote server with the service's environment + variables loaded (including those from service.env and the usual variables). + The command is executed in the service's template directory context. + )"}); + } + } exec_command_register; + + int exec_handler(const CommandContext &ctx) + { + if (ctx.args.size() < 3) + { + error << "Server name, service name, and command are required" << std::endl; + return 1; + } + + std::string server = safearg(ctx.args, 0); + std::string service = safearg(ctx.args, 1); + std::string command = safearg(ctx.args, 2); + + // Collect any additional arguments for the command + std::vector command_args; + for (size_t i = 3; i < ctx.args.size(); ++i) + { + command_args.push_back(ctx.args[i]); + } + + // Validate server + ServerConfig server_env(server); + if (!server_env.is_valid()) + { + error << "Server " << server << " is not valid" << std::endl; + return 1; + } + + // Validate service name + if (!legal_service_name(service)) + { + error << "Service name contains illegal characters: " << service << std::endl; + return 1; + } + + // Get service info to validate it exists + LocalServiceInfo sinfo = get_service_info(server, service); + if (!SIvalid(sinfo)) + { + error << "Service " << service << " is not valid on server " << server << std::endl; + return 1; + } + + // Get the user for this service + std::string user = server_env.get_user_for_service(service); + + // Get all service environment variables + std::map env_vars; + if (!get_all_service_env_vars(server, service, env_vars)) + { + error << "Failed to get environment variables for service " << service << std::endl; + return 1; + } + + // Add HOST_NAME like other commands do + env_vars["HOST_NAME"] = server_env.get_SSH_HOST(); + + // Get the remote service template path for working directory + std::string remote_service_template_path = remotepath(server, user).service_template(service); + + // Build the command string with arguments + std::string full_command = command; + for (const auto &arg : command_args) + { + full_command += " " + quote(dequote(trim(arg))); + } + + // Create the command structure with environment variables + // Note: execute_ssh_command will automatically use bb64 to encode and execute this safely + sCommand scommand(remote_service_template_path, full_command, env_vars); + + // Execute the command on the remote server + info << "Executing command on " << server << "/" << service << ": " << command; + if (!command_args.empty()) + { + rawout << " with args:"; + for (const auto &arg : command_args) + rawout << " " << arg; + } + rawout << std::endl; + + bool success = execute_ssh_command(server_env.get_SSH_INFO(user), scommand, cMode::Interactive); + + if (!success) + { + error << "Command execution failed" << std::endl; + return 1; + } + + return 0; + } + +} // namespace dropshell \ No newline at end of file diff --git a/source/src/utils/execute.cpp b/source/src/utils/execute.cpp index 9b95868..8cedfc1 100644 --- a/source/src/utils/execute.cpp +++ b/source/src/utils/execute.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "execute.hpp" @@ -203,6 +204,28 @@ namespace dropshell return commandstr; } + // ---------------------------------------------------------------------------------------------------------- + // sanitize_env_var_name - Basic sanity check for environment variable names + // ---------------------------------------------------------------------------------------------------------- + static bool is_valid_env_var_name(const std::string &name) + { + if (name.empty()) + return false; + + // Must start with letter or underscore + if (!std::isalpha(name[0]) && name[0] != '_') + return false; + + // Rest must be alphanumeric or underscore + for (char c : name) + { + if (!std::isalnum(c) && c != '_') + return false; + } + + return true; + } + // ---------------------------------------------------------------------------------------------------------- // construct_cmd // ---------------------------------------------------------------------------------------------------------- @@ -220,8 +243,29 @@ namespace dropshell cmdstr += "cd " + quote(mDir) + " && "; if (!mVars.empty()) + { + // Export variables so they're available for expansion in the command for (const auto &env_var : mVars) - cmdstr += env_var.first + "=" + quote(dequote(trim(env_var.second))) + " "; + { + // Basic sanity check - skip invalid variable names + if (!is_valid_env_var_name(env_var.first)) + { + error << "Skipping invalid environment variable name: " << env_var.first << std::endl; + continue; + } + + // Very basic check for completely broken values that could break the command + // We still use quote() for proper escaping, but warn about suspicious values + const std::string &value = env_var.second; + if (value.find('\0') != std::string::npos) + { + error << "Skipping environment variable with null byte: " << env_var.first << std::endl; + continue; + } + + cmdstr += "export " + env_var.first + "=" + quote(dequote(trim(value))) + " && "; + } + } cmdstr += mCmd;