diff --git a/docs/advanced-usage/registry-management.md b/docs/advanced-usage/registry-management.md index 51b041493..2900342a7 100644 --- a/docs/advanced-usage/registry-management.md +++ b/docs/advanced-usage/registry-management.md @@ -178,6 +178,10 @@ dokku registry:set node-js-app push-on-release dokku registry:set --global push-on-release ``` +#### Recovering from local image GC + +When `push-on-release` is enabled, Dokku treats the remote registry as the canonical store for app images. If a local image disappears - for example because a `docker image prune` cron ran, the host rebooted, or an operator removed it manually - subsequent commands that need the image (`ps:restart`, `ps:scale`, `dokku run`, `domains:add`, `certs:add`, etc.) will pull the missing tag back from the registry automatically. Per-app registry credentials configured via `registry:login` are honored during the pull. This recovery covers the deployed numeric tag pushed by the registry plugin - a missing `latest` tag will be ignored. + ### Push extra tags To push the image on release with extra tags, set the `push-extra-tags` to a comma-separated list of tags via the `registry:set` command. The default value for this property is empty. Setting the property will result in the image being tagged with extra tags every release. diff --git a/plugins/00_dokku-standard/subcommands/report b/plugins/00_dokku-standard/subcommands/report index d9ead2b29..daa5ee32f 100755 --- a/plugins/00_dokku-standard/subcommands/report +++ b/plugins/00_dokku-standard/subcommands/report @@ -28,7 +28,7 @@ cmd-report() { dokku_log_info1 "herokuish version: " if [[ "$ARCHITECTURE" == "arm64" ]]; then dokku_log_warn "herokuish not supported on $ARCHITECTURE architecture" - elif verify_image "$DOKKU_IMAGE"; then + elif fn-verify-app-image "" "$DOKKU_IMAGE"; then "$DOCKER_BIN" container run $DOKKU_GLOBAL_RUN_ARGS --rm "$DOKKU_IMAGE" herokuish version | sed "s/^/ /" else dokku_log_warn "Herokuish image $DOKKU_IMAGE is not available" diff --git a/plugins/common/functions b/plugins/common/functions index c933c4791..4986d6f45 100755 --- a/plugins/common/functions +++ b/plugins/common/functions @@ -279,8 +279,9 @@ verify_app_name() { } verify_image() { - declare desc="verify image existence" + declare desc="[deprecated] verify image existence. Use 'fn-verify-app-image' instead." local IMAGE="$1" + dokku_log_warn "Deprecated: please use 'fn-verify-app-image' instead of 'verify_image'" if "$DOCKER_BIN" image inspect "$IMAGE" &>/dev/null; then return 0 else @@ -288,6 +289,44 @@ verify_image() { fi } +fn-verify-app-image() { + declare desc="verify image existence, pulling from the registry when push-on-release is enabled" + declare APP="$1" IMAGE="$2" + + if "$DOCKER_BIN" image inspect "$IMAGE" &>/dev/null; then + return 0 + fi + + if [[ "$IMAGE" == *:latest ]]; then + return 1 + fi + + local DEPLOYED_TAG + DEPLOYED_TAG="$(plugn trigger deployed-app-image-tag "$APP")" + if [[ -z "$DEPLOYED_TAG" ]]; then + return 1 + fi + + local DOCKER_CONFIG + DOCKER_CONFIG="$(fn-registry-docker-config-dir "$APP")" + [[ -n "$DOCKER_CONFIG" ]] && export DOCKER_CONFIG + + local REMOTE_REPO + REMOTE_REPO="$(plugn trigger deployed-app-repository "$APP")" + + dokku_log_info1 "Image $IMAGE not found locally, pulling from registry" 1>&2 + if [[ "$IMAGE" == "${REMOTE_REPO}"* ]]; then + "$DOCKER_BIN" image pull "$IMAGE" 1>&2 && return 0 + return 1 + fi + + local REMOTE_IMAGE="${REMOTE_REPO}${IMAGE}" + if "$DOCKER_BIN" image pull "$REMOTE_IMAGE" 1>&2; then + "$DOCKER_BIN" image tag "$REMOTE_IMAGE" "$IMAGE" 1>&2 && return 0 + fi + return 1 +} + get_app_image_repo() { declare desc="central definition of image repo pattern" local APP="$1" @@ -309,7 +348,7 @@ get_deploying_app_image_name() { [[ -z "$IMAGE_REPO" ]] && IMAGE_REPO=$(get_app_image_repo "$APP") local IMAGE="${IMAGE_REMOTE_REPOSITORY}${IMAGE_REPO}:${IMAGE_TAG}" - verify_image "$IMAGE" || dokku_log_fail "App image ($IMAGE) not found" + fn-verify-app-image "$APP" "$IMAGE" || dokku_log_fail "App image ($IMAGE) not found" echo "$IMAGE" } @@ -325,7 +364,7 @@ get_app_image_name() { if [[ -n "$IMAGE_TAG" ]]; then local IMAGE="$IMAGE_REPO:$IMAGE_TAG" - verify_image "$IMAGE" || dokku_log_fail "App image ($IMAGE) not found" + fn-verify-app-image "$APP" "$IMAGE" || dokku_log_fail "App image ($IMAGE) not found" else local IMAGE="$IMAGE_REPO:latest" fi @@ -627,7 +666,7 @@ release_and_deploy() { local IMAGE=$(get_app_image_name "$APP" "$IMAGE_TAG") local exit_code=0 - if verify_image "$IMAGE"; then + if fn-verify-app-image "$APP" "$IMAGE"; then if ! declare -f -F fn-plugin-property-get-default >/dev/null; then source "$PLUGIN_CORE_AVAILABLE_PATH/common/property-functions" fi @@ -752,23 +791,6 @@ get_ipv6_regex() { echo "${RE_IPV6}\$" } -get_entrypoint_from_image() { - declare desc="return .Config.Entrypoint from passed image name" - local IMAGE="$1" - verify_image "$IMAGE" - local DOCKER_IMAGE_ENTRYPOINT="$("$DOCKER_BIN" image inspect --format '{{range .Config.Entrypoint}}{{.}} {{end}}' "$IMAGE")" - echo "ENTRYPOINT $DOCKER_IMAGE_ENTRYPOINT" -} - -get_cmd_from_image() { - declare desc="return .Config.Cmd from passed image name" - local IMAGE="$1" - verify_image "$IMAGE" - local DOCKER_IMAGE_CMD="$("$DOCKER_BIN" image inspect --format '{{range .Config.Cmd}}{{.}} {{end}}' "$IMAGE")" - DOCKER_IMAGE_CMD="${DOCKER_IMAGE_CMD/\/bin\/sh -c/}" - echo "CMD $DOCKER_IMAGE_CMD" -} - extract_directive_from_dockerfile() { declare desc="return requested directive from passed file path" local FILE_PATH="$1" diff --git a/plugins/config/config.go b/plugins/config/config.go index 5b898370d..cc4fc15a6 100644 --- a/plugins/config/config.go +++ b/plugins/config/config.go @@ -144,10 +144,12 @@ func triggerRestart(appName string) { return } + imageTag, _ := common.GetRunningImageTag(appName, "") + common.LogInfo1(fmt.Sprintf("Restarting app %s", appName)) _, err := common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "release-and-deploy", - Args: []string{appName, "", "config-redeploy"}, + Args: []string{appName, imageTag, "config-redeploy"}, StreamStdio: true, }) if err != nil { diff --git a/plugins/git/internal-functions b/plugins/git/internal-functions index 916c70ca5..11d4f367b 100755 --- a/plugins/git/internal-functions +++ b/plugins/git/internal-functions @@ -176,7 +176,7 @@ cmd-git-load-image() { dokku_setup_build_capture "$APP" "git:load-image" cat | docker load - if ! verify_image "$DOCKER_IMAGE"; then + if ! fn-verify-app-image "$APP" "$DOCKER_IMAGE"; then dokku_log_fail "Loaded image tarball but the specified docker image was not found: $DOCKER_IMAGE" fi diff --git a/plugins/nginx-vhosts/functions b/plugins/nginx-vhosts/functions index 2242b695b..730edba72 100755 --- a/plugins/nginx-vhosts/functions +++ b/plugins/nginx-vhosts/functions @@ -510,7 +510,7 @@ nginx_build_config() { if [[ "$(plugn trigger network-get-static-listeners "$APP" "web")" == "" ]]; then local IMAGE_TAG=$(get_running_image_tag "$APP") local IMAGE=$(get_deploying_app_image_name "$APP" "$IMAGE_TAG" 2>/dev/null) - if ! verify_image "$IMAGE" 2>/dev/null; then + if ! fn-verify-app-image "$APP" "$IMAGE" 2>/dev/null; then dokku_log_fail "Missing image for app" fi fi diff --git a/plugins/storage/src/subcommands/subcommands.go b/plugins/storage/src/subcommands/subcommands.go index 3a5bb8d6b..5040e6418 100644 --- a/plugins/storage/src/subcommands/subcommands.go +++ b/plugins/storage/src/subcommands/subcommands.go @@ -65,7 +65,7 @@ func main() { chown := args.String("chown", "herokuish", "--chown: chown option (herokuish, heroku, paketo, root, false)") args.Parse(os.Args[2:]) directory := args.Arg(0) - common.LogWarn("storage:ensure-directory is deprecated; use storage:create instead.") + common.LogWarn("Deprecated: please use 'storage:create' instead of 'storage:ensure-directory'") err = storage.CommandEnsureDirectory(directory, *chown) case "info": args := flag.NewFlagSet("storage:info", flag.ExitOnError) diff --git a/plugins/storage/triggers.go b/plugins/storage/triggers.go index 19081a389..d07ce3ac2 100644 --- a/plugins/storage/triggers.go +++ b/plugins/storage/triggers.go @@ -180,7 +180,7 @@ func detectDistro() string { // should use storage.ListAppMountEntries directly. A deprecation warning // is emitted on every invocation. func TriggerStorageList(appName string, phase string, format string) error { - common.LogWarn("the storage-list plugn trigger is deprecated; use the storage-app-mounts trigger or the storage Go package directly") + common.LogWarn("Deprecated: please use the 'storage-app-mounts' plugn trigger or the storage Go package directly instead of 'storage-list'") rows, err := ListAppMountEntries(appName, phase) if err != nil { diff --git a/tests/unit/registry_2.bats b/tests/unit/registry_2.bats new file mode 100644 index 000000000..927b955b9 --- /dev/null +++ b/tests/unit/registry_2.bats @@ -0,0 +1,112 @@ +#!/usr/bin/env bats + +load test_helper + +setup() { + global_setup + create_app + dokku checks:set $TEST_APP wait-to-retire 30 +} + +teardown() { + destroy_app + global_teardown +} + +setup_push_on_release_app() { + if [[ -z "$DOCKERHUB_USERNAME" ]] || [[ -z "$DOCKERHUB_TOKEN" ]]; then + skip "skipping due to missing docker.io credentials DOCKERHUB_USERNAME:DOCKERHUB_TOKEN" + fi + + run /bin/bash -c "dokku builder:set $TEST_APP selected herokuish" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku proxy:set $TEST_APP nginx" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku registry:login $TEST_APP docker.io $DOCKERHUB_USERNAME $DOCKERHUB_TOKEN" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku registry:set $TEST_APP image-repo $DOCKERHUB_USERNAME/$TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku registry:set $TEST_APP push-on-release true" + echo "output: $output" + echo "status: $status" + assert_success + + run deploy_app python dokku@$DOKKU_DOMAIN:$TEST_APP + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "docker image rm $DOCKERHUB_USERNAME/$TEST_APP:1" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "docker image inspect $DOCKERHUB_USERNAME/$TEST_APP:1" + echo "output: $output" + echo "status: $status" + assert_failure +} + +@test "(registry) [recover] ps:restart pulls missing image" { + setup_push_on_release_app + + run /bin/bash -c "dokku ps:restart $TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "not found locally, pulling from registry" + + run /bin/bash -c "docker image inspect $DOCKERHUB_USERNAME/$TEST_APP:1" + echo "output: $output" + echo "status: $status" + assert_success +} + +@test "(registry) [recover] dokku run pulls missing image" { + setup_push_on_release_app + + run /bin/bash -c "dokku run $TEST_APP echo recovery-ok" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "not found locally, pulling from registry" + assert_output_contains "recovery-ok" +} + +@test "(registry) [recover] domains:add pulls missing image" { + setup_push_on_release_app + + run /bin/bash -c "dokku domains:add $TEST_APP recovery.${DOKKU_DOMAIN}" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "docker image inspect $DOCKERHUB_USERNAME/$TEST_APP:1" + echo "output: $output" + echo "status: $status" + assert_success +} + +@test "(registry) [recover] config:set forwards deployed tag to release-and-deploy" { + setup_push_on_release_app + + run /bin/bash -c "dokku config:set $TEST_APP RECOVERY_KEY=value" + echo "output: $output" + echo "status: $status" + # triggerRestart forwards the deployed numeric tag (not "latest") to + # release-and-deploy, so the wrapper attempts recovery against ":1" rather + # than skipping recovery for ":latest" which is never pushed. + assert_output_contains "dokku/$TEST_APP:1 not found locally, pulling from registry" +}