exec command, and remote execution improvements!
This commit is contained in:
95
TEMPLATES.md
95
TEMPLATES.md
@@ -164,7 +164,16 @@ docker stop "$CONTAINER_NAME" 2>/dev/null || true
|
|||||||
```
|
```
|
||||||
|
|
||||||
### status.sh
|
### 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
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
@@ -178,6 +187,42 @@ else
|
|||||||
fi
|
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
|
### logs.sh
|
||||||
Shows container logs:
|
Shows container logs:
|
||||||
|
|
||||||
@@ -365,6 +410,54 @@ else
|
|||||||
fi
|
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
|
## Testing Templates
|
||||||
|
|
||||||
After creating a template, validate it:
|
After creating a template, validate it:
|
||||||
|
137
source/src/commands/exec.cpp
Normal file
137
source/src/commands/exec.cpp
Normal file
@@ -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 <libassert/assert.hpp>
|
||||||
|
|
||||||
|
namespace dropshell
|
||||||
|
{
|
||||||
|
|
||||||
|
int exec_handler(const CommandContext &ctx);
|
||||||
|
|
||||||
|
static std::vector<std::string> 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<std::string> 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<std::string, std::string> 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
|
@@ -6,6 +6,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <cctype>
|
||||||
#include <libassert/assert.hpp>
|
#include <libassert/assert.hpp>
|
||||||
|
|
||||||
#include "execute.hpp"
|
#include "execute.hpp"
|
||||||
@@ -203,6 +204,28 @@ namespace dropshell
|
|||||||
return commandstr;
|
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
|
// construct_cmd
|
||||||
// ----------------------------------------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------------------------------------
|
||||||
@@ -220,8 +243,29 @@ namespace dropshell
|
|||||||
cmdstr += "cd " + quote(mDir) + " && ";
|
cmdstr += "cd " + quote(mDir) + " && ";
|
||||||
|
|
||||||
if (!mVars.empty())
|
if (!mVars.empty())
|
||||||
|
{
|
||||||
|
// Export variables so they're available for expansion in the command
|
||||||
for (const auto &env_var : mVars)
|
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;
|
cmdstr += mCmd;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user