#!/usr/bin/env bash ####################################### # Check a parameter has been provided # # Arguments: # # $1 Argument value # # $2 Argument name # # $3 Argument envvar # # $4 Argument flag # ####################################### 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: Missing argument \"$argument_name\"." echo "Try setting \"$argument_envvar\" environment variable or using the \"-$argument_flag\" flag." exit 1 fi } ########################################### # Checks for error exit codes from httpie # # Arguments: # # $1 Httpie exit code # # $2 Response returned by Portainer API # ########################################### check_for_errors () { local exit_code=$1 local response=$2 if [ $exit_code -ne 0 ]; then case $exit_code in 2) echo 'Request timed out!' ;; 3) echo 'Unexpected HTTP 3xx Redirection!' ;; 4) echo 'HTTP 4xx Client Error!' && echo $response ;; 5) echo 'HTTP 5xx Server Error!' && echo $response ;; 6) echo 'Exceeded --max-redirects= redirects!' ;; *) echo 'Unholy Error!' ;; esac exit 1 fi } # Set arguments through envvars PORTAINER_USER=${PORTAINER_USER} PORTAINER_PASSWORD=${PORTAINER_PASSWORD} PORTAINER_URL=${PORTAINER_URL} PORTAINER_STACK_NAME=${PORTAINER_STACK_NAME} DOCKER_COMPOSE_FILE=${DOCKER_COMPOSE_FILE} PORTAINER_ENDPOINT=${PORTAINER_ENDPOINT:-"1"} PORTAINER_PRUNE=${PORTAINER_PRUNE:-"false"} HTTPIE_VERIFY_SSL=${HTTPIE_VERIFY_SSL:-"yes"} # Set arguments through flags while getopts u:p:l:n:c:e:rs option; do case "${option}" in 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:-$PORTAINER_ENDPOINT} ;; r) PORTAINER_PRUNE=${"true":-$PORTAINER_PRUNE} ;; s) HTTPIE_VERIFY_SSL=${"no":-$HTTPIE_VERIFY_SSL} ;; esac done # Check required arguments have been provided 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" check_argument "$DOCKER_COMPOSE_FILE" "docker compose file" "DOCKER_COMPOSE_FILE" "c" STACK_NAME=$PORTAINER_STACK_NAME STACK_YAML_PATH=$DOCKER_COMPOSE_FILE STACK_YAML_CONTENT=$(cat "$STACK_YAML_PATH") # Escape carriage returns STACK_YAML_CONTENT="${STACK_YAML_CONTENT//$'\r'/''}" # Escape double quotes STACK_YAML_CONTENT="${STACK_YAML_CONTENT//$'"'/'\"'}" # Escape newlines STACK_YAML_CONTENT="${STACK_YAML_CONTENT//$'\n'/'\n'}" echo "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" AUTH_TOKEN=$(echo $AUTH_TOKEN | jq -r .jwt) echo "Done" echo "Getting stack $STACK_NAME..." STACKS=$(http \ --check-status \ --ignore-stdin \ --verify=$HTTPIE_VERIFY_SSL \ "$PORTAINER_URL/api/stacks" \ "Authorization: Bearer $AUTH_TOKEN") check_for_errors $? "$STACKS" STACK=$(echo "$STACKS" \ | jq --arg STACK_NAME "$STACK_NAME" -jc '.[] | select(.Name == $STACK_NAME)') if [ -z "$STACK" ]; then echo "Result: Stack $STACK_NAME not found." echo "Getting swarm cluster (if any)..." SWARM_ID=$(http \ --check-status \ --ignore-stdin \ --verify=$HTTPIE_VERIFY_SSL \ "$PORTAINER_URL/api/endpoints/$PORTAINER_ENDPOINT/docker/info" \ "Authorization: Bearer $AUTH_TOKEN") check_for_errors $? "$SWARM_ID" SWARM_ID=$(echo $SWARM_ID | jq -r ".Swarm.Cluster.ID // empty") echo "Creating stack $STACK_NAME..." if [ -z "$SWARM_ID" ];then DATA_PREFIX="{\"Name\":\"$STACK_NAME\",\"StackFileContent\":\"" DATA_SUFFIX="\"}" echo "$DATA_PREFIX$STACK_YAML_CONTENT$DATA_SUFFIX" > json.tmp 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) else DATA_PREFIX="{\"Name\":\"$STACK_NAME\",\"SwarmID\":\"$SWARM_ID\",\"StackFileContent\":\"" DATA_SUFFIX="\"}" echo "$DATA_PREFIX$STACK_YAML_CONTENT$DATA_SUFFIX" > json.tmp 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) fi check_for_errors $? "$CREATE" rm json.tmp else echo "Result: Stack $STACK_NAME found." STACK_ID="$(echo "$STACK" | jq -j ".Id")" STACK_ENV_VARS="$(echo -n "$STACK"| jq ".Env" -jc)" DATA_PREFIX="{\"Id\":\"$STACK_ID\",\"StackFileContent\":\"" DATA_SUFFIX="\",\"Env\":"$STACK_ENV_VARS",\"Prune\":$PORTAINER_PRUNE}" echo "$DATA_PREFIX$STACK_YAML_CONTENT$DATA_SUFFIX" > json.tmp echo "Updating stack $STACK_NAME..." 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" rm json.tmp fi echo "Done"