Files
docker-infrastructure/portainer.sh
T

310 lines
8.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Portainer management script
# Usage:
# ./portainer.sh list
# ./portainer.sh redeploy <stack-name>
# ./portainer.sh deploy <stack-name> <compose-path>
# ./portainer.sh get-env <stack-name>
# ./portainer.sh set-env <stack-name> KEY=VALUE [KEY=VALUE ...]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CREDENTIALS_FILE="$SCRIPT_DIR/.credentials"
if [[ ! -f "$CREDENTIALS_FILE" ]]; then
echo "Error: credentials file not found at $CREDENTIALS_FILE" >&2
exit 1
fi
# shellcheck source=.credentials
source "$CREDENTIALS_FILE"
API="$PORTAINER_URL/api"
ENDPOINT_ID="$PORTAINER_ENDPOINT_ID"
AUTH_HEADER="X-API-Key: $PORTAINER_API_TOKEN"
# --------------------------------------------------------------------------
# Helpers
# --------------------------------------------------------------------------
api_get() {
curl -sk -H "$AUTH_HEADER" "$API/$1"
}
api_put() {
curl -sk -X PUT -H "$AUTH_HEADER" -H "Content-Type: application/json" \
-d "$2" "$API/$1"
}
api_post() {
curl -sk -X POST -H "$AUTH_HEADER" -H "Content-Type: application/json" \
-d "$2" "$API/$1"
}
get_stack_by_name() {
local name="$1"
api_get "stacks?filters=%7B%22EndpointID%22%3A${ENDPOINT_ID}%7D" \
| python3 -c "
import json, sys
stacks = json.load(sys.stdin)
matches = [s for s in stacks if s['Name'] == '$name']
if not matches:
sys.exit(1)
s = matches[0]
print(s['Id'])
"
}
get_stack_json_by_name() {
local name="$1"
api_get "stacks?filters=%7B%22EndpointID%22%3A${ENDPOINT_ID}%7D" \
| python3 -c "
import json, sys
stacks = json.load(sys.stdin)
matches = [s for s in stacks if s['Name'] == '$name']
if not matches:
sys.exit(1)
print(json.dumps(matches[0]))
"
}
# --------------------------------------------------------------------------
# Commands
# --------------------------------------------------------------------------
cmd_list() {
echo "Fetching stacks..."
api_get "stacks?filters=%7B%22EndpointID%22%3A${ENDPOINT_ID}%7D" \
| python3 -c "
import json, sys
stacks = json.load(sys.stdin)
stacks.sort(key=lambda s: s['Id'])
status_map = {1: 'active', 2: 'inactive'}
print(f'{'ID':<6} {'STATUS':<10} NAME')
print('-' * 40)
for s in stacks:
status = status_map.get(s['Status'], str(s['Status']))
print(f\"{s['Id']:<6} {status:<10} {s['Name']}\")
"
}
cmd_redeploy() {
local name="${1:-}"
if [[ -z "$name" ]]; then
echo "Usage: $0 redeploy <stack-name>" >&2
exit 1
fi
echo "Looking up stack '$name'..."
local stack_json
if ! stack_json=$(get_stack_json_by_name "$name"); then
echo "Error: stack '$name' not found" >&2
exit 1
fi
local stack_id
stack_id=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['Id'])" "$stack_json")
echo "Found stack ID: $stack_id"
# Preserve existing env vars — git/redeploy clears them if env is omitted
local payload
payload=$(python3 -c "
import json, sys
stack = json.loads(sys.argv[1])
env = stack.get('Env') or []
print(json.dumps({'pullImage': True, 'prune': False, 'env': env}))
" "$stack_json")
echo "Redeploying..."
local response
response=$(api_put "stacks/${stack_id}/git/redeploy?endpointId=${ENDPOINT_ID}" "$payload")
python3 -c "
import json, sys
d = json.loads(sys.argv[1])
if 'message' in d and 'Id' not in d:
print('Error:', d['message'])
sys.exit(1)
hash = d.get('GitConfig', {}).get('ConfigHash', 'unknown')[:10]
print(f'Done. ConfigHash: {hash}')
" "$response"
}
cmd_deploy() {
local name="${1:-}"
local compose_path="${2:-}"
if [[ -z "$name" || -z "$compose_path" ]]; then
echo "Usage: $0 deploy <stack-name> <compose-path> [KEY=VALUE ...]" >&2
echo " Example: $0 deploy myapp myapp/docker-compose.yml FOO=bar BAZ=qux" >&2
exit 1
fi
shift 2
local kvs=("$@")
echo "Creating stack '$name' from $compose_path..."
local payload
payload=$(python3 -c "
import json, sys
kvs = sys.argv[1:]
env_list = []
for kv in kvs:
eq = kv.index('=')
env_list.append({'name': kv[:eq], 'value': kv[eq+1:]})
print(json.dumps({
'name': '$name',
'repositoryURL': '$GITEA_REPO_URL',
'repositoryReferenceName': 'refs/heads/main',
'composeFile': '$compose_path',
'repositoryAuthentication': True,
'repositoryUsername': '$GITEA_USER',
'repositoryPassword': '$GITEA_TOKEN',
'env': env_list,
}))
" "${kvs[@]}")
local response
response=$(api_post "stacks/create/standalone/repository?endpointId=${ENDPOINT_ID}" "$payload")
python3 -c "
import json, sys
d = json.load(sys.stdin)
if 'message' in d and 'Id' not in d:
print('Error:', d['message'])
sys.exit(1)
print(f\"Done. Stack ID: {d['Id']}, Name: {d['Name']}\")
" <<< "$response"
}
cmd_get_env() {
local name="${1:-}"
if [[ -z "$name" ]]; then
echo "Usage: $0 get-env <stack-name>" >&2
exit 1
fi
echo "Looking up stack '$name'..."
local stack_json
if ! stack_json=$(get_stack_json_by_name "$name"); then
echo "Error: stack '$name' not found" >&2
exit 1
fi
python3 -c "
import json, sys
s = json.loads(sys.argv[1])
env = s.get('Env') or []
if not env:
print('(no env vars set)')
else:
for e in sorted(env, key=lambda x: x['name']):
print(f\"{e['name']}={e['value']}\")
" "$stack_json"
}
cmd_set_env() {
local name="${1:-}"
shift || true
if [[ -z "$name" || $# -eq 0 ]]; then
echo "Usage: $0 set-env <stack-name> KEY=VALUE [KEY=VALUE ...]" >&2
echo " Example: $0 set-env authelia SECRET_KEY=abc123 OTHER_KEY=xyz" >&2
exit 1
fi
echo "Looking up stack '$name'..."
local stack_json
if ! stack_json=$(get_stack_json_by_name "$name"); then
echo "Error: stack '$name' not found" >&2
exit 1
fi
local stack_id
stack_id=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['Id'])" "$stack_json")
echo "Found stack ID: $stack_id"
local kvs=("$@")
local payload
payload=$(python3 -c "
import json, sys
stack = json.loads(sys.argv[1])
new_kvs = sys.argv[2:]
# Merge env vars: preserve existing, override/add new
env_dict = {e['name']: e['value'] for e in (stack.get('Env') or [])}
for kv in new_kvs:
eq = kv.index('=')
env_dict[kv[:eq]] = kv[eq+1:]
env_list = [{'name': k, 'value': v} for k, v in env_dict.items()]
git = stack.get('GitConfig') or {}
if git:
# Use git/redeploy endpoint (pullImage:false = redeploy with existing images only)
p = {'pullImage': False, 'prune': False, 'env': env_list}
else:
print('Error: string-based stacks are not supported; update env vars via Portainer UI.', file=sys.stderr)
sys.exit(2)
print(json.dumps(p))
" "$stack_json" "${kvs[@]}")
echo "Updating env vars (redeploys with existing images)..."
local endpoint
local stack_json_check
stack_json_check=$(python3 -c "
import json, sys
stack = json.loads(sys.argv[1])
git = stack.get('GitConfig') or {}
print('git' if git else 'string')
" "$stack_json")
local response
if [[ "$stack_json_check" == "git" ]]; then
response=$(api_put "stacks/${stack_id}/git/redeploy?endpointId=${ENDPOINT_ID}" "$payload")
else
echo "Error: string-based stacks are not supported; update env vars via Portainer UI." >&2
exit 2
fi
python3 -c "
import json, sys
try:
d = json.loads(sys.argv[1])
except Exception:
print('Failed to parse response:', sys.argv[1][:300])
sys.exit(1)
if 'message' in d and 'Id' not in d:
print('Error:', d['message'])
if 'details' in d:
print('Details:', d['details'])
sys.exit(1)
updated = len(d.get('Env') or [])
print(f'Done. Stack has {updated} env var(s) set.')
" "$response"
}
# --------------------------------------------------------------------------
# Dispatch
# --------------------------------------------------------------------------
command="${1:-}"
case "$command" in
list) cmd_list ;;
redeploy) cmd_redeploy "${2:-}" ;;
deploy) cmd_deploy "${@:2}" ;;
get-env) cmd_get_env "${2:-}" ;;
set-env) cmd_set_env "${2:-}" "${@:3}" ;;
*)
echo "Usage: $0 <command> [args]"
echo ""
echo "Commands:"
echo " list List all stacks"
echo " redeploy <stack-name> Pull latest git commit and redeploy"
echo " deploy <stack-name> <path> [K=V ...] Create new git-linked stack with optional env vars"
echo " get-env <stack-name> Show env vars for a stack"
echo " set-env <stack-name> KEY=VAL [...] Set env vars (redeploys without new image pull)"
exit 1
;;
esac