diff --git a/Dockerfile b/Dockerfile index 348d617..4e60329 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,7 @@ ENV LANG="en_US.UTF-8" \ PORTAINER_URL="" \ PORTAINER_STACK_NAME="" \ DOCKER_COMPOSE_FILE="" \ + ENVIRONMENT_VARIABLES_FILE="" \ PORTAINER_PRUNE="false" \ PORTAINER_ENDPOINT="1" \ HTTPIE_VERIFY_SSL="yes" \ diff --git a/README.md b/README.md index f372c0d..ed470dc 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ This is particularly useful for CI/CD pipelines. - `PORTAINER_URL` (string, required): URL to Portainer - `PORTAINER_STACK_NAME` (string, required): Stack name - `DOCKER_COMPOSE_FILE` (string, required if action=deploy): Path to doker-compose file +- `ENVIRONMENT_VARIABLES_FILE` (string, optional, only used when action=deploy or action=update): Path to file with environment variables to be used by the stack. See [stack environment variables](#stack-environment-variables) below. - `PORTAINER_PRUNE` ("true" or "false", optional): Whether to prune unused containers or not. Defaults to `"false"`. - `PORTAINER_ENDPOINT` (int, optional): Which endpoint to use. Defaults to `1`. - `HTTPIE_VERIFY_SSL` ("yes" or "no", optional): Whether to verify SSL certificate or not. Defaults to `"yes"`. @@ -60,6 +61,7 @@ export PORTAINER_PASSWORD="password" export PORTAINER_URL="http://portainer.local" export PORTAINER_STACK_NAME="mystack" export DOCKER_COMPOSE_FILE="/path/to/docker-compose.yml" +export ENVIRONMENT_VARIABLES_FILE="/path/to/env_vars_file" ./psu ``` @@ -84,6 +86,7 @@ This is more suitable for standalone script usage. - `-l` (string, required): URL to Portainer - `-n` (string, required): Stack name - `-c` (string, required if action=deploy): Path to doker-compose file +- `-g` (string, optional, only used when action=deploy or action=update): Path to file with environment variables to be used by the stack. See [stack environment variables](#stack-environment-variables) below. - `-r` ("true" or "false", optional): Whether to prune unused containers or not. Defaults to `"false"`. - `-e` (int, optional): Which endpoint to use. Defaults to `1`. - `-s` ("yes" or "no", optional): Whether to verify SSL certificate or not. Defaults to `"yes"`. @@ -94,13 +97,26 @@ This is more suitable for standalone script usage. #### Examples ```bash -./psu -a deploy -u admin -p password -l http://portainer.local -n mystack -c /path/to/docker-compose.yml +./psu -a deploy -u admin -p password -l http://portainer.local -n mystack -c /path/to/docker-compose.yml -g /path/to/env_vars_file ``` ```bash ./psu -a undeploy -u admin -p password -l http://portainer.local -n mystack ``` +### Stack environment variables + +There can be set environment variables for each stack, be it a new deployment or an update. For example: + +```bash +touch .env +echo "MYSQL_ROOT_PASSWORD=agoodpassword" >> .env +echo "ALLOWED_HOSTS=*" >> .env +./psu -a deploy -u admin -p password -l http://portainer.local -n django-stack -c /path/to/docker-compose.yml -g env_vars +``` + +Stack environment variables can be enabled through [ENVIRONMENT_VARIABLES_FILE envvar](#with-envvars) or [-g flag](#with-flags). + ### Verbose mode In verbose mode the script prints execution steps. diff --git a/psu b/psu index c68fd7d..021f402 100755 --- a/psu +++ b/psu @@ -66,26 +66,27 @@ main() { exit 1 } -########################## -# Set globals # -# Globals: # -# ACTION # -# PORTAINER_USER # -# PORTAINER_PASSWORD # -# PORTAINER_URL # -# PORTAINER_STACK_NAME # -# DOCKER_COMPOSE_FILE # -# PORTAINER_ENDPOINT # -# PORTAINER_PRUNE # -# HTTPIE_VERIFY_SSL # -# VERBOSE_MODE # -# DEBUG_MODE # -# STRICT_MODE # -# Arguments: # -# None # -# Returns: # -# None # -########################## +################################ +# 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} @@ -94,6 +95,7 @@ set_globals() { 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"} @@ -102,7 +104,7 @@ set_globals() { STRICT_MODE=${STRICT_MODE:-"false"} # Set arguments through flags (overwrite envvars) - while getopts a:u:p:l:n:c:e:rsvdt option; do + while getopts a:u:p:l:n:c:e:g:rsvdt option; do case "${option}" in a) ACTION=${OPTARG} ;; u) PORTAINER_USER=${OPTARG} ;; @@ -111,6 +113,7 @@ set_globals() { 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" ;; @@ -130,6 +133,7 @@ set_globals() { 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" @@ -145,6 +149,10 @@ set_globals() { 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 } @@ -254,21 +262,22 @@ echo_debug() { fi } -########################## -# Create/update a stack # -# Globals: # -# STACK # -# DOCKER_COMPOSE_FILE # -# PORTAINER_STACK_NAME # -# PORTAINER_URL # -# HTTPIE_VERIFY_SSL # -# PORTAINER_ENDPOINT # -# AUTH_TOKEN # -# Arguments: # -# None # -# Returns: # -# None # -########################## +################################ +# 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 @@ -304,14 +313,19 @@ deploy() { 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="\"}" + 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 .)" @@ -335,8 +349,13 @@ deploy() { 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="\"}" + 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 .)" @@ -370,12 +389,17 @@ deploy() { local stack_id stack_id="$(echo "$STACK" | jq -j ".Id")" local stack_envvars - stack_envvars="$(echo -n "$STACK"| jq ".Env" -jc)" + 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 -n "${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 @@ -390,7 +414,7 @@ deploy() { @json.tmp) check_for_errors $? "$update" echo_debug "Update action response -> $(echo $update | jq -C .)" - + rm json.tmp fi } @@ -436,4 +460,17 @@ undeploy() { 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 $(cat $ENVIRONMENT_VARIABLES_FILE) jq -n 'env | to_entries | map({name: .key, value: .value})')" +} + main "$@"