psu/psu
Tortue Torche e386126466
Use the realpath of the env file to be able to source it
Without this commit:
This command works correctly:
psu -a deploy -u admin -p password -l http://portainer.local -n mystack -c /path/to/docker-compose.yml -g ./env_vars

But this command doesn't work correctly:
psu -a deploy -u admin -p password -l http://portainer.local -n mystack -c /path/to/docker-compose.yml -g env_vars
2019-05-29 10:25:33 +02:00

477 lines
16 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Deploy/update/undeploy Docker stacks in a Portainer instance.
##########################
# Main entrypoint #
# Globals: #
# AUTH_TOKEN #
# HTTPIE_VERIFY_SSL #
# PORTAINER_URL #
# PORTAINER_USER #
# PORTAINER_PASSWORD #
# PORTAINER_STACK_NAME #
# STACK #
# ACTION #
# Arguments: #
# None #
# Returns: #
# None #
##########################
main() {
set_globals "$@"
# Get Portainer auth token. Will be used on every API request.
echo_verbose "Getting auth token..."
AUTH_TOKEN=$(http \
--check-status \
--ignore-stdin \
--verify=$HTTPIE_VERIFY_SSL \
$PORTAINER_URL/api/auth \
username=$PORTAINER_USER \
password=$PORTAINER_PASSWORD)
check_for_errors $? "$AUTH_TOKEN"
echo_debug "Get auth token response -> $(echo $AUTH_TOKEN | jq -C .)"
AUTH_TOKEN=$(echo $AUTH_TOKEN | jq -r .jwt)
echo_debug "Auth token -> $AUTH_TOKEN"
# Get list of all stacks
echo_verbose "Getting stack $PORTAINER_STACK_NAME..."
local stacks
stacks=$(http \
--check-status \
--ignore-stdin \
--verify=$HTTPIE_VERIFY_SSL \
"$PORTAINER_URL/api/stacks" \
"Authorization: Bearer $AUTH_TOKEN")
check_for_errors $? "$stacks"
echo_debug "Get stacks response -> $(echo $stacks | jq -C .)"
# Get desired stack from stacks list by it's name
STACK=$(echo "$stacks" \
| jq --arg PORTAINER_STACK_NAME "$PORTAINER_STACK_NAME" -jc '.[] | select(.Name == $PORTAINER_STACK_NAME)')
echo_debug "Stack ${PORTAINER_STACK_NAME} -> $(echo $STACK | jq -C .)"
if [ $ACTION == "deploy" ]; then
deploy
exit 0
fi
if [ $ACTION == "undeploy" ]; then
undeploy
exit 0
fi
echo_error "Error: Unknown action \"$ACTION\"."
exit 1
}
################################
# Set globals #
# Globals: #
# ACTION #
# PORTAINER_USER #
# PORTAINER_PASSWORD #
# PORTAINER_URL #
# PORTAINER_STACK_NAME #
# DOCKER_COMPOSE_FILE #
# ENVIRONMENT_VARIABLES_FILE #
# PORTAINER_ENDPOINT #
# PORTAINER_PRUNE #
# HTTPIE_VERIFY_SSL #
# VERBOSE_MODE #
# DEBUG_MODE #
# STRICT_MODE #
# Arguments: #
# None #
# Returns: #
# None #
################################
set_globals() {
# Set arguments through envvars
ACTION=${ACTION}
PORTAINER_USER=${PORTAINER_USER}
PORTAINER_PASSWORD=${PORTAINER_PASSWORD}
PORTAINER_URL=${PORTAINER_URL}
PORTAINER_STACK_NAME=${PORTAINER_STACK_NAME}
DOCKER_COMPOSE_FILE=${DOCKER_COMPOSE_FILE}
ENVIRONMENT_VARIABLES_FILE=${ENVIRONMENT_VARIABLES_FILE}
PORTAINER_ENDPOINT=${PORTAINER_ENDPOINT:-"1"}
PORTAINER_PRUNE=${PORTAINER_PRUNE:-"false"}
HTTPIE_VERIFY_SSL=${HTTPIE_VERIFY_SSL:-"yes"}
VERBOSE_MODE=${VERBOSE_MODE:-"false"}
DEBUG_MODE=${DEBUG_MODE:-"false"}
STRICT_MODE=${STRICT_MODE:-"false"}
# Set arguments through flags (overwrite envvars)
while getopts a:u:p:l:n:c:e:g:rsvdt option; do
case "${option}" in
a) ACTION=${OPTARG} ;;
u) PORTAINER_USER=${OPTARG} ;;
p) PORTAINER_PASSWORD=${OPTARG} ;;
l) PORTAINER_URL=${OPTARG} ;;
n) PORTAINER_STACK_NAME=${OPTARG} ;;
c) DOCKER_COMPOSE_FILE=${OPTARG} ;;
e) PORTAINER_ENDPOINT=${OPTARG} ;;
g) ENVIRONMENT_VARIABLES_FILE=${OPTARG} ;;
r) PORTAINER_PRUNE="true" ;;
s) HTTPIE_VERIFY_SSL="no" ;;
v) VERBOSE_MODE="true" ;;
d) DEBUG_MODE="true" ;;
t) STRICT_MODE="true" ;;
*)
echo_error "Unexpected option ${option}"
exit 1
;;
esac
done
# Print config (only if debug mode is active)
echo_debug "ACTION -> $ACTION"
echo_debug "PORTAINER_USER -> $PORTAINER_USER"
echo_debug "PORTAINER_PASSWORD -> $PORTAINER_PASSWORD"
echo_debug "PORTAINER_URL -> $PORTAINER_URL"
echo_debug "PORTAINER_STACK_NAME -> $PORTAINER_STACK_NAME"
echo_debug "DOCKER_COMPOSE_FILE -> $DOCKER_COMPOSE_FILE"
echo_debug "ENVIRONMENT_VARIABLES_FILE -> $ENVIRONMENT_VARIABLES_FILE"
echo_debug "PORTAINER_ENDPOINT -> $PORTAINER_ENDPOINT"
echo_debug "PORTAINER_PRUNE -> $PORTAINER_PRUNE"
echo_debug "HTTPIE_VERIFY_SSL -> $HTTPIE_VERIFY_SSL"
echo_debug "VERBOSE_MODE -> $VERBOSE_MODE"
echo_debug "DEBUG_MODE -> $DEBUG_MODE"
echo_debug "STRICT_MODE -> $STRICT_MODE"
# Check required arguments have been provided
check_argument "$ACTION" "action" "ACTION" "a"
check_argument "$PORTAINER_USER" "portainer user" "PORTAINER_USER" "u"
check_argument "$PORTAINER_PASSWORD" "portainer password" "PORTAINER_PASSWORD" "p"
check_argument "$PORTAINER_URL" "portainer url" "PORTAINER_URL" "l"
check_argument "$PORTAINER_STACK_NAME" "portainer stack name" "PORTAINER_STACK_NAME" "n"
if [ $ACTION == "deploy" ]; then
check_argument "$DOCKER_COMPOSE_FILE" "docker compose file" "DOCKER_COMPOSE_FILE" "c"
if [ -n "$ENVIRONMENT_VARIABLES_FILE" ] && [[ ! -f "$ENVIRONMENT_VARIABLES_FILE" ]]; then
echo_error "Error: File path \"$ENVIRONMENT_VARIABLES_FILE\" not found for \"ENVIRONMENT_VARIABLES_FILE\" environment variable or the \"-g\" flag."
exit 1
fi
fi
}
############################
# Print an error to stderr #
# Globals: #
# None #
# Arguments: #
# $1 Error message #
# Returns: #
# None #
############################
echo_error() {
local error_message="$@"
local red='\033[0;31m'
local nc='\033[0m'
echo -e "${red}[$(date +'%Y-%m-%dT%H:%M:%S%z')]: ${error_message}${nc}" >&2
}
#######################################
# Check a parameter has been provided #
# Globals: #
# None #
# Arguments: #
# $1 Argument value #
# $2 Argument name #
# $3 Argument envvar #
# $4 Argument flag #
# Returns: #
# None #
#######################################
check_argument() {
local argument_value=$1
local argument_name=$2
local argument_envvar=$3
local argument_flag=$4
if [ -z "$argument_value" ]; then
echo_error "Error: Missing argument \"$argument_name\"."
echo_error "Try setting \"$argument_envvar\" environment variable or using the \"-$argument_flag\" flag."
exit 1
fi
}
###########################################
# Checks for error exit codes from httpie #
# Globals: #
# None #
# Arguments: #
# $1 Httpie exit code #
# $2 Response returned by Portainer API #
# Returns: #
# None #
###########################################
check_for_errors() {
local exit_code=$1
local response=$2
if [ $exit_code -ne 0 ]; then
case $exit_code in
2) echo_error 'Request timed out!' ;;
3) echo_error 'Unexpected HTTP 3xx Redirection!' ;;
4)
echo_error 'HTTP 4xx Client Error!'
echo_error $response
;;
5)
echo_error 'HTTP 5xx Server Error!'
echo_error $response
;;
6) echo_error 'Exceeded --max-redirects=<n> redirects!' ;;
*) echo_error 'Unholy Error!' ;;
esac
exit 1
fi
}
###########################################
# Print message if verbose mode is active #
# Globals: #
# VERBOSE_MODE #
# Arguments: #
# $1 Message #
# Returns: #
# None #
###########################################
echo_verbose() {
local message=$1
local yellow='\033[1;33m'
local nc='\033[0m'
if [ $VERBOSE_MODE == "true" ]; then
echo -e "${yellow}${message}${nc}"
fi
}
#########################################
# Print message if debug mode is active #
# Globals: #
# DEBUG_MODE #
# Arguments: #
# $1 Message #
# Returns: #
# None #
#########################################
echo_debug() {
local message=$1
if [ $DEBUG_MODE == "true" ]; then
echo -e "${message}"
fi
}
################################
# Create/update a stack #
# Globals: #
# STACK #
# DOCKER_COMPOSE_FILE #
# PORTAINER_STACK_NAME #
# PORTAINER_URL #
# ENVIRONMENT_VARIABLES_FILE #
# HTTPIE_VERIFY_SSL #
# PORTAINER_ENDPOINT #
# AUTH_TOKEN #
# Arguments: #
# None #
# Returns: #
# None #
################################
deploy() {
# Read docker-compose file content
local docker_compose_file_content
docker_compose_file_content=$(cat "$DOCKER_COMPOSE_FILE")
# Remove carriage returns
docker_compose_file_content="${docker_compose_file_content//$'\r'/''}"
# Escape double quotes
docker_compose_file_content="${docker_compose_file_content//$'"'/'\"'}"
# Escape newlines
docker_compose_file_content="${docker_compose_file_content//$'\n'/'\n'}"
# If the stack does not exist
if [ -z "$STACK" ]; then
echo_verbose "Stack $PORTAINER_STACK_NAME does not exist."
# Get Docker info
echo_verbose "Getting Docker info..."
local docker_info
docker_info=$(http \
--check-status \
--ignore-stdin \
--verify=$HTTPIE_VERIFY_SSL \
"$PORTAINER_URL/api/endpoints/$PORTAINER_ENDPOINT/docker/info" \
"Authorization: Bearer $AUTH_TOKEN")
check_for_errors $? "$docker_info"
echo_debug "Docker info -> $(echo $docker_info | jq -C .)"
# Get Docker swarm ID
echo_verbose "Getting swarm cluster (if any)..."
local swarm_id
swarm_id=$(echo $docker_info | jq -r ".Swarm.Cluster.ID // empty")
echo_debug "Swarm ID -> $swarm_id"
# If there is no swarm ID
if [ -z "$swarm_id" ];then
echo_verbose "Swarm cluster not found."
echo_verbose "Preparing stack JSON..."
local stack_envvars
stack_envvars="[]"
if [ -n "$ENVIRONMENT_VARIABLES_FILE" ]; then
stack_envvars=$(env_file_to_json)
fi
local data_prefix="{\"Name\":\"$PORTAINER_STACK_NAME\",\"StackFileContent\":\""
local data_suffix="\",\"Env\":"$stack_envvars"}"
echo "$data_prefix$docker_compose_file_content$data_suffix" > json.tmp
echo_debug "Stack JSON -> $(echo $data_prefix$docker_compose_file_content$data_suffix | jq -C .)"
# Create stack for single Docker instance
echo_verbose "Creating stack $PORTAINER_STACK_NAME..."
local create
create=$(http \
--check-status \
--ignore-stdin \
--verify=$HTTPIE_VERIFY_SSL \
--timeout=300 \
"$PORTAINER_URL/api/stacks" \
"Authorization: Bearer $AUTH_TOKEN" \
type==2 \
method==string \
endpointId==$PORTAINER_ENDPOINT \
@json.tmp)
check_for_errors $? "$create"
echo_debug "Create action response -> $(echo $create | jq -C .)"
else
echo_verbose "Swarm cluster found."
echo_verbose "Preparing stack JSON..."
local stack_envvars
stack_envvars="[]"
if [ -n "$ENVIRONMENT_VARIABLES_FILE" ]; then
stack_envvars=$(env_file_to_json)
fi
local data_prefix="{\"Name\":\"$PORTAINER_STACK_NAME\",\"SwarmID\":\"$swarm_id\",\"StackFileContent\":\""
local data_suffix="\",\"Env\":"$stack_envvars"}"
echo "$data_prefix$docker_compose_file_content$data_suffix" > json.tmp
echo_debug "Stack JSON -> $(echo $data_prefix$docker_compose_file_content$data_suffix | jq -C .)"
# Create stack for Docker swarm
echo_verbose "Creating stack $PORTAINER_STACK_NAME..."
local create
create=$(http \
--check-status \
--ignore-stdin \
--verify=$HTTPIE_VERIFY_SSL \
--timeout=300 \
"$PORTAINER_URL/api/stacks" \
"Authorization: Bearer $AUTH_TOKEN" \
type==1 \
method==string \
endpointId==$PORTAINER_ENDPOINT \
@json.tmp)
check_for_errors $? "$create"
echo_debug "Create action response -> $(echo $create | jq -C .)"
fi
rm json.tmp
else
if [ $STRICT_MODE == "true" ]; then
echo_error "Error: Stack $PORTAINER_STACK_NAME already exists."
exit 1
fi
echo_verbose "Stack $PORTAINER_STACK_NAME exists."
echo_verbose "Preparing stack JSON..."
local stack_id
stack_id="$(echo "$STACK" | jq -j ".Id")"
local stack_envvars
stack_envvars="$(echo -n "$STACK" | jq ".Env" -jc)"
if [ -n "$ENVIRONMENT_VARIABLES_FILE" ]; then
local new_stack_envvars
new_stack_envvars=$(env_file_to_json)
stack_envvars="$(echo "${new_stack_envvars}${stack_envvars}" | jq -sjc 'add | unique_by(.name)')"
fi
local data_prefix="{\"Id\":\"$stack_id\",\"StackFileContent\":\""
local data_suffix="\",\"Env\":"$stack_envvars",\"Prune\":$PORTAINER_PRUNE}"
echo "$data_prefix$docker_compose_file_content$data_suffix" > json.tmp
echo_debug "Stack JSON -> $(echo $data_prefix$docker_compose_file_content$data_suffix | jq -C .)"
# Update stack
echo_verbose "Updating stack $PORTAINER_STACK_NAME..."
local update
update=$(http \
--check-status \
--ignore-stdin \
--verify=$HTTPIE_VERIFY_SSL \
--timeout=300 \
PUT "$PORTAINER_URL/api/stacks/$stack_id" \
"Authorization: Bearer $AUTH_TOKEN" \
endpointId==$PORTAINER_ENDPOINT \
@json.tmp)
check_for_errors $? "$update"
echo_debug "Update action response -> $(echo $update | jq -C .)"
rm json.tmp
fi
}
##########################
# Remove a stack #
# Globals: #
# STACK #
# PORTAINER_STACK_NAME #
# PORTAINER_URL #
# HTTPIE_VERIFY_SSL #
# AUTH_TOKEN #
# Arguments: #
# None #
# Returns: #
# None #
##########################
undeploy() {
if [ -z "$STACK" ]; then
if [ $STRICT_MODE == "true" ]; then
echo_error "Error: Stack $PORTAINER_STACK_NAME does not exist."
exit 1
else
echo_verbose "Stack $PORTAINER_STACK_NAME does not exist. No need to undeploy it."
exit 0
fi
fi
echo_verbose "Stack $PORTAINER_STACK_NAME exists."
local stack_id
stack_id="$(echo "$STACK" | jq -j ".Id")"
echo_debug "Stack ID -> $stack_id"
echo_verbose "Deleting stack $PORTAINER_STACK_NAME..."
local delete
delete=$(http \
--check-status \
--ignore-stdin \
--verify=$HTTPIE_VERIFY_SSL \
DELETE "$PORTAINER_URL/api/stacks/$stack_id" \
"Authorization: Bearer $AUTH_TOKEN")
check_for_errors $? "$delete"
echo_debug "Delete action response -> $(echo $delete | jq -C .)"
}
###################################################
# Convert environment variables from file to JSON #
# Globals: #
# ENVIRONMENT_VARIABLES_FILE #
# Arguments: #
# None #
# Returns: #
# JSON string #
###################################################
env_file_to_json() {
echo "$(env -i sh -c "(unset \$(env | sed 's/=.*//'); set -a; . $(readlink -f $ENVIRONMENT_VARIABLES_FILE); set +a; jq -njc 'env | to_entries | map({name: .key, value: .value})')")"
}
main "$@"