diff --git a/docs/advanced-usage/registry-management.md b/docs/advanced-usage/registry-management.md index 8e2eef8f6..8f33ee319 100644 --- a/docs/advanced-usage/registry-management.md +++ b/docs/advanced-usage/registry-management.md @@ -4,9 +4,10 @@ > New as of 0.25.0 ``` -registry:login [--password-stdin] [] # Login to a docker registry -registry:report [] [] # Displays a registry report for one or more apps -registry:set () # Set or clear a registry property for an app +registry:login [--global|--password-stdin] [] [] # Login to a docker registry +registry:logout [--global] [] # Logout from a docker registry +registry:report [] [] # Displays a registry report for one or more apps +registry:set |--global () # Set or clear a registry property for an app ``` The registry plugin enables interacting with remote registries, which is useful when either deploying images via `git:from-image` or when interacting with custom schedulers to deploy built image artifacts. @@ -15,34 +16,75 @@ The registry plugin enables interacting with remote registries, which is useful ### Logging into a registry -The `registry:login` command can be used to log into a docker registry. The following are examples for logging into various common registries: +The `registry:login` command can be used to log into a docker registry. Credentials can be stored globally (for all apps) or on a per-app basis. + +#### Global login + +To log in globally (credentials shared by all apps), use the `--global` flag: ```shell # hub.docker.com -dokku registry:login docker.io $USERNAME $PASSWORD +dokku registry:login --global docker.io $USERNAME $PASSWORD # digitalocean # the username and password are both defined as the same api token -dokku registry:login registry.digitalocean.com $DIGITALOCEAN_API_TOKEN $DIGITALOCEAN_API_TOKEN +dokku registry:login --global registry.digitalocean.com $DIGITALOCEAN_API_TOKEN $DIGITALOCEAN_API_TOKEN # github container registry # see the following link for information on retrieving a personal access token # https://docs.github.com/en/packages/guides/pushing-and-pulling-docker-images#authenticating-to-github-container-registry -dokku registry:login ghcr.io $USERNAME $REGISTRY_PAT_TOKEN +dokku registry:login --global ghcr.io $USERNAME $REGISTRY_PAT_TOKEN # quay # a robot user may be used to login -dokku registry:login quay.io $USERNAME $PASSWORD +dokku registry:login --global quay.io $USERNAME $PASSWORD ``` -For security reasons, the password may also be specified as stdin by specifying the `--password-stdin` flag. This is supported regardless of the registry being logged into. +> [!NOTE] +> For backwards compatibility, if the `--global` flag is omitted and only three arguments are provided (server, username, password), the command will behave as a global login but will show a deprecation warning. + +#### Per-app login + +To log in for a specific app, specify the app name as the first argument: ```shell -echo "$PASSWORD" | dokku registry:login --password-stdin docker.io $USERNAME +# log into docker.io for a specific app +dokku registry:login node-js-app docker.io $USERNAME $PASSWORD + +# log into ghcr.io for a specific app +dokku registry:login node-js-app ghcr.io $USERNAME $REGISTRY_PAT_TOKEN +``` + +Per-app credentials are stored in `/var/lib/dokku/config/registry/$APP/config.json` and are automatically used for docker operations (build, push, pull) for that specific app. + +#### Password via stdin + +For security reasons, the password may also be specified as stdin by specifying the `--password-stdin` flag. This is supported for both global and per-app logins: + +```shell +# global login via stdin +echo "$PASSWORD" | dokku registry:login --global --password-stdin docker.io $USERNAME + +# per-app login via stdin +echo "$PASSWORD" | dokku registry:login node-js-app --password-stdin docker.io $USERNAME ``` For certain Docker registries - such as Amazon ECR or Google's GCR registries - users may instead wish to use a docker credential helper to automatically authenticate against a server; please see the documentation regarding the credential helper in question for further setup instructions. +### Logging out from a registry + +The `registry:logout` command can be used to log out from a docker registry: + +```shell +# global logout +dokku registry:logout --global docker.io + +# per-app logout +dokku registry:logout node-js-app docker.io +``` + +When an app is destroyed, any per-app registry credentials are automatically removed. + ### Setting a remote server To specify a remote server registry for pushes, set the `server` property via the `registry:set` command. The default value for this property is empty string. Setting the value to `docker.io` or `hub.docker.com` will result in the computed value being empty string (as that is the default, implicit registry), while any non-zero length value will have a `/` appended to it if there is not one already. diff --git a/plugins/builder-dockerfile/builder-build b/plugins/builder-dockerfile/builder-build index 1fc87e7ab..d15ef8fbe 100755 --- a/plugins/builder-dockerfile/builder-build +++ b/plugins/builder-dockerfile/builder-build @@ -246,6 +246,9 @@ trigger-builder-dockerfile-builder-build() { done eval "$(config_export app "$APP")" + local DOCKER_CONFIG + DOCKER_CONFIG="$(fn-registry-docker-config "$APP")" + [[ -n "$DOCKER_CONFIG" ]] && export DOCKER_CONFIG "$DOCKER_BIN" image build "${DOCKER_BUILD_LABEL_ARGS[@]}" "${DOCKERFILE_ARGS[@]}" --tag $IMAGE . plugn trigger ports-set-detected "$APP" "$(fn-builder-dockerfile-get-detect-port-map "$APP" "$IMAGE" "$SOURCECODE_WORK_DIR/Dockerfile")" diff --git a/plugins/builder-dockerfile/builder-release b/plugins/builder-dockerfile/builder-release index b7023d27e..a9a331bd6 100755 --- a/plugins/builder-dockerfile/builder-release +++ b/plugins/builder-dockerfile/builder-release @@ -29,6 +29,9 @@ trigger-builder-dockerfile-builder-release() { DOCKER_BUILD_ARGS+=("--platform=linux/amd64") fi + local DOCKER_CONFIG + DOCKER_CONFIG="$(fn-registry-docker-config "$APP")" + [[ -n "$DOCKER_CONFIG" ]] && export DOCKER_CONFIG if ! suppress_output "$DOCKER_BIN" image build "${DOCKER_BUILD_ARGS[@]}" $DOKKU_GLOBAL_BUILD_ARGS -f "$PLUGIN_AVAILABLE_PATH/builder-dockerfile/dockerfiles/builder-release.Dockerfile" --build-arg APP_IMAGE="$IMAGE" -t "$IMAGE" "$TMP_WORK_DIR"; then dokku_log_warn "Failure injecting docker labels on image" return 1 diff --git a/plugins/builder-herokuish/builder-build b/plugins/builder-herokuish/builder-build index f8a396512..d24960dc8 100755 --- a/plugins/builder-herokuish/builder-build +++ b/plugins/builder-herokuish/builder-build @@ -1,6 +1,7 @@ #!/usr/bin/env bash set -eo pipefail [[ $DOKKU_TRACE ]] && set -x +source "$PLUGIN_CORE_AVAILABLE_PATH/common/functions" source "$PLUGIN_AVAILABLE_PATH/config/functions" fn-builder-herokuish-ensure-cache() { @@ -46,6 +47,9 @@ trigger-builder-herokuish-builder-build() { [[ -n "$NEW_DOKKU_IMAGE" ]] && DOKKU_IMAGE="$NEW_DOKKU_IMAGE" local DOCKER_BUILD_LABEL_ARGS=("--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" "--label=dokku") + local DOCKER_CONFIG + DOCKER_CONFIG="$(fn-registry-docker-config "$APP")" + [[ -n "$DOCKER_CONFIG" ]] && export DOCKER_CONFIG DOKKU_APP_USER=$(config_get "$APP" DOKKU_APP_USER || true) DOKKU_APP_USER=${DOKKU_APP_USER:="herokuishuser"} if ! suppress_output "$DOCKER_BIN" image build "${DOCKER_BUILD_LABEL_ARGS[@]}" $DOKKU_GLOBAL_BUILD_ARGS -f "$PLUGIN_AVAILABLE_PATH/builder-herokuish/dockerfiles/copy-source.Dockerfile" --build-arg APP_IMAGE="$DOKKU_IMAGE" --build-arg "DOKKU_APP_USER=$DOKKU_APP_USER" --build-arg "TRACE=$DOKKU_TRACE" -t $IMAGE "$SOURCECODE_WORK_DIR"; then diff --git a/plugins/builder-herokuish/builder-release b/plugins/builder-herokuish/builder-release index f0a72f890..73df6ce7b 100755 --- a/plugins/builder-herokuish/builder-release +++ b/plugins/builder-herokuish/builder-release @@ -30,6 +30,10 @@ trigger-builder-herokuish-builder-release() { DOKKU_APP_USER=$(config_get "$APP" DOKKU_APP_USER || true) DOKKU_APP_USER=${DOKKU_APP_USER:="herokuishuser"} + local DOCKER_CONFIG + DOCKER_CONFIG="$(fn-registry-docker-config "$APP")" + [[ -n "$DOCKER_CONFIG" ]] && export DOCKER_CONFIG + if ! suppress_output "$DOCKER_BIN" image build "${DOCKER_BUILD_ARGS[@]}" $DOKKU_GLOBAL_BUILD_ARGS -f "$PLUGIN_AVAILABLE_PATH/builder-herokuish/dockerfiles/builder-pre-release.Dockerfile" --build-arg APP_IMAGE="$IMAGE" --build-arg "DOKKU_APP_USER=$DOKKU_APP_USER" -t "$IMAGE" "$TMP_WORK_DIR"; then dokku_log_warn "Failure injecting environment variables" return 1 diff --git a/plugins/builder-herokuish/pre-build b/plugins/builder-herokuish/pre-build index 35bd1bb9e..dc1453e96 100755 --- a/plugins/builder-herokuish/pre-build +++ b/plugins/builder-herokuish/pre-build @@ -41,6 +41,9 @@ trigger-builder-herokuish-pre-build-buildpack() { DOKKU_APP_USER=$(config_get "$APP" DOKKU_APP_USER || true) DOKKU_APP_USER=${DOKKU_APP_USER:="herokuishuser"} + local DOCKER_CONFIG + DOCKER_CONFIG="$(fn-registry-docker-config "$APP")" + [[ -n "$DOCKER_CONFIG" ]] && export DOCKER_CONFIG if ! suppress_output "$DOCKER_BIN" image build "${DOCKER_BUILD_LABEL_ARGS[@]}" $DOKKU_GLOBAL_BUILD_ARGS -f "$PLUGIN_AVAILABLE_PATH/builder-herokuish/dockerfiles/pre-build.Dockerfile" --build-arg APP_IMAGE="$IMAGE" --build-arg "DOKKU_APP_USER=$DOKKU_APP_USER" -t $IMAGE "$TMP_WORK_DIR"; then dokku_log_warn "Failure injecting BUILD_ENV into build environment" return 1 diff --git a/plugins/builder-lambda/builder-build b/plugins/builder-lambda/builder-build index c843135a1..d085a374e 100755 --- a/plugins/builder-lambda/builder-build +++ b/plugins/builder-lambda/builder-build @@ -24,6 +24,9 @@ trigger-builder-lambda-builder-build() { fi plugn trigger pre-build "$BUILDER_TYPE" "$APP" "$SOURCECODE_WORK_DIR" + local DOCKER_CONFIG + DOCKER_CONFIG="$(fn-registry-docker-config "$APP")" + [[ -n "$DOCKER_CONFIG" ]] && export DOCKER_CONFIG lambda-builder build --generate-image --write-procfile --image-env=DOCKER_LAMBDA_STAY_OPEN=1 --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=lambda "--label=com.dokku.app-name=$APP" $DOKKU_GLOBAL_BUILD_ARGS --port 5000 --tag "$IMAGE" --working-directory "$SOURCECODE_WORK_DIR" if [[ ! -f "$SOURCECODE_WORK_DIR/lambda.zip" ]]; then dokku_log_warn "Compressed lambda.zip not detected, failed to build lambda function" diff --git a/plugins/builder-lambda/builder-release b/plugins/builder-lambda/builder-release index fcfe701bb..ad3ebf32b 100755 --- a/plugins/builder-lambda/builder-release +++ b/plugins/builder-lambda/builder-release @@ -23,6 +23,9 @@ trigger-builder-lambda-builder-release() { trap "rm -rf '$TMP_WORK_DIR' >/dev/null" RETURN INT TERM EXIT local DOCKER_BUILD_LABEL_ARGS=("--label=org.label-schema.schema-version=1.0" "--label=org.label-schema.vendor=dokku" "--label=com.dokku.image-stage=release" "--label=com.dokku.builder-type=lambda" "--label=com.dokku.app-name=$APP" "--label=dokku") + local DOCKER_CONFIG + DOCKER_CONFIG="$(fn-registry-docker-config "$APP")" + [[ -n "$DOCKER_CONFIG" ]] && export DOCKER_CONFIG if ! suppress_output "$DOCKER_BIN" image build "${DOCKER_BUILD_LABEL_ARGS[@]}" $DOKKU_GLOBAL_BUILD_ARGS -f "$PLUGIN_AVAILABLE_PATH/builder-lambda/dockerfiles/builder-release.Dockerfile" --build-arg APP_IMAGE="$IMAGE" -t "$IMAGE" "$TMP_WORK_DIR"; then dokku_log_warn "Failure injecting docker labels on image" return 1 diff --git a/plugins/builder-nixpacks/builder-build b/plugins/builder-nixpacks/builder-build index 8509c2a29..dd2b41871 100755 --- a/plugins/builder-nixpacks/builder-build +++ b/plugins/builder-nixpacks/builder-build @@ -243,7 +243,10 @@ trigger-builder-nixpacks-builder-build() { eval "$(config_export app "$APP" --merged)" - # shellcheck disable=SC2086 + local DOCKER_CONFIG + DOCKER_CONFIG="$(fn-registry-docker-config "$APP")" + [[ -n "$DOCKER_CONFIG" ]] && export DOCKER_CONFIG + if ! nixpacks build "${DOCKER_BUILD_LABEL_ARGS[@]}" "${NIXPACKS_ARGS[@]}" --name "$IMAGE" "$SOURCECODE_WORK_DIR"; then dokku_log_warn "Failure building image" return 1 diff --git a/plugins/builder-nixpacks/builder-release b/plugins/builder-nixpacks/builder-release index ca75d124f..bf90016c7 100755 --- a/plugins/builder-nixpacks/builder-release +++ b/plugins/builder-nixpacks/builder-release @@ -19,6 +19,9 @@ trigger-builder-nixpacks-builder-release() { trap "rm -rf '$TMP_WORK_DIR' >/dev/null" RETURN INT TERM EXIT local DOCKER_BUILD_LABEL_ARGS=("--label=org.label-schema.schema-version=1.0" "--label=org.label-schema.vendor=dokku" "--label=com.dokku.image-stage=release" "--label=com.dokku.builder-type=nixpacks" "--label=com.dokku.app-name=$APP" "--label=dokku") + local DOCKER_CONFIG + DOCKER_CONFIG="$(fn-registry-docker-config "$APP")" + [[ -n "$DOCKER_CONFIG" ]] && export DOCKER_CONFIG if ! suppress_output "$DOCKER_BIN" image build "${DOCKER_BUILD_LABEL_ARGS[@]}" $DOKKU_GLOBAL_BUILD_ARGS -f "$PLUGIN_AVAILABLE_PATH/builder-nixpacks/dockerfiles/builder-release.Dockerfile" --build-arg APP_IMAGE="$IMAGE" -t "$IMAGE" "$TMP_WORK_DIR"; then dokku_log_warn "Failure injecting docker labels on image" return 1 diff --git a/plugins/builder-pack/builder-build b/plugins/builder-pack/builder-build index 7457b51d7..0041e0d23 100755 --- a/plugins/builder-pack/builder-build +++ b/plugins/builder-pack/builder-build @@ -293,6 +293,9 @@ trigger-builder-pack-builder-build() { done <"$SOURCECODE_WORK_DIR/.buildpacks" fi + local DOCKER_CONFIG + DOCKER_CONFIG="$(fn-registry-docker-config "$APP")" + [[ -n "$DOCKER_CONFIG" ]] && export DOCKER_CONFIG pack build "$IMAGE" --builder "$DOKKU_CNB_BUILDER" --path "$SOURCECODE_WORK_DIR" --default-process web "${PACK_ARGS[@]}" "${ENV_ARGS[@]}" docker-image-labeler relabel --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=pack --label=com.dokku.app-name=$APP "$IMAGE" diff --git a/plugins/builder-railpack/builder-build b/plugins/builder-railpack/builder-build index 1cfdd4322..092b02be8 100755 --- a/plugins/builder-railpack/builder-build +++ b/plugins/builder-railpack/builder-build @@ -125,7 +125,10 @@ trigger-builder-railpack-builder-build() { eval "$(config_export app "$APP" --merged)" - # shellcheck disable=SC2086 + local DOCKER_CONFIG + DOCKER_CONFIG="$(fn-registry-docker-config "$APP")" + [[ -n "$DOCKER_CONFIG" ]] && export DOCKER_CONFIG + if ! railpack build "${RAILPACK_ARGS[@]}" --name "$IMAGE-build" "$SOURCECODE_WORK_DIR"; then dokku_log_warn "Failure building image" return 1 diff --git a/plugins/builder-railpack/builder-release b/plugins/builder-railpack/builder-release index dbaa730f5..aca762295 100755 --- a/plugins/builder-railpack/builder-release +++ b/plugins/builder-railpack/builder-release @@ -19,7 +19,9 @@ trigger-builder-railpack-builder-release() { trap "rm -rf '$TMP_WORK_DIR' >/dev/null" RETURN INT TERM EXIT local DOCKER_BUILD_LABEL_ARGS=("--label=org.label-schema.schema-version=1.0" "--label=org.label-schema.vendor=dokku" "--label=com.dokku.image-stage=release" "--label=com.dokku.builder-type=railpack" "--label=com.dokku.app-name=$APP" "--label=dokku") - + local DOCKER_CONFIG + DOCKER_CONFIG="$(fn-registry-docker-config "$APP")" + [[ -n "$DOCKER_CONFIG" ]] && export DOCKER_CONFIG if ! suppress_output "$DOCKER_BIN" image build "${DOCKER_BUILD_LABEL_ARGS[@]}" $DOKKU_GLOBAL_BUILD_ARGS -f "$PLUGIN_AVAILABLE_PATH/builder-railpack/dockerfiles/builder-release.Dockerfile" --build-arg APP_IMAGE="$IMAGE" -t "$IMAGE" "$TMP_WORK_DIR"; then dokku_log_warn "Failure injecting docker labels on image" return 1 diff --git a/plugins/common/functions b/plugins/common/functions index 9a7e12c84..2bf73f328 100755 --- a/plugins/common/functions +++ b/plugins/common/functions @@ -947,3 +947,14 @@ fn-migrate-config-to-property() { fi done } + +fn-registry-docker-config() { + declare desc="returns docker config file path if per-app registry credentials exist" + declare APP="$1" + local config_dir="/var/lib/dokku/config/registry/$APP" + local config_file="$config_dir/config.json" + + if [[ -f "$config_file" ]] && [[ "$(jq -r '.auths | length' "$config_file" 2>/dev/null)" != "0" ]]; then + echo "$config_file" + fi +} diff --git a/plugins/git/git-from-image b/plugins/git/git-from-image index e5731d2d9..9540dddac 100755 --- a/plugins/git/git-from-image +++ b/plugins/git/git-from-image @@ -26,6 +26,10 @@ trigger-git-git-from-image() { echo "FROM $DOCKER_IMAGE" >>"$TMP_WORK_DIR/Dockerfile" echo "LABEL com.dokku.docker-image-labeler/alternate-tags=[\\\"$DOCKER_IMAGE\\\"]" >>"$TMP_WORK_DIR/Dockerfile" + local DOCKER_CONFIG + DOCKER_CONFIG="$(fn-registry-docker-config "$APP")" + [[ -n "$DOCKER_CONFIG" ]] && export DOCKER_CONFIG + if [[ "$("$DOCKER_BIN" image ls -q "$DOCKER_IMAGE" 2>/dev/null)" == "" ]]; then dokku_log_info1 "Pulling image" if ! "$DOCKER_BIN" image pull "$DOCKER_IMAGE"; then diff --git a/plugins/registry/Makefile b/plugins/registry/Makefile index dc8ac2e7f..82ab1be61 100644 --- a/plugins/registry/Makefile +++ b/plugins/registry/Makefile @@ -1,5 +1,5 @@ -SUBCOMMANDS = subcommands/login subcommands/report subcommands/set -TRIGGERS = triggers/deployed-app-image-repo triggers/deployed-app-image-tag triggers/deployed-app-repository triggers/install triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/post-delete triggers/post-release-builder triggers/report +SUBCOMMANDS = subcommands/login subcommands/logout subcommands/report subcommands/set +TRIGGERS = triggers/deployed-app-image-repo triggers/deployed-app-image-tag triggers/deployed-app-repository triggers/install triggers/post-app-clone-setup triggers/post-app-rename-setup triggers/post-create triggers/post-delete triggers/post-release-builder triggers/report BUILD = commands subcommands triggers PLUGIN_NAME = registry diff --git a/plugins/registry/functions.go b/plugins/registry/functions.go index eb85e060b..4c421d75d 100644 --- a/plugins/registry/functions.go +++ b/plugins/registry/functions.go @@ -2,7 +2,10 @@ package registry import ( "bytes" + "encoding/json" "fmt" + "os" + "path/filepath" "strconv" "strings" "text/template" @@ -10,6 +13,47 @@ import ( "github.com/dokku/dokku/plugins/common" ) +const registryConfigDir = "/var/lib/dokku/config/registry" + +// GetAppRegistryConfigDir returns the per-app registry config directory +func GetAppRegistryConfigDir(appName string) string { + return filepath.Join(registryConfigDir, appName) +} + +// GetAppRegistryConfigPath returns the path to per-app docker config.json +func GetAppRegistryConfigPath(appName string) string { + return filepath.Join(GetAppRegistryConfigDir(appName), "config.json") +} + +// HasAppRegistryAuth checks if an app has registry credentials configured +func HasAppRegistryAuth(appName string) bool { + configPath := GetAppRegistryConfigPath(appName) + if !common.FileExists(configPath) { + return false + } + + content, err := os.ReadFile(configPath) + if err != nil { + return false + } + + var config map[string]interface{} + if err := json.Unmarshal(content, &config); err != nil { + return false + } + + auths, ok := config["auths"].(map[string]interface{}) + return ok && len(auths) > 0 +} + +// GetDockerConfigArgs returns docker --config arguments if per-app config exists +func GetDockerConfigArgs(appName string) []string { + if appName == "" || !HasAppRegistryAuth(appName) { + return []string{} + } + return []string{"--config", GetAppRegistryConfigDir(appName)} +} + func getImageRepoFromTemplate(appName string) (string, error) { imageRepoTemplate := common.PropertyGet("registry", "--global", "image-repo-template") if imageRepoTemplate == "" { @@ -119,14 +163,14 @@ func pushToRegistry(appName string, tag int, imageID string, imageRepo string) e } }() common.LogVerboseQuiet(fmt.Sprintf("Pushing %s", extraTagImage)) - if err := dockerPush(extraTagImage); err != nil { + if err := dockerPush(appName, extraTagImage); err != nil { return fmt.Errorf("unable to push image with %s tag: %w", extraTag, err) } } } common.LogVerboseQuiet(fmt.Sprintf("Pushing %s", fullImage)) - if err := dockerPush(fullImage); err != nil { + if err := dockerPush(appName, fullImage); err != nil { return fmt.Errorf("unable to push image %s: %w", fullImage, err) } @@ -159,17 +203,19 @@ func dockerTag(imageID string, imageTag string) error { return nil } -func dockerPush(imageTag string) error { +func dockerPush(appName string, imageTag string) error { + args := GetDockerConfigArgs(appName) + args = append(args, "image", "push", imageTag) result, err := common.CallExecCommand(common.ExecCommandInput{ Command: common.DockerBin(), - Args: []string{"image", "push", imageTag}, + Args: args, StreamStdio: true, }) if err != nil { - return fmt.Errorf("docker push command failed: %w", err) + return fmt.Errorf("docker image push command failed: %w", err) } if result.ExitCode != 0 { - return fmt.Errorf("docker push command exited with code %d: %s", result.ExitCode, result.Stderr) + return fmt.Errorf("docker image push command exited with code %d: %s", result.ExitCode, result.Stderr) } return nil } diff --git a/plugins/registry/src/commands/commands.go b/plugins/registry/src/commands/commands.go index d665f17b9..247acee0a 100644 --- a/plugins/registry/src/commands/commands.go +++ b/plugins/registry/src/commands/commands.go @@ -18,9 +18,10 @@ Manage registry settings for an app Additional commands:` helpContent = ` - registry:login [--password-stdin] [], Login to a docker registry + registry:login [--global|--password-stdin] [] [], Login to a docker registry + registry:logout [--global] [] , Logout from a docker registry registry:report [] [], Displays a registry report for one or more apps - registry:set (), Set or clear a registry property for an app` + registry:set |--global (), Set or clear a registry property for an app` ) func main() { diff --git a/plugins/registry/src/subcommands/subcommands.go b/plugins/registry/src/subcommands/subcommands.go index 0ed00197b..35377be84 100644 --- a/plugins/registry/src/subcommands/subcommands.go +++ b/plugins/registry/src/subcommands/subcommands.go @@ -21,11 +21,66 @@ func main() { case "login": args := flag.NewFlagSet("registry:login", flag.ExitOnError) passwordStdin := args.Bool("password-stdin", false, "--password-stdin: read password from stdin") + global := args.Bool("global", false, "--global: login globally instead of per-app") args.Parse(os.Args[2:]) - server := args.Arg(0) - username := args.Arg(1) - password := args.Arg(2) - err = registry.CommandLogin(server, username, password, *passwordStdin) + + argCount := args.NArg() + var appName, server, username, password string + + // When --password-stdin is used, password is not in args + // Global login: 2 args (server, username) with --password-stdin, 3 args without + // Per-app login: 3 args (app, server, username) with --password-stdin, 4 args without + globalArgCount := 3 + perAppArgCount := 4 + if *passwordStdin { + globalArgCount = 2 + perAppArgCount = 3 + } + + if *global { + // --global: server, username, [password] + server = args.Arg(0) + username = args.Arg(1) + if !*passwordStdin { + password = args.Arg(2) + } + } else if argCount == globalArgCount { + // global login without --global flag: warn and treat as global + common.LogWarn("Deprecated: please use --global flag for global registry login") + server = args.Arg(0) + username = args.Arg(1) + if !*passwordStdin { + password = args.Arg(2) + } + } else if argCount >= perAppArgCount { + // per-app login: app, server, username, [password] + appName = args.Arg(0) + server = args.Arg(1) + username = args.Arg(2) + if !*passwordStdin { + password = args.Arg(3) + } + } + + err = registry.CommandLogin(appName, server, username, password, *passwordStdin) + case "logout": + args := flag.NewFlagSet("registry:logout", flag.ExitOnError) + global := args.Bool("global", false, "--global: logout globally instead of per-app") + args.Parse(os.Args[2:]) + + var appName, server string + if *global { + server = args.Arg(0) + } else if args.NArg() == 1 { + // 1 arg: global logout (backwards compatible) + server = args.Arg(0) + } else { + // 2 args: app, server + appName = args.Arg(0) + server = args.Arg(1) + } + + err = registry.CommandLogout(appName, server) case "report": args := flag.NewFlagSet("registry:report", flag.ExitOnError) format := args.String("format", "stdout", "format: [ stdout | json ]") diff --git a/plugins/registry/src/triggers/triggers.go b/plugins/registry/src/triggers/triggers.go index 3571aa15f..f1f64dd87 100644 --- a/plugins/registry/src/triggers/triggers.go +++ b/plugins/registry/src/triggers/triggers.go @@ -29,6 +29,9 @@ func main() { err = registry.TriggerDeployedAppRepository(appName) case "install": err = registry.TriggerInstall() + case "post-create": + appName := flag.Arg(0) + err = registry.TriggerPostCreate(appName) case "post-app-clone-setup": oldAppName := flag.Arg(0) newAppName := flag.Arg(1) diff --git a/plugins/registry/subcommands.go b/plugins/registry/subcommands.go index eb7e68757..b4bde8b7c 100644 --- a/plugins/registry/subcommands.go +++ b/plugins/registry/subcommands.go @@ -12,7 +12,7 @@ import ( ) // CommandLogin logs a user into the specified server -func CommandLogin(server string, username string, password string, passwordStdin bool) error { +func CommandLogin(appName string, server string, username string, password string, passwordStdin bool) error { if passwordStdin { stdin, err := io.ReadAll(os.Stdin) if err != nil { @@ -39,9 +39,22 @@ func CommandLogin(server string, username string, password string, passwordStdin buffer := bytes.Buffer{} buffer.Write([]byte(password + "\n")) + env := map[string]string{} + if appName != "" { + if err := common.VerifyAppName(appName); err != nil { + return err + } + configDir := GetAppRegistryConfigDir(appName) + if err := os.MkdirAll(configDir, 0700); err != nil { + return fmt.Errorf("Unable to create registry config directory: %w", err) + } + env["DOCKER_CONFIG"] = GetAppRegistryConfigPath(appName) + } + result, err := common.CallExecCommand(common.ExecCommandInput{ Command: common.DockerBin(), Args: []string{"login", "--username", username, "--password-stdin", server}, + Env: env, Stdin: &buffer, StreamStdio: true, }) @@ -52,6 +65,7 @@ func CommandLogin(server string, username string, password string, passwordStdin return fmt.Errorf("Unable to run docker login: %s", result.StderrContents()) } + // todo: change the signature of the trigger to include the app name _, err = common.CallPlugnTrigger(common.PlugnTriggerInput{ Trigger: "post-registry-login", Args: []string{server, username}, @@ -67,6 +81,44 @@ func CommandLogin(server string, username string, password string, passwordStdin return nil } +// CommandLogout logs a user out from the specified server +func CommandLogout(appName string, server string) error { + if server == "" { + return errors.New("Missing server argument") + } + + if server == "hub.docker.com" || server == "docker.com" { + server = "docker.io" + } + + env := map[string]string{} + if appName != "" { + if err := common.VerifyAppName(appName); err != nil { + return err + } + configDir := GetAppRegistryConfigDir(appName) + if !common.DirectoryExists(configDir) { + return fmt.Errorf("No registry credentials found for app %s", appName) + } + env["DOCKER_CONFIG"] = GetAppRegistryConfigPath(appName) + } + + result, err := common.CallExecCommand(common.ExecCommandInput{ + Command: common.DockerBin(), + Args: []string{"logout", server}, + Env: env, + StreamStdio: true, + }) + if err != nil { + return fmt.Errorf("Unable to run docker logout: %w", err) + } + if result.ExitCode != 0 { + return fmt.Errorf("Unable to run docker logout: %s", result.StderrContents()) + } + + return nil +} + // CommandReport displays a registry report for one or more apps func CommandReport(appName string, format string, infoFlag string) error { if len(appName) == 0 { diff --git a/plugins/registry/triggers.go b/plugins/registry/triggers.go index 30cc97564..665641185 100644 --- a/plugins/registry/triggers.go +++ b/plugins/registry/triggers.go @@ -2,6 +2,7 @@ package registry import ( "fmt" + "os" "strings" "github.com/dokku/dokku/plugins/common" @@ -57,6 +58,11 @@ func TriggerInstall() error { return nil } +// TriggerPostCreate creates the registry config directory for a new app +func TriggerPostCreate(appName string) error { + return common.PropertySetupApp("registry", appName) +} + // TriggerPostAppCloneSetup creates new registry files func TriggerPostAppCloneSetup(oldAppName string, newAppName string) error { err := common.PropertyClone("registry", oldAppName, newAppName) @@ -64,6 +70,18 @@ func TriggerPostAppCloneSetup(oldAppName string, newAppName string) error { return err } + // Clone docker config.json if it exists + oldConfigPath := GetAppRegistryConfigPath(oldAppName) + if common.FileExists(oldConfigPath) { + newConfigDir := GetAppRegistryConfigDir(newAppName) + if err := os.MkdirAll(newConfigDir, 0700); err != nil { + return fmt.Errorf("Unable to create registry config directory: %w", err) + } + if err := common.Copy(oldConfigPath, GetAppRegistryConfigPath(newAppName)); err != nil { + return fmt.Errorf("Unable to clone registry config: %w", err) + } + } + return nil } @@ -73,6 +91,18 @@ func TriggerPostAppRenameSetup(oldAppName string, newAppName string) error { return err } + // Move docker config.json if it exists + oldConfigPath := GetAppRegistryConfigPath(oldAppName) + if common.FileExists(oldConfigPath) { + newConfigDir := GetAppRegistryConfigDir(newAppName) + if err := os.MkdirAll(newConfigDir, 0700); err != nil { + return fmt.Errorf("Unable to create registry config directory: %w", err) + } + if err := os.Rename(oldConfigPath, GetAppRegistryConfigPath(newAppName)); err != nil { + return fmt.Errorf("Unable to rename registry config: %w", err) + } + } + if err := common.PropertyDestroy("registry", oldAppName); err != nil { return err } diff --git a/tests/unit/registry.bats b/tests/unit/registry.bats index 321c7bf31..a6dba06bd 100644 --- a/tests/unit/registry.bats +++ b/tests/unit/registry.bats @@ -27,7 +27,7 @@ teardown() { assert_output "$help_output" } -@test "(registry) registry:login" { +@test "(registry:login) global login with deprecated warning" { if [[ -z "$DOCKERHUB_USERNAME" ]] || [[ -z "$DOCKERHUB_TOKEN" ]]; then skip "skipping due to missing docker.io credentials DOCKERHUB_USERNAME:DOCKERHUB_TOKEN" fi @@ -37,6 +37,7 @@ teardown() { echo "status: $status" assert_success assert_output_contains "Login Succeeded" + assert_output_contains "Deprecated: please use --global flag" run /bin/bash -c "echo $DOCKERHUB_TOKEN | dokku registry:login docker.io --password-stdin $DOCKERHUB_USERNAME" echo "output: $output" @@ -45,6 +46,93 @@ teardown() { assert_output_contains "Login Succeeded" } +@test "(registry:login) global login with --global flag" { + 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 registry:login --global docker.io $DOCKERHUB_USERNAME $DOCKERHUB_TOKEN" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Login Succeeded" + assert_output_not_contains "Deprecated" +} + +@test "(registry:login) per-app login" { + 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 registry:login $TEST_APP docker.io $DOCKERHUB_USERNAME $DOCKERHUB_TOKEN" + echo "output: $output" + echo "status: $status" + assert_success + assert_output_contains "Login Succeeded" + + run /bin/bash -c "test -f /var/lib/dokku/config/registry/$TEST_APP/config.json" + echo "output: $output" + echo "status: $status" + assert_success +} + +@test "(registry:logout) per-app logout" { + 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 registry:login $TEST_APP docker.io $DOCKERHUB_USERNAME $DOCKERHUB_TOKEN" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku registry:logout $TEST_APP docker.io" + echo "output: $output" + echo "status: $status" + assert_success +} + +@test "(registry:logout) global logout" { + 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 registry:login --global docker.io $DOCKERHUB_USERNAME $DOCKERHUB_TOKEN" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "dokku registry:logout --global docker.io" + echo "output: $output" + echo "status: $status" + assert_success +} + +@test "(registry) per-app credentials deleted on app destroy" { + 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 registry:login $TEST_APP docker.io $DOCKERHUB_USERNAME $DOCKERHUB_TOKEN" + echo "output: $output" + echo "status: $status" + assert_success + + run /bin/bash -c "test -d /var/lib/dokku/config/registry/$TEST_APP" + echo "output: $output" + echo "status: $status" + assert_success + + destroy_app + + run /bin/bash -c "test -d /var/lib/dokku/config/registry/$TEST_APP" + echo "output: $output" + echo "status: $status" + assert_failure + + create_app +} + @test "(registry) registry:set server" { run /bin/bash -c "dokku registry:set --global server ghcr.io" echo "output: $output"