Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cmd/hauler/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/spf13/cobra"
"hauler.dev/go/hauler/v2/internal/flags"
"hauler.dev/go/hauler/v2/pkg/consts"
"hauler.dev/go/hauler/v2/pkg/content"
"hauler.dev/go/hauler/v2/pkg/log"
)

Expand Down Expand Up @@ -46,6 +47,10 @@ func New(ctx context.Context, ro *flags.CliRootOpts) *cobra.Command {
l.SetLevel(ro.LogLevel)
l.Debugf("running cli command [%s]", cmd.CommandPath())

if dir, set := content.SetDefaultDockerConfig(); set {
l.Debugf("defaulted $DOCKER_CONFIG to [%s] for registry credential resolution", dir)
}

if ro.LogLevel == "debug" {
logrus.SetLevel(logrus.DebugLevel)
} else {
Expand Down
54 changes: 54 additions & 0 deletions pkg/content/dockerconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package content

import (
"os"
"os/user"
"path/filepath"
)

// currentUser is a seam so tests can simulate a running UID with no passwd
// entry (where user.Current() returns an error).
var currentUser = user.Current

// SetDefaultDockerConfig defaults the DOCKER_CONFIG environment variable to the
// directory where `hauler login` (crane -> docker/cli) writes credentials, so
// that go-containerregistry's authn.DefaultKeychain -- used to resolve registry
// credentials during `hauler store copy registry://`, `add`, and `sync` -- can
// find them even when $HOME is unset.
//
// It replicates docker/cli's config.Dir()/getHomeDir() resolution using only the
// standard library (no docker/cli dependency): DOCKER_CONFIG if already set,
// otherwise <home>/.docker, where home is os.UserHomeDir() ($HOME on Unix,
// %USERPROFILE% on Windows) with an /etc/passwd fallback via os/user.Current()
// when $HOME is empty -- exactly the passwd fallback `hauler login` uses.
//
// When no home directory can be resolved at all ($HOME empty AND
// user.Current() fails or returns an empty HomeDir), this mirrors docker/cli's
// own config.Dir(), which computes filepath.Join(getHomeDir(), ".docker"): with
// getHomeDir() == "", that join collapses to the relative path ".docker". So
// DOCKER_CONFIG is defaulted to the relative path ".docker" in that case too,
// keeping `hauler login`'s (relative) write and the keychain's (relative) read
// in agreement as long as both run from the same working directory.
//
// The only remaining no-op case is when DOCKER_CONFIG is already explicitly
// set (an explicit value always wins). Setting DOCKER_CONFIG does not disable
// DefaultKeychain's $REGISTRY_AUTH_FILE / Podman auth.json fallbacks: those
// still apply when no config.json exists at the set path.
//
// Returns the directory it set and true, or "" and false if it made no change.
func SetDefaultDockerConfig() (string, bool) {
if os.Getenv("DOCKER_CONFIG") != "" {
return "", false
}

home, err := os.UserHomeDir()
if err != nil || home == "" {
if u, uerr := currentUser(); uerr == nil {
home = u.HomeDir
}
}

dir := filepath.Join(home, ".docker")
os.Setenv("DOCKER_CONFIG", dir)
return dir, true
}
10 changes: 7 additions & 3 deletions pkg/content/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,19 @@ func NewRegistryTarget(host string, opts RegistryOptions, client *http.Client) *
// Bridge to go-containerregistry's keychain for credential lookup.
reg, err := goname.NewRegistry(h, goname.Insecure)
if err != nil {
return "", "", nil
return "", "", fmt.Errorf("parsing registry host [%s] for credential lookup: %w", h, err)
}
a, err := goauthn.DefaultKeychain.Resolve(reg)
if err != nil || a == goauthn.Anonymous {
if err != nil {
// don't fall back to anonymous on a real resolution error
return "", "", fmt.Errorf("resolving credentials for [%s]: %w", h, err)
}
if a == goauthn.Anonymous {
return "", "", nil
}
cfg, err := a.Authorization()
if err != nil {
return "", "", nil
return "", "", fmt.Errorf("reading resolved authorization for [%s]: %w", h, err)
}
return cfg.Username, cfg.Password, nil
}),
Expand Down
Loading