diff --git a/docs/deployment/deployment-tasks.md b/docs/deployment/deployment-tasks.md new file mode 100644 index 00000000000..c5ff3fe6970 --- /dev/null +++ b/docs/deployment/deployment-tasks.md @@ -0,0 +1,55 @@ +# Deployment Tasks + +Sometimes you need to run a command on at deployment time, but before an app is completely deployed. +Common use cases include: + +* Checking a database is initialised +* Running database migrations +* Any commands required to set up the server (e.g. something like a Django `collectstatic`) + +## `app.json` and `scripts.dokku` + +Dokku accomplishes this by using an `app.json` file. We (mostly) use the same format as Heroku's [app.json](https://devcenter.heroku.com/articles/app-json-schema). +However, dokku currently only supports the nodes `scripts.dokku.predeploy` and `scripts.dokku.postdeploy`. +Simply place an `app.json` file in the root of your repository. +NOTE: `app.json` is only supported in buildpack-deployed apps and postdeploy changes are not committed to the app image. + +### Example app.json + +``` +{ + "name": "barebones nodejs", + "description": "A barebones Node.js app using Express 4.", + "keywords": [ + "nodejs", + "express" + ], + "repository": "https://github.com/michaelshobbs/node-js-sample", + "scripts": { + "dokku": { + "predeploy": "touch /app/predeploy.test", + "postdeploy": "touch /app/postdeploy.test" + } + }, + "env": { + "SECRET_TOKEN": { + "description": "A secret key for verifying the integrity of signed cookies.", + "value": "secret" + }, + "WEB_CONCURRENCY": { + "description": "The number of processes to run.", + "generator": "echo 5" + } + }, + "image": "gliderlabs/herokuish", + "addons": [ + "dokku-postgres", + "dokku-redis" + ], + "buildpacks": [ + { + "url": "https://github.com/heroku/heroku-buildpack-nodejs" + } + ] +} +``` diff --git a/docs/development/plugin-triggers.md b/docs/development/plugin-triggers.md index e3a8d6ed9d4..643d9a72a3d 100644 --- a/docs/development/plugin-triggers.md +++ b/docs/development/plugin-triggers.md @@ -405,7 +405,7 @@ dokku postgres:link $POSTGRES $APP - Description: Allows running of commands after a deploy has completed. Dokku core currently uses this to switch traffic on nginx. - Invoked by: `dokku deploy` -- Arguments: `$APP $INTERNAL_PORT $INTERNAL_IP_ADDRESS` +- Arguments: `$APP $INTERNAL_PORT $INTERNAL_IP_ADDRESS $IMAGE_TAG` - Example: ```shell diff --git a/dokku b/dokku index 19720a56d4a..d056c65580f 100755 --- a/dokku +++ b/dokku @@ -207,7 +207,7 @@ case "$1" in done < "$DOKKU_SCALE_FILE" dokku_log_info1 "Running post-deploy" - plugn trigger post-deploy $APP $port $ipaddr + plugn trigger post-deploy $APP $port $ipaddr $IMAGE_TAG # kill the old container if [[ -n "$oldids" ]]; then diff --git a/plugins/00_dokku-standard/exec-app-json-scripts b/plugins/00_dokku-standard/exec-app-json-scripts new file mode 100755 index 00000000000..89985426f33 --- /dev/null +++ b/plugins/00_dokku-standard/exec-app-json-scripts @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" +APP="$1" + +case "$0" in + *pre-deploy) + IMAGE_TAG="$2" + IMAGE=$(get_app_image_name $APP $IMAGE_TAG) + PHASE_SCRIPT_KEY="predeploy" + ;; + *post-deploy) + IMAGE_TAG="$4" + IMAGE=$(get_app_image_name $APP $IMAGE_TAG) + PHASE_SCRIPT_KEY="postdeploy" + ;; +esac + +get_phase_script() { + local PHASE_SCRIPT_KEY="$1" + local TMP_WORK_DIR=$(mktemp -d) + local APP_JSON_FILE="$TMP_WORK_DIR/app.json" + trap 'rm -rf "$TMP_WORK_DIR" > /dev/null' RETURN + + copy_from_image "$IMAGE" "/app/app.json" "$TMP_WORK_DIR" 2>/dev/null || true + + if [[ -f "$APP_JSON_FILE" ]];then + local VALUE=$(get_json_value scripts.dokku.${PHASE_SCRIPT_KEY} < "$APP_JSON_FILE") + else + return 0 + fi + + echo "$VALUE" +} + +execute_script() { + local SCRIPT_CMD=$(get_phase_script "$PHASE_SCRIPT_KEY") + if [[ -n "$SCRIPT_CMD" ]];then + dokku_log_info1 "Running '$SCRIPT_CMD' in app container" + local COMMAND="cd /app ; " + local COMMAND+=" for file in /app/.profile.d/*; do source \$file; done ; " + local COMMAND+=" echo restoring installation cache... ; " + local COMMAND+=" cp -rfp /cache /tmp/ || true ; " + local COMMAND+=" $SCRIPT_CMD ; " + local COMMAND+=" echo removing installation cache... ; " + local COMMAND+=" rm -rf /tmp/cache || true " + local DOCKER_ARGS=$(: | plugn trigger docker-args-deploy $APP) + local CACHE_DIR="$DOKKU_ROOT/$APP/cache" + id=$(docker run $DOKKU_GLOBAL_RUN_ARGS --label=dokku_phase_script=${PHASE_SCRIPT_KEY} -d -v $CACHE_DIR:/cache $DOCKER_ARGS $IMAGE /bin/bash -c "$COMMAND") + test "$(docker wait $id)" -eq 0 + dokku_container_log_verbose_quiet $id + if [[ "$PHASE_SCRIPT_KEY" != "postdeploy" ]];then + docker commit "$id" "$IMAGE" > /dev/null + fi + fi +} + +dokku_log_info1 "Attempting to run scripts.dokku.$PHASE_SCRIPT_KEY from app.json (if defined)" +execute_script diff --git a/plugins/00_dokku-standard/post-deploy b/plugins/00_dokku-standard/post-deploy new file mode 120000 index 00000000000..b2423179067 --- /dev/null +++ b/plugins/00_dokku-standard/post-deploy @@ -0,0 +1 @@ +exec-app-json-scripts \ No newline at end of file diff --git a/plugins/00_dokku-standard/pre-deploy b/plugins/00_dokku-standard/pre-deploy new file mode 120000 index 00000000000..b2423179067 --- /dev/null +++ b/plugins/00_dokku-standard/pre-deploy @@ -0,0 +1 @@ +exec-app-json-scripts \ No newline at end of file diff --git a/plugins/common/functions b/plugins/common/functions index d60fe425dd6..a750241ad27 100755 --- a/plugins/common/functions +++ b/plugins/common/functions @@ -599,3 +599,26 @@ get_app_urls() { fi fi } + +get_json_value() { + # return value of provided json key from a json stream on stdin + # JSON_NODE should be expresses as either a top-level object that has no children + # or in the format of json.node.path + local JSON_NODE="$1" + local JSON_NODE=${JSON_NODE//\./\"][\"} + local JSON_NODE="[\"${JSON_NODE}\"]" + cat | python -c 'import json,sys;obj=json.load(sys.stdin);print json.dumps(obj'"${JSON_NODE}"').strip("\"")'; +} + +get_json_keys() { + # return space-separated list of json keys from json provided on stdin + # JSON_NODE should be expressed as json.node.path and is expected to have children + local JSON_NODE="$1" + local JSON_NODE=${JSON_NODE//\./\"][\"} + local JSON_NODE="[\"${JSON_NODE}\"]" + if [[ "$JSON_NODE" == "[\"\"]" ]]; then + cat | python -c 'import json,sys;obj=json.load(sys.stdin);print " ".join(obj.keys())'; + else + cat | python -c 'import json,sys;obj=json.load(sys.stdin);print " ".join(obj'"${JSON_NODE}"'.keys())'; + fi +} diff --git a/tests/apps/nodejs-express/app.json b/tests/apps/nodejs-express/app.json new file mode 100644 index 00000000000..9b9aa95ce4e --- /dev/null +++ b/tests/apps/nodejs-express/app.json @@ -0,0 +1,38 @@ +{ + "name": "Sample node.js express app", + "description": "Used in dokku's test suite", + "keywords": [ + "nodejs", + "express", + "testing" + ], + "website": "http://dokku.viewdocs.io/dokku/", + "repository": "https://github.com/dokku/dokku", + "logo": "https://raw.githubusercontent.com/dokku/dokku/master/docs/assets/dokku.png", + "scripts": { + "dokku": { + "predeploy": "touch /app/predeploy.test", + "postdeploy": "touch /app/postdeploy.test" + } + }, + "env": { + "SECRET_TOKEN": { + "description": "A secret key for verifying the integrity of signed cookies.", + "value": "secret" + }, + "WEB_CONCURRENCY": { + "description": "The number of processes to run.", + "generator": "echo 5" + } + }, + "image": "gliderlabs/herokuish", + "addons": [ + "dokku-postgres", + "dokku-redis" + ], + "buildpacks": [ + { + "url": "https://github.com/heroku/heroku-buildpack-nodejs" + } + ] +} diff --git a/tests/unit/10_core_1.bats b/tests/unit/10_core_1.bats index 2f7ae644c8a..5b2272b7a2a 100644 --- a/tests/unit/10_core_1.bats +++ b/tests/unit/10_core_1.bats @@ -146,3 +146,24 @@ EOF echo "status: "$status assert_output "3001/udp 3000/tcp 3003" } + +@test "(core) app.json scripts" { + deploy_app + + run /bin/bash -c "dokku run $TEST_APP ls /app/prebuild.test" + echo "output: "$output + echo "status: "$status + assert_failure + + run /bin/bash -c "dokku run $TEST_APP ls /app/predeploy.test" + echo "output: "$output + echo "status: "$status + assert_success + + CID=$(docker ps -a -q -f "ancestor=dokku/${TEST_APP}" -f "label=dokku_phase_script=postdeploy") + IMAGE_ID=$(docker commit $CID dokku-test/${TEST_APP}) + run /bin/bash -c "docker run -ti $IMAGE_ID ls /app/postdeploy.test" + echo "output: "$output + echo "status: "$status + assert_success +}