- 11 scripts for stack lifecycle (start/stop/restart, update, prune) - Detailed documentation with usage examples and workflow - Updated README.md files with portainer skill info
232 lines
7.5 KiB
Bash
Executable File
232 lines
7.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
CONFIG_PATH="/home/node/.openclaw/workspace/.clawdbot/credentials/portainer/config.json"
|
|
|
|
err() {
|
|
echo "Error: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
require_cmd() {
|
|
command -v "$1" >/dev/null 2>&1 || err "Required command not found: $1"
|
|
}
|
|
|
|
usage() {
|
|
cat >&2 <<'EOF'
|
|
Usage: update-stack.sh "<stack-name>" "<compose-file>" [options]
|
|
|
|
Updates a Portainer stack by name with a new docker-compose file.
|
|
Preserves existing env vars unless --env-file is provided.
|
|
|
|
Arguments:
|
|
stack-name Name of the stack to update
|
|
compose-file Path to the new docker-compose.yml file
|
|
|
|
Options:
|
|
--env-file <file> Path to a file with env vars (format: NAME=value per line)
|
|
If not provided, existing env vars are preserved.
|
|
--pull Force pull images and redeploy (like docker compose down/pull/up).
|
|
--prune-old After update, remove images that were used by this stack
|
|
but are no longer in use (old versions left dangling).
|
|
|
|
Requires config at:
|
|
/home/node/.openclaw/workspace/.clawdbot/credentials/portainer/config.json
|
|
EOF
|
|
exit 2
|
|
}
|
|
|
|
# Parse arguments
|
|
[[ $# -ge 2 ]] || usage
|
|
STACK_NAME="$1"
|
|
COMPOSE_FILE="$2"
|
|
shift 2
|
|
|
|
ENV_FILE=""
|
|
PRUNE_OLD=false
|
|
PULL_IMAGE=false
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--env-file)
|
|
[[ $# -ge 2 ]] || err "--env-file requires a value"
|
|
ENV_FILE="$2"
|
|
shift 2
|
|
;;
|
|
--pull)
|
|
PULL_IMAGE=true
|
|
shift
|
|
;;
|
|
--prune-old)
|
|
PRUNE_OLD=true
|
|
shift
|
|
;;
|
|
*)
|
|
err "Unknown option: $1"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
require_cmd curl
|
|
require_cmd jq
|
|
|
|
[[ -f "$CONFIG_PATH" ]] || err "Missing config file: $CONFIG_PATH"
|
|
[[ -f "$COMPOSE_FILE" ]] || err "Compose file not found: $COMPOSE_FILE"
|
|
|
|
BASE_URL="$(jq -r '.base_url // empty' "$CONFIG_PATH")"
|
|
API_KEY="$(jq -r '.api_key // empty' "$CONFIG_PATH")"
|
|
|
|
[[ -n "$BASE_URL" ]] || err "config.base_url is missing"
|
|
[[ -n "$API_KEY" ]] || err "config.api_key is missing"
|
|
|
|
BASE_URL="${BASE_URL%/}"
|
|
|
|
# Resolve stack ID from name
|
|
STACK_ID="$(bash "$SCRIPT_DIR/get-stack-id.sh" "$STACK_NAME")"
|
|
[[ -n "$STACK_ID" ]] || err "Failed to resolve stack ID for: $STACK_NAME"
|
|
|
|
# Get current stack info for EndpointId and existing env
|
|
STACK_INFO="$(curl -sS -w $'\n%{http_code}' \
|
|
-H "X-API-Key: $API_KEY" \
|
|
"$BASE_URL/api/stacks/$STACK_ID")"
|
|
|
|
http_code="$(printf '%s' "$STACK_INFO" | tail -n1)"
|
|
body="$(printf '%s' "$STACK_INFO" | sed '$d')"
|
|
|
|
if [[ "$http_code" -lt 200 || "$http_code" -ge 300 ]]; then
|
|
msg="$(printf '%s' "$body" | jq -r '.message // empty' 2>/dev/null || true)"
|
|
[[ -n "$msg" ]] || msg="$body"
|
|
err "Failed to fetch stack info (HTTP $http_code): $msg"
|
|
fi
|
|
|
|
ENDPOINT_ID="$(printf '%s' "$body" | jq -r '.EndpointId')"
|
|
[[ -n "$ENDPOINT_ID" && "$ENDPOINT_ID" != "null" ]] || err "Could not determine endpoint ID for stack"
|
|
|
|
# Capture old image IDs before update (if --prune-old)
|
|
OLD_IMAGE_IDS=""
|
|
if [[ "$PRUNE_OLD" == true ]]; then
|
|
# Get containers for this stack using the compose project label
|
|
# The label format is: com.docker.compose.project=<stack-name>
|
|
FILTERS="$(jq -n --arg project "$STACK_NAME" '{"label": ["com.docker.compose.project=\($project)"]}')"
|
|
|
|
CONTAINERS_RESPONSE="$(curl -sS -w $'\n%{http_code}' \
|
|
-H "X-API-Key: $API_KEY" \
|
|
-G \
|
|
--data-urlencode "all=1" \
|
|
--data-urlencode "filters=$FILTERS" \
|
|
"$BASE_URL/api/endpoints/$ENDPOINT_ID/docker/containers/json")"
|
|
|
|
containers_http_code="$(printf '%s' "$CONTAINERS_RESPONSE" | tail -n1)"
|
|
containers_body="$(printf '%s' "$CONTAINERS_RESPONSE" | sed '$d')"
|
|
|
|
if [[ "$containers_http_code" -ge 200 && "$containers_http_code" -lt 300 ]]; then
|
|
# Extract image IDs from containers
|
|
OLD_IMAGE_IDS="$(printf '%s' "$containers_body" | jq -r '.[].ImageID // empty' | sort -u | tr '\n' ' ' | sed 's/ $//')"
|
|
if [[ -n "$OLD_IMAGE_IDS" ]]; then
|
|
echo "Captured old image IDs for pruning: $OLD_IMAGE_IDS"
|
|
else
|
|
echo "No existing container images found to track for pruning"
|
|
PRUNE_OLD=false
|
|
fi
|
|
else
|
|
echo "Warning: Could not fetch containers for prune tracking, skipping prune"
|
|
PRUNE_OLD=false
|
|
fi
|
|
fi
|
|
|
|
# Determine env vars to use
|
|
if [[ -n "$ENV_FILE" ]]; then
|
|
# Read env file and convert to Portainer format
|
|
[[ -f "$ENV_FILE" ]] || err "Env file not found: $ENV_FILE"
|
|
ENV_JSON="$(grep -v '^#' "$ENV_FILE" | grep -v '^$' | while IFS='=' read -r name value; do
|
|
[[ -n "$name" ]] && printf '{"name":"%s","value":"%s"},' "$name" "$value"
|
|
done | sed 's/,$//')"
|
|
ENV_PAYLOAD="[$ENV_JSON]"
|
|
else
|
|
# Preserve existing env vars
|
|
ENV_PAYLOAD="$(printf '%s' "$body" | jq '.Env // []')"
|
|
fi
|
|
|
|
# Read new compose file content
|
|
COMPOSE_CONTENT="$(cat "$COMPOSE_FILE")"
|
|
|
|
# Build JSON payload (include pullImage if --pull flag is set)
|
|
if [[ "$PULL_IMAGE" == true ]]; then
|
|
PAYLOAD="$(jq -n \
|
|
--argjson env "$ENV_PAYLOAD" \
|
|
--arg compose "$COMPOSE_CONTENT" \
|
|
'{Env: $env, StackFileContent: $compose, pullImage: true}')"
|
|
else
|
|
PAYLOAD="$(jq -n \
|
|
--argjson env "$ENV_PAYLOAD" \
|
|
--arg compose "$COMPOSE_CONTENT" \
|
|
'{Env: $env, StackFileContent: $compose}')"
|
|
fi
|
|
|
|
# Build URL
|
|
UPDATE_URL="$BASE_URL/api/stacks/$STACK_ID?endpointId=$ENDPOINT_ID"
|
|
|
|
# Update the stack
|
|
response="$(curl -sS -w $'\n%{http_code}' -X PUT \
|
|
-H "X-API-Key: $API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$PAYLOAD" \
|
|
"$UPDATE_URL")"
|
|
|
|
http_code="$(printf '%s' "$response" | tail -n1)"
|
|
body="$(printf '%s' "$response" | sed '$d')"
|
|
|
|
if [[ "$http_code" -lt 200 || "$http_code" -ge 300 ]]; then
|
|
msg="$(printf '%s' "$body" | jq -r '.message // empty' 2>/dev/null || true)"
|
|
[[ -n "$msg" ]] || msg="$body"
|
|
err "Failed to update stack (HTTP $http_code): $msg"
|
|
fi
|
|
|
|
echo "Stack '$STACK_NAME' (ID: $STACK_ID) updated successfully"
|
|
|
|
# Prune old images if requested and we captured image IDs
|
|
if [[ "$PRUNE_OLD" == true && -n "$OLD_IMAGE_IDS" ]]; then
|
|
echo "Pruning old images no longer in use..."
|
|
|
|
for IMAGE_ID in $OLD_IMAGE_IDS; do
|
|
# Check if image is still in use by any container
|
|
IMAGE_FILTERS="$(jq -n --arg imageId "$IMAGE_ID" '{"ancestor": [$imageId]}')"
|
|
|
|
USAGE_RESPONSE="$(curl -sS -w $'\n%{http_code}' \
|
|
-H "X-API-Key: $API_KEY" \
|
|
-G \
|
|
--data-urlencode "all=1" \
|
|
--data-urlencode "filters=$IMAGE_FILTERS" \
|
|
"$BASE_URL/api/endpoints/$ENDPOINT_ID/docker/containers/json")"
|
|
|
|
usage_http_code="$(printf '%s' "$USAGE_RESPONSE" | tail -n1)"
|
|
usage_body="$(printf '%s' "$USAGE_RESPONSE" | sed '$d')"
|
|
|
|
if [[ "$usage_http_code" -ge 200 && "$usage_http_code" -lt 300 ]]; then
|
|
container_count="$(printf '%s' "$usage_body" | jq 'length')"
|
|
|
|
if [[ "$container_count" -eq 0 ]]; then
|
|
# Image not in use, safe to remove
|
|
DELETE_RESPONSE="$(curl -sS -w $'\n%{http_code}' -X DELETE \
|
|
-H "X-API-Key: $API_KEY" \
|
|
"$BASE_URL/api/endpoints/$ENDPOINT_ID/docker/images/$IMAGE_ID?force=false")"
|
|
|
|
delete_http_code="$(printf '%s' "$DELETE_RESPONSE" | tail -n1)"
|
|
delete_body="$(printf '%s' "$DELETE_RESPONSE" | sed '$d')"
|
|
|
|
if [[ "$delete_http_code" -ge 200 && "$delete_http_code" -lt 300 ]]; then
|
|
echo "Removed unused image: $IMAGE_ID"
|
|
else
|
|
msg="$(printf '%s' "$delete_body" | jq -r '.message // empty' 2>/dev/null || true)"
|
|
[[ -n "$msg" ]] || msg="$delete_body"
|
|
echo "Warning: Could not remove image $IMAGE_ID: $msg"
|
|
fi
|
|
else
|
|
echo "Image $IMAGE_ID still in use by $container_count container(s), skipping"
|
|
fi
|
|
fi
|
|
done
|
|
|
|
echo "Pruning complete"
|
|
fi
|