Merge pull request #8268 from dokku/5324-login-to-registry-per-app

Add the ability to log into a registry on a per-app basis
This commit is contained in:
Jose Diaz-Gonzalez
2026-01-10 08:48:16 -05:00
committed by GitHub
25 changed files with 419 additions and 34 deletions

View File

@@ -4,9 +4,10 @@
> New as of 0.25.0
```
registry:login [--password-stdin] <server> <username> [<password>] # Login to a docker registry
registry:report [<app>] [<flag>] # Displays a registry report for one or more apps
registry:set <app> <key> (<value>) # Set or clear a registry property for an app
registry:login [--global|--password-stdin] [<app>] <server> <username> [<password>] # Login to a docker registry
registry:logout [--global] [<app>] <server> # Logout from a docker registry
registry:report [<app>] [<flag>] # Displays a registry report for one or more apps
registry:set <app>|--global <key> (<value>) # 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.

View File

@@ -246,6 +246,9 @@ trigger-builder-dockerfile-builder-build() {
done
eval "$(config_export app "$APP")"
local DOCKER_CONFIG
DOCKER_CONFIG="$(fn-registry-docker-config-dir "$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")"

View File

@@ -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-dir "$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

View File

@@ -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-dir "$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

View File

@@ -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-dir "$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

View File

@@ -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-dir "$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

View File

@@ -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-dir "$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"

View File

@@ -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-dir "$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

View File

@@ -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-dir "$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

View File

@@ -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-dir "$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

View File

@@ -293,6 +293,9 @@ trigger-builder-pack-builder-build() {
done <"$SOURCECODE_WORK_DIR/.buildpacks"
fi
local DOCKER_CONFIG
DOCKER_CONFIG="$(fn-registry-docker-config-dir "$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"

View File

@@ -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-dir "$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

View File

@@ -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-dir "$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

View File

@@ -947,3 +947,14 @@ fn-migrate-config-to-property() {
fi
done
}
fn-registry-docker-config-dir() {
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_dir"
fi
}

View File

@@ -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-dir "$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

View File

@@ -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

View File

@@ -2,7 +2,10 @@ package registry
import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"text/template"
@@ -10,6 +13,55 @@ 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")
}
func GetComputedAppRegistryConfigDir(appName string) string {
if HasAppRegistryAuth(appName) {
return GetAppRegistryConfigDir(appName)
}
return filepath.Join(os.Getenv("DOKKU_ROOT"), ".docker")
}
// 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 +171,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 +211,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
}

View File

@@ -18,9 +18,10 @@ Manage registry settings for an app
Additional commands:`
helpContent = `
registry:login [--password-stdin] <server> <username> [<password>], Login to a docker registry
registry:login [--global|--password-stdin] [<app>] <server> <username> [<password>], Login to a docker registry
registry:logout [--global] [<app>] <server>, Logout from a docker registry
registry:report [<app>] [<flag>], Displays a registry report for one or more apps
registry:set <app> <property> (<value>), Set or clear a registry property for an app`
registry:set <app>|--global <property> (<value>), Set or clear a registry property for an app`
)
func main() {

View File

@@ -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 ]")

View File

@@ -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)

View File

@@ -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,11 +39,23 @@ 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"] = GetAppRegistryConfigDir(appName)
}
result, err := common.CallExecCommand(common.ExecCommandInput{
Command: common.DockerBin(),
Args: []string{"login", "--username", username, "--password-stdin", server},
Stdin: &buffer,
StreamStdio: true,
Command: common.DockerBin(),
Args: []string{"login", "--username", username, "--password-stdin", server},
Env: env,
Stdin: &buffer,
})
if err != nil {
return fmt.Errorf("Unable to run docker login: %w", err)
@@ -52,6 +64,13 @@ func CommandLogin(server string, username string, password string, passwordStdin
return fmt.Errorf("Unable to run docker login: %s", result.StderrContents())
}
if appName != "" {
common.LogWarn(fmt.Sprintf("Login Succeeded for %s", appName))
} else {
common.LogWarn("Login Succeeded for global registry")
}
// 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 +86,43 @@ 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"] = GetAppRegistryConfigDir(appName)
}
result, err := common.CallExecCommand(common.ExecCommandInput{
Command: common.DockerBin(),
Args: []string{"logout", server},
Env: env,
})
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 {

View File

@@ -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
}

View File

@@ -12,6 +12,7 @@ require (
github.com/dokku/dokku/plugins/docker-options v0.0.0-20250618161309-8d0c35f1333c
github.com/dokku/dokku/plugins/logs v0.0.0-20250618161309-8d0c35f1333c
github.com/dokku/dokku/plugins/nginx-vhosts v0.0.0-20250618161309-8d0c35f1333c
github.com/dokku/dokku/plugins/registry v0.0.0-20250618161309-8d0c35f1333c
github.com/fatih/color v1.18.0
github.com/fluxcd/pkg/kustomize v1.24.0
github.com/go-openapi/jsonpointer v0.22.4
@@ -190,6 +191,8 @@ replace github.com/dokku/dokku/plugins/logs => ../logs
replace github.com/dokku/dokku/plugins/nginx-vhosts => ../nginx-vhosts
replace github.com/dokku/dokku/plugins/registry => ../registry
replace github.com/joho/godotenv => github.com/joho/godotenv v1.2.0
replace github.com/imdario/mergo => github.com/imdario/mergo v0.3.16

View File

@@ -23,6 +23,7 @@ import (
"github.com/dokku/dokku/plugins/config"
"github.com/dokku/dokku/plugins/cron"
nginxvhosts "github.com/dokku/dokku/plugins/nginx-vhosts"
"github.com/dokku/dokku/plugins/registry"
"github.com/fatih/color"
"github.com/gosimple/slug"
"github.com/kballard/go-shellquote"
@@ -405,7 +406,7 @@ func TriggerSchedulerDeploy(scheduler string, appName string, imageTag string) e
pullSecretBase64 := base64.StdEncoding.EncodeToString([]byte(""))
imagePullSecrets := getComputedImagePullSecrets(appName)
if imagePullSecrets == "" {
dockerConfigPath := filepath.Join(os.Getenv("DOKKU_ROOT"), ".docker/config.json")
dockerConfigPath := filepath.Join(registry.GetComputedAppRegistryConfigDir(appName), "config.json")
if fi, err := os.Stat(dockerConfigPath); err == nil && !fi.IsDir() {
b, err := os.ReadFile(dockerConfigPath)
if err != nil {

View File

@@ -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"