diff --git a/docs/appendices/0.31.0-migration-guide.md b/docs/appendices/0.31.0-migration-guide.md new file mode 100644 index 00000000000..547cbc6cecb --- /dev/null +++ b/docs/appendices/0.31.0-migration-guide.md @@ -0,0 +1,5 @@ +# 0.30.0 Migration Guide + +## Changes + +- Herokuish build cache is now mounted from a docker volume - eg. `cache-node-js-app` - instead of the local filesystem. All existing app cache will be cleared upon upgrading past 0.29.0. diff --git a/docs/getting-started/upgrading/index.md b/docs/getting-started/upgrading/index.md index 0279ee147ef..9332e844359 100644 --- a/docs/getting-started/upgrading/index.md +++ b/docs/getting-started/upgrading/index.md @@ -18,6 +18,7 @@ Docker releases updates periodically to their engine. We recommend reading their Before upgrading, check the migration guides to get comfortable with new features and prepare your deployment to be upgraded. +- [Upgrading to 0.31](/docs/appendices/0.31.0-migration-guide.md) - [Upgrading to 0.30](/docs/appendices/0.30.0-migration-guide.md) - [Upgrading to 0.29](/docs/appendices/0.29.0-migration-guide.md) - [Upgrading to 0.28](/docs/appendices/0.28.0-migration-guide.md) diff --git a/plugins/builder-herokuish/builder-build b/plugins/builder-herokuish/builder-build index 401ab58d665..7505a992dfc 100755 --- a/plugins/builder-herokuish/builder-build +++ b/plugins/builder-herokuish/builder-build @@ -3,6 +3,16 @@ set -eo pipefail [[ $DOKKU_TRACE ]] && set -x source "$PLUGIN_AVAILABLE_PATH/config/functions" +fn-builder-herokuish-ensure-cache() { + declare APP="$1" + + existing_cache="$("$DOCKER_BIN" volume ls --quiet --filter label=com.dokku.app-name=test --filter label=com.dokku.builder-type=herokuish)" + if [[ "$existing_cache" != "cache-$APP" ]]; then + "$DOCKER_BIN" volume rm "cache-$APP" >/dev/null 2>&1 || true + "$DOCKER_BIN" volume create "--label=org.label-schema.schema-version=1.0" "--label=org.label-schema.vendor=dokku" "--label=com.dokku.app-name=$APP" "--label=com.dokku.builder-type=herokuish" "cache-$APP" >/dev/null + fi +} + trigger-builder-herokuish-builder-build() { declare desc="builder-herokuish builder-build plugin trigger" declare trigger="builder-build" @@ -21,8 +31,6 @@ trigger-builder-herokuish-builder-build() { fi local IMAGE=$(get_app_image_name "$APP") - local DOKKU_APP_CACHE_DIR="$DOKKU_ROOT/$APP/cache" - local DOKKU_APP_HOST_CACHE_DIR="$DOKKU_HOST_ROOT/$APP/cache" local DOCKER_COMMIT_LABEL_ARGS=("--change" "LABEL dokku=" "--change" "LABEL org.label-schema.schema-version=1.0" "--change" "LABEL org.label-schema.vendor=dokku" "--change" "LABEL com.dokku.image-stage=build" "--change" "LABEL com.dokku.builder-type=herokuish" "--change" "LABEL com.dokku.app-name=$APP") local DOCKER_RUN_LABEL_ARGS=("--label=dokku" "--label=org.label-schema.schema-version=1.0" "--label=org.label-schema.vendor=dokku" "--label=com.dokku.image-stage=build" "--label=com.dokku.builder-type=herokuish" "--label=com.dokku.app-name=$APP") local CID TAR_CID @@ -49,7 +57,6 @@ trigger-builder-herokuish-builder-build() { "$DOCKER_BIN" container commit "${DOCKER_COMMIT_LABEL_ARGS[@]}" "$TAR_CID" "$IMAGE" >/dev/null DOKKU_SKIP_IMAGE_CLEANUP_REGISTRATION=1 plugn trigger scheduler-register-retired "$APP" "$TAR_CID" - [[ -d $DOKKU_APP_CACHE_DIR ]] || mkdir -p "$DOKKU_APP_CACHE_DIR" plugn trigger pre-build-buildpack "$APP" "$SOURCECODE_WORK_DIR" local DOCKER_ARGS=$(: | plugn trigger docker-args-build "$APP" "$BUILDER_TYPE") @@ -60,8 +67,8 @@ trigger-builder-herokuish-builder-build() { eval "ARG_ARRAY=($DOCKER_ARGS)" local DOKKU_CONTAINER_EXIT_CODE=0 - if ! CID=$("$DOCKER_BIN" container create "${DOCKER_RUN_LABEL_ARGS[@]}" $DOKKU_GLOBAL_RUN_ARGS -v "$DOKKU_APP_HOST_CACHE_DIR:/cache" --env=CACHE_PATH=/cache "${ARG_ARRAY[@]}" "$IMAGE" /build); then - DOKKU_SKIP_IMAGE_CLEANUP_REGISTRATION=1 plugn trigger scheduler-register-retired "$APP" "$TAR_CID" + fn-builder-herokuish-ensure-cache "$APP" + if ! CID=$("$DOCKER_BIN" container create "${DOCKER_RUN_LABEL_ARGS[@]}" $DOKKU_GLOBAL_RUN_ARGS -v "cache-$APP:/cache" --env=CACHE_PATH=/cache "${ARG_ARRAY[@]}" "$IMAGE" /build); then plugn trigger scheduler-register-retired "$APP" "$CID" dokku_log_warn "Failure during app build" return 1 @@ -70,7 +77,6 @@ trigger-builder-herokuish-builder-build() { plugn trigger post-container-create "app" "$CID" "$APP" "build" "$DOCKER_BIN" container start "$CID" >/dev/null || DOKKU_CONTAINER_EXIT_CODE=$? if ! "$DOCKER_BIN" container attach "$CID"; then - DOKKU_SKIP_IMAGE_CLEANUP_REGISTRATION=1 plugn trigger scheduler-register-retired "$APP" "$TAR_CID" plugn trigger scheduler-register-retired "$APP" "$CID" dokku_log_warn "Failure during app build" return 1 @@ -78,14 +84,12 @@ trigger-builder-herokuish-builder-build() { DOKKU_CONTAINER_EXIT_CODE="$("$DOCKER_BIN" container wait "$CID" 2>/dev/null || echo "$DOKKU_CONTAINER_EXIT_CODE")" if [[ "$DOKKU_CONTAINER_EXIT_CODE" -ne 0 ]]; then - DOKKU_SKIP_IMAGE_CLEANUP_REGISTRATION=1 plugn trigger scheduler-register-retired "$APP" "$TAR_CID" plugn trigger scheduler-register-retired "$APP" "$CID" dokku_log_warn "Failure during app build" return 1 fi "$DOCKER_BIN" container commit "${DOCKER_COMMIT_LABEL_ARGS[@]}" "$CID" "$IMAGE" >/dev/null - DOKKU_SKIP_IMAGE_CLEANUP_REGISTRATION=1 plugn trigger scheduler-register-retired "$APP" "$TAR_CID" plugn trigger scheduler-register-retired "$APP" "$CID" plugn trigger post-build-buildpack "$APP" "$SOURCECODE_WORK_DIR" } diff --git a/plugins/builder-herokuish/install b/plugins/builder-herokuish/install index 00efe10cfed..357b692f761 100755 --- a/plugins/builder-herokuish/install +++ b/plugins/builder-herokuish/install @@ -1,13 +1,28 @@ #!/usr/bin/env bash +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" source "$PLUGIN_CORE_AVAILABLE_PATH/common/property-functions" set -eo pipefail [[ $DOKKU_TRACE ]] && set -x +fn-builder-herokuish-remove-old-cache() { + for app in $(dokku_apps "false" 2>/dev/null); do + local DOKKU_APP_CACHE_DIR="$DOKKU_ROOT/$APP/cache" + local DOKKU_APP_HOST_CACHE_DIR="$DOKKU_HOST_ROOT/$APP/cache" + if [[ ! -d "$DOKKU_APP_CACHE_DIR" ]]; then + continue + fi + + "$DOCKER_BIN" container run --rm --label=dokku --label=org.label-schema.schema-version=1.0 --label=org.label-schema.vendor=dokku "--label=com.dokku.app-name=$APP" -v "$DOKKU_APP_HOST_CACHE_DIR:/cache" "$DOKKU_IMAGE" "find /cache -depth -mindepth 1 -maxdepth 1 -exec rm -Rf {} ;" + rm -rf "$DOKKU_APP_CACHE_DIR" + done +} + trigger-builder-herokuish-install() { declare desc="installs the builder-herokuish plugin" declare trigger="install" fn-plugin-property-setup "builder-herokuish" + fn-builder-herokuish-remove-old-cache } trigger-builder-herokuish-install "$@" diff --git a/plugins/common/docker.go b/plugins/common/docker.go index 95d1b2f0dfe..70bae570324 100644 --- a/plugins/common/docker.go +++ b/plugins/common/docker.go @@ -203,7 +203,7 @@ func DockerCleanup(appName string, forceCleanup bool) error { containerIDs := append(exitedContainerIDs, deadContainerIDs...) if len(containerIDs) > 0 { - removeContainers(containerIDs) + DockerRemoveContainers(containerIDs) } // delete dangling images @@ -351,21 +351,18 @@ func VerifyImage(image string) bool { return imageCmd.Execute() } -func listContainers(status string, appName string) ([]string, error) { +// DockerFilterContainers returns a slice of container IDs based on the passed in filters +func DockerFilterContainers(filters []string) ([]string, error) { command := []string{ DockerBin(), "container", "list", "--quiet", "--all", - "--filter", - fmt.Sprintf("status=%v", status), - "--filter", - fmt.Sprintf("label=%v", os.Getenv("DOKKU_CONTAINER_LABEL")), } - if appName != "" { - command = append(command, []string{"--filter", fmt.Sprintf("label=com.dokku.app-name=%v", appName)}...) + for _, filter := range filters { + command = append(command, "--filter", filter) } var stderr bytes.Buffer @@ -382,6 +379,18 @@ func listContainers(status string, appName string) ([]string, error) { return output, nil } +func listContainers(status string, appName string) ([]string, error) { + filters := []string{ + fmt.Sprintf("status=%v", status), + fmt.Sprintf("label=%v", os.Getenv("DOKKU_CONTAINER_LABEL")), + } + + if appName != "" { + filters = append(filters, fmt.Sprintf("label=com.dokku.app-name=%v", appName)) + } + return DockerFilterContainers(filters) +} + func pruneUnusedImages(appName string) { command := []string{ DockerBin(), @@ -400,7 +409,8 @@ func pruneUnusedImages(appName string) { pruneCmd.Execute() } -func removeContainers(containerIDs []string) { +// DockerRemoveContainers will call `docker container rm` on the specified containers +func DockerRemoveContainers(containerIDs []string) { command := []string{ DockerBin(), "container", diff --git a/plugins/common/subprocess.go b/plugins/common/subprocess.go index 642b06c801e..19b02e81e4d 100644 --- a/plugins/common/subprocess.go +++ b/plugins/common/subprocess.go @@ -18,6 +18,7 @@ type ShellCmd struct { CommandString string Args []string ShowOutput bool + ExitError *exec.ExitError } // NewShellCmd returns a new ShellCmd struct @@ -57,6 +58,10 @@ func (sc *ShellCmd) Execute() bool { sc.setup() if err := sc.Command.Run(); err != nil { + exitError, ok := err.(*exec.ExitError) + if ok { + sc.ExitError = exitError + } return false } return true diff --git a/plugins/repo/.gitignore b/plugins/repo/.gitignore index 4e8c51e8a6e..e1f4bd87f6f 100644 --- a/plugins/repo/.gitignore +++ b/plugins/repo/.gitignore @@ -2,5 +2,4 @@ /subcommands/* /triggers/* /triggers -/pre-delete -/post-stack-set +/post-* diff --git a/plugins/repo/Makefile b/plugins/repo/Makefile index fd3d745474c..0af9469831d 100644 --- a/plugins/repo/Makefile +++ b/plugins/repo/Makefile @@ -1,5 +1,5 @@ SUBCOMMANDS = subcommands/gc subcommands/purge-cache -TRIGGERS = triggers/post-stack-set triggers/pre-delete +TRIGGERS = triggers/post-stack-set triggers/post-delete BUILD = commands subcommands triggers PLUGIN_NAME = repo diff --git a/plugins/repo/repo.go b/plugins/repo/repo.go index 04e7cdea166..1f459fe1e10 100644 --- a/plugins/repo/repo.go +++ b/plugins/repo/repo.go @@ -6,28 +6,45 @@ import ( "strings" "github.com/dokku/dokku/plugins/common" - "github.com/dokku/dokku/plugins/config" ) +// PurgeCacheFailed wraps error to allow returning the correct exit code +type PurgeCacheFailed struct { + exitCode int +} + +// ExitCode returns an exit code to use in case this error bubbles +// up into an os.Exit() call +func (err *PurgeCacheFailed) ExitCode() int { + return err.exitCode +} + +// Error returns a standard non-existent app error +func (err *PurgeCacheFailed) Error() string { + return fmt.Sprintf("failed to purge cache, exit code %d", err.exitCode) +} + // PurgeCache deletes the contents of the build cache stored in the repository func PurgeCache(appName string) error { - cacheDir := strings.Join([]string{common.AppRoot(appName), "cache"}, "/") - cacheHostDir := strings.Join([]string{common.AppHostRoot(appName), "cache"}, "/") - dokkuGlobalRunArgs := common.MustGetEnv("DOKKU_GLOBAL_RUN_ARGS") - image := config.GetWithDefault(appName, "DOKKU_IMAGE", os.Getenv("DOKKU_IMAGE")) - if info, _ := os.Stat(cacheDir); info != nil && info.IsDir() { - dockerLabelArgs := fmt.Sprintf("--label=com.dokku.app-name=%s", appName) - purgeCacheCmd := common.NewShellCmd(strings.Join([]string{ - common.DockerBin(), - "container", - "run", "--rm", dockerLabelArgs, dokkuGlobalRunArgs, - "-v", strings.Join([]string{cacheHostDir, ":/cache"}, ""), image, - `find /cache -depth -mindepth 1 -maxdepth 1 -exec rm -Rf {} ;`}, " ")) - purgeCacheCmd.Execute() - err := os.MkdirAll(cacheDir, 0644) - if err != nil { - return err + containerIDs, _ := common.DockerFilterContainers([]string{ + fmt.Sprintf("label=com.dokku.app-name=%v", appName), + "label=com.dokku.image-stage=build", + }) + if len(containerIDs) > 0 { + common.DockerRemoveContainers(containerIDs) + } + purgeCacheCmd := common.NewShellCmd(strings.Join([]string{ + common.DockerBin(), + "volume", + "rm", "-f", fmt.Sprintf("cache-%s", appName)}, " ")) + purgeCacheCmd.ShowOutput = false + purgeCacheCmd.Command.Stderr = os.Stderr + if !purgeCacheCmd.Execute() { + exitCode := 1 + if purgeCacheCmd.ExitError != nil { + exitCode = purgeCacheCmd.ExitError.ExitCode() } + return &PurgeCacheFailed{exitCode} } return nil diff --git a/plugins/repo/src/triggers/triggers.go b/plugins/repo/src/triggers/triggers.go index 0dd1c21d0b4..63c4b468b7a 100644 --- a/plugins/repo/src/triggers/triggers.go +++ b/plugins/repo/src/triggers/triggers.go @@ -21,7 +21,7 @@ func main() { case "post-stack-set": appName := flag.Arg(0) err = repo.PurgeCache(appName) - case "pre-delete": + case "post-delete": appName := flag.Arg(0) err = repo.PurgeCache(appName) default: diff --git a/tests/unit/build-env.bats b/tests/unit/build-env.bats index 2a0df8f61c0..f53fe5372a7 100644 --- a/tests/unit/build-env.bats +++ b/tests/unit/build-env.bats @@ -59,12 +59,16 @@ teardown() { } @test "(build-env) buildpack deploy with Dockerfile" { - run /bin/bash -c "dokku config:set --no-restart $TEST_APP BUILDPACK_URL='https://github.com/heroku/heroku-buildpack-nodejs'" + run /bin/bash -c "dokku config:set --no-restart $TEST_APP BUILDPACK_URL='https://github.com/dokku/heroku-buildpack-null'" + echo "output: $output" + echo "status: $status" + assert_success + + run deploy_app python dokku@dokku.me:$TEST_APP move_dockerfile_into_place echo "output: $output" echo "status: $status" assert_success - deploy_app dockerfile run /bin/bash -c "dokku --quiet config:get $TEST_APP DOKKU_APP_TYPE" echo "output: $output" echo "status: $status" diff --git a/tests/unit/repo.bats b/tests/unit/repo.bats index abdc68213fe..6a74c315da8 100644 --- a/tests/unit/repo.bats +++ b/tests/unit/repo.bats @@ -4,9 +4,11 @@ load test_helper setup() { global_setup + create_app } teardown() { + destroy_app global_teardown } @@ -24,7 +26,7 @@ teardown() { assert_output "$help_output" } -@test "(repo) repo:gc, repo:purge-cache" { +@test "(repo) repo:gc" { run deploy_app echo "output: $output" echo "status: $status" @@ -34,29 +36,36 @@ teardown() { echo "output: $output" echo "status: $status" assert_success +} - run /bin/bash -c "touch $DOKKU_ROOT/$TEST_APP/cache/derp" +@test "(repo) repo:purge-cache" { + run deploy_app echo "output: $output" echo "status: $status" assert_success + run /bin/bash -c "docker volume ls -q --filter label=com.dokku.app-name=$TEST_APP | wc -l" + echo "count: '$output'" + echo "status: $status" + assert_output 1 + run /bin/bash -c "find $DOKKU_ROOT/$TEST_APP/cache -type f | wc -l" echo "count: '$output'" echo "status: $status" - assert_not_output 0 + assert_output 0 run /bin/bash -c "dokku repo:purge-cache $TEST_APP" echo "output: $output" echo "status: $status" assert_success - run /bin/bash -c "find $DOKKU_ROOT/$TEST_APP/cache -type f | wc -l" + run /bin/bash -c "docker volume ls -q --filter label=com.dokku.app-name=$TEST_APP | wc -l" echo "count: '$output'" echo "status: $status" assert_output 0 - run destroy_app - echo "output: $output" + run /bin/bash -c "find $DOKKU_ROOT/$TEST_APP/cache -type f | wc -l" + echo "count: '$output'" echo "status: $status" - assert_success + assert_output 0 } diff --git a/tests/unit/test_helper.bash b/tests/unit/test_helper.bash index 7ca8aad7fc7..7d39ec012d0 100644 --- a/tests/unit/test_helper.bash +++ b/tests/unit/test_helper.bash @@ -535,6 +535,13 @@ add_postdeploy_command() { echo "${contents}" >"$APP_REPO_DIR/app.json" } +move_dockerfile_into_place() { + local APP="$1" + local APP_REPO_DIR="$2" + [[ -z "$APP" ]] && local APP="$TEST_APP" + mv "$APP_REPO_DIR/alt.Dockerfile" "$APP_REPO_DIR/Dockerfile" +} + add_requirements_txt() { local APP="$1" local APP_REPO_DIR="$2"