diff --git a/psu b/psu index b357ff3..eb911cf 100644 --- a/psu +++ b/psu @@ -48,6 +48,7 @@ main() { "masked-variables;-m;--masked-variables;In debug and/or verbose mode, the value of sensitive variables will be hidden, useful in CI to avoid leaking passwords/tokens in logs. Possible values: true|extended|false. Defaults to false" "quiet;-q;--quiet;Display the minimum of information or nothing, UNIX/Linux friendly. Defaults to false" "strict;-t;--strict;Never updates an existent stack nor removes an inexistent one, and instead exits with an error. Defaults to false" + "lint;-L;--lint;Validate the Docker compose/stack file before deploying the stack (only used when action=deploy). Defaults to false" "help;-h;--help;Display help message. To display help of a given action, run: 'psu --help'" "version;-V;--version;Display the version of this program" "secure;-s;--secure[=yes|no];DEPRECATED: Use the '--insecure' option instead. Enable or disable the host's SSL certificate verification. Defaults to 'yes'" @@ -56,7 +57,7 @@ main() { ACTIONS_TABLE=( # action_name;description[;required_option_key1|required_option_key2...][;optional_option_key1|optional_option_key2...] - "deploy;Deploy the stack;url|user|password|name|compose-file;auth-token|endpoint|env-file|prune|insecure|verbose|debug|masked-variables|strict" + "deploy;Deploy the stack;url|user|password|name|compose-file;auth-token|endpoint|lint|env-file|prune|insecure|verbose|debug|masked-variables|strict" "undeploy;Undeploy/remove the stack;url|user|password|name;auth-token|endpoint|insecure|verbose|debug|masked-variables|strict" "login;Log in to a Portainer instance;url|user|password;insecure|verbose|debug|masked-variables" "list;Lists of the stacks already deployed;url|user|password;auth-token|endpoint|quiet|insecure|verbose|debug|masked-variables|help" @@ -67,14 +68,15 @@ main() { "tasks;Lists tasks for the current stack;url|user|password|name;auth-token|endpoint|service|detect-job|timeout|quiet|insecure|verbose|debug|masked-variables" "tasks:healthy;Lists tasks who are running correctly for the current stack;url|user|password|name;auth-token|endpoint|service|detect-job|timeout|quiet|insecure|verbose|debug|masked-variables" "containers;Lists containers running for the current stack;url|user|password|name;auth-token|endpoint|service|quiet|insecure|verbose|debug|masked-variables" - "actions;Lists available actions of this program;;verbose|debug|masked-variables" + "lint;Validate the Docker compose/stack file;compose-file;verbose|debug" + "actions;Lists available actions of this program;;verbose|debug" "help;Display help message" "version;Display this program version" ) # Special actions who display text only - # No HTTP requests are done - ACTIONS_TEXT_ONLY="actions help version" + # No HTTP requests will be made + ACTIONS_TEXT_ONLY="actions lint help version" # Aliases of default actions # NOTICE: This is an experimental feature @@ -325,6 +327,12 @@ main() { exit 0 fi + # Lint the Docker compose/stack file + if [ $ACTION == "lint" ]; then + lint + exit 0 + fi + # Get list of all actions who can be used for this program if [ $ACTION == "actions" ]; then echo "Portainer Stack Utils, version $VERSION" @@ -350,6 +358,7 @@ main() { # PORTAINER_STACK_NAME # # PORTAINER_SERVICE_NAME # # DOCKER_COMPOSE_FILE # +# DOCKER_COMPOSE_LINT # # ENVIRONMENT_VARIABLES_FILE # # PORTAINER_ENDPOINT # # PORTAINER_PRUNE # @@ -378,6 +387,7 @@ set_globals() { PORTAINER_STACK_NAME=${PORTAINER_STACK_NAME} PORTAINER_SERVICE_NAME=${PORTAINER_SERVICE_NAME} DOCKER_COMPOSE_FILE=${DOCKER_COMPOSE_FILE} + DOCKER_COMPOSE_LINT=${DOCKER_COMPOSE_LINT:-"false"} ENVIRONMENT_VARIABLES_FILE=${ENVIRONMENT_VARIABLES_FILE} PORTAINER_ENDPOINT=${PORTAINER_ENDPOINT:-"1"} PORTAINER_PRUNE=${PORTAINER_PRUNE:-"false"} @@ -402,6 +412,7 @@ set_globals() { echo_debug "PORTAINER_STACK_NAME -> $PORTAINER_STACK_NAME" echo_debug "PORTAINER_SERVICE_NAME -> $PORTAINER_SERVICE_NAME" echo_debug "DOCKER_COMPOSE_FILE -> $DOCKER_COMPOSE_FILE" + echo_debug "DOCKER_COMPOSE_LINT -> $DOCKER_COMPOSE_LINT" echo_debug "ENVIRONMENT_VARIABLES_FILE -> $ENVIRONMENT_VARIABLES_FILE" echo_debug "PORTAINER_ENDPOINT -> $PORTAINER_ENDPOINT" echo_debug "PORTAINER_PRUNE -> $PORTAINER_PRUNE" @@ -423,8 +434,8 @@ set_globals() { fi check_argument "$PORTAINER_URL" "portainer url" "PORTAINER_URL" "l" "url" fi - if [ "$ACTION" == "deploy" ]; then - check_argument "$DOCKER_COMPOSE_FILE" "docker stack file" "DOCKER_COMPOSE_FILE" "f" "stack-file" + if [ "$ACTION" == "deploy" ] || [ "$ACTION" == "lint" ]; then + check_argument "$DOCKER_COMPOSE_FILE" "docker compose file" "DOCKER_COMPOSE_FILE" "c" "compose-file" 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 or the \"--env-file\" option." exit 1 @@ -488,6 +499,13 @@ inputs() { shift fi ;; + -L|--lint|-L=*|--lint=*) + DOCKER_COMPOSE_LINT=$(input_flag "$1" "$2") + if [ -n "$2" ] && [[ ! $2 =~ ^-.+$ ]] ; then + # When the second argument is the value of the current option + shift + fi + ;; -g=*|--env-file=*|-g|--env-file) ENVIRONMENT_VARIABLES_FILE=$(input_option "$1" "$2") if [ -n "$2" ] && [[ ! $2 =~ ^-.+$ ]] ; then @@ -910,6 +928,7 @@ mask_variables() { # Globals: # # STACK # # DOCKER_COMPOSE_FILE # +# DOCKER_COMPOSE_LINT # # PORTAINER_STACK_NAME # # PORTAINER_URL # # ENVIRONMENT_VARIABLES_FILE # @@ -922,6 +941,10 @@ mask_variables() { # None # ################################ deploy() { + # Lint docker-compose file if --lint option is "true" + if [ "$DOCKER_COMPOSE_LINT" == "true" ]; then + lint + fi # Read docker-compose file content local docker_compose_file_content docker_compose_file_content="$( jq -Rscjr '{StackFileContent: . }' $DOCKER_COMPOSE_FILE | tail -c +2 | head -c -1 )" @@ -1236,6 +1259,29 @@ containers() { echo "$containers" } +lint() { + local docker_stack_error + local docker_stack_validation + + if [ -x "$(command -v docker-compose)" ]; then + echo_verbose "Linting Docker compose/stack file..." + docker_stack_error="error_docker_stack_is_invalid" + docker_stack_validation=$(docker-compose -f "$DOCKER_COMPOSE_FILE" config -q > docker_stack_validation_report 2>&1 || echo $docker_stack_error) + if [[ $docker_stack_validation =~ $docker_stack_error$ ]]; then + echo_error "Error: The '$DOCKER_COMPOSE_FILE' Docker compose/stack file is invalid:" + echo_error "$(cat docker_stack_validation_report)" + rm -f docker_stack_validation_report + exit 1 + else + echo "[OK]" + echo_verbose "The '$DOCKER_COMPOSE_FILE' Docker compose/stack file is valid" + fi + else + echo "WARNING: Cannot validate '$DOCKER_COMPOSE_FILE' file, 'docker-compose' is not installed or not executable." + echo_verbose "For more informations, see: https://docs.docker.com/compose/install/" + fi || true +} + display_options_message() { echo "Options:" local table