From 7d797483b89bb2bcc9e6f76c2e437986c44b6461 Mon Sep 17 00:00:00 2001 From: Ryan Noronha Date: Sun, 28 Jun 2026 22:39:26 +0530 Subject: [PATCH 1/2] feat: Use auth configs provided in Docker config file Detects a mounted Docker configuration file and parses it for authentication information and passes the authentication details to the Docker client for building the chaincode image. closes #5504 Signed-off-by: Ryan Noronha --- .../dockercontroller/dockercontroller.go | 51 +++++++++++++++++ .../dockercontroller/dockercontroller_test.go | 57 +++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/core/container/dockercontroller/dockercontroller.go b/core/container/dockercontroller/dockercontroller.go index 6995e0ad28d..40767deaecb 100644 --- a/core/container/dockercontroller/dockercontroller.go +++ b/core/container/dockercontroller/dockercontroller.go @@ -13,8 +13,11 @@ import ( "context" "encoding/base64" "encoding/hex" + "encoding/json" "fmt" "io" + "os" + "path/filepath" "regexp" "strconv" "strings" @@ -27,6 +30,7 @@ import ( "github.com/hyperledger/fabric/core/container" "github.com/hyperledger/fabric/core/container/ccintf" dcontainer "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/registry" dcli "github.com/moby/moby/client" "github.com/pkg/errors" ) @@ -114,11 +118,14 @@ func (vm *DockerVM) buildImage(ccid string, reader io.Reader) error { return err } + authConfigs := vm.getAuthFromDockerConfig() + startTime := time.Now() res, err := vm.Client.ImageBuild(context.Background(), reader, dcli.ImageBuildOptions{ Tags: []string{id}, PullParent: vm.ChaincodePull, NetworkMode: vm.NetworkMode, + AuthConfigs: authConfigs, }) vm.BuildMetrics.ChaincodeImageBuildDuration.With( @@ -139,6 +146,50 @@ func (vm *DockerVM) buildImage(ccid string, reader io.Reader) error { return nil } +type dockerConfigFile struct { + Auths map[string]registry.AuthConfig `json:"auths,omitempty"` +} + +func (vm *DockerVM) getAuthFromDockerConfig() map[string]registry.AuthConfig { + emptyConfig := make(map[string]registry.AuthConfig) + + homeEnv := os.Getenv("HOME") + if homeEnv == "" { + return emptyConfig + } + + validPaths := []string{ + filepath.Join(homeEnv, ".docker", "config.json"), + filepath.Join(homeEnv, ".dockercfg"), + } + for _, p := range validPaths { + file, err := os.Open(p) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + dockerLogger.Debugf("Docker config file \"%s\" was not found", p) + continue + } + + dockerLogger.Debugf("Unhandled error occurred while trying to read Docker config file \"%s\". Returning auth empty config.", p) + return emptyConfig + } + defer file.Close() + + var currConfig dockerConfigFile + err = json.NewDecoder(file).Decode(&currConfig) + if err != nil { + return emptyConfig + } + + if len(currConfig.Auths) > 0 { + return currConfig.Auths + } + } + + dockerLogger.Debugf("No Docker config files were found. Returning empty auth config.") + return emptyConfig +} + // Build is responsible for building an image if it does not already exist. func (vm *DockerVM) Build(ccid string, metadata *persistence.ChaincodePackageMetadata, codePackage io.Reader) (container.Instance, error) { imageName, err := vm.GetVMNameForDocker(ccid) diff --git a/core/container/dockercontroller/dockercontroller_test.go b/core/container/dockercontroller/dockercontroller_test.go index 58542f49e7e..8aa55dc2eeb 100644 --- a/core/container/dockercontroller/dockercontroller_test.go +++ b/core/container/dockercontroller/dockercontroller_test.go @@ -16,6 +16,8 @@ import ( "errors" "fmt" "io" + "os" + "path/filepath" "testing" "time" @@ -428,6 +430,61 @@ func Test_buildImageFailure(t *testing.T) { require.EqualError(t, err, "oh-bother-we-failed-badly") } +func Test_getAuthConfigFromDockerConfig(t *testing.T) { + tempDir := t.TempDir() + t.Setenv("HOME", tempDir) + + configFile := []byte(` +{ + "auths": { + "repo.example.com": { + "auth": "exampleAuthString" + } + } +} + `) + + rc := &mock.ReadCloser{} + rc.ReadReturns(0, io.EOF) + client := &mock.DockerClient{} + client.ImageBuildReturns(dcli.ImageBuildResult{Body: rc}, nil) + + t.Run("when .dockercfg is available with auth config", func(t *testing.T) { + configFilePath := filepath.Join(tempDir, ".dockercfg") + err := os.WriteFile(configFilePath, configFile, 0o600) + require.NoError(t, err) + + dvm := DockerVM{ + BuildMetrics: NewBuildMetrics(&disabled.Provider{}), + Client: client, + NetworkMode: "network-mode", + } + + configs := dvm.getAuthFromDockerConfig() + require.Greater(t, len(configs), 0, "len of auth configs must be greater than 0") + }) + + t.Run("when config.json is available with auth config", func(t *testing.T) { + dockerDir := filepath.Join(tempDir, ".docker") + if _, err := os.Stat(dockerDir); err != nil { + _ = os.Mkdir(dockerDir, 0o700) + } + + configFilePath := filepath.Join(dockerDir, "config.json") + err := os.WriteFile(configFilePath, configFile, 0o644) + require.NoError(t, err) + + dvm := DockerVM{ + BuildMetrics: NewBuildMetrics(&disabled.Provider{}), + Client: client, + NetworkMode: "network-mode", + } + + configs := dvm.getAuthFromDockerConfig() + require.Greater(t, len(configs), 0, "len of auth configs should not be 0") + }) +} + func TestBuild(t *testing.T) { buildMetrics := NewBuildMetrics(&disabled.Provider{}) md := &persistence.ChaincodePackageMetadata{ From 1d219f32b759f77f21ab0b8e273485cadb0ffb87 Mon Sep 17 00:00:00 2001 From: Ryan Noronha Date: Mon, 13 Jul 2026 23:04:21 +0530 Subject: [PATCH 2/2] fix: convert base64 auth string to username and password for auth config * Converts base64 encoded "auth" string to username and password before sending it in the auth configs while building an image. * Remove `~/.dockercfg` config file support. Signed-off-by: Ryan Noronha --- .../dockercontroller/dockercontroller.go | 102 +++++++++++++----- .../dockercontroller/dockercontroller_test.go | 47 +++----- 2 files changed, 88 insertions(+), 61 deletions(-) diff --git a/core/container/dockercontroller/dockercontroller.go b/core/container/dockercontroller/dockercontroller.go index 40767deaecb..e1f4a2f376a 100644 --- a/core/container/dockercontroller/dockercontroller.go +++ b/core/container/dockercontroller/dockercontroller.go @@ -118,14 +118,12 @@ func (vm *DockerVM) buildImage(ccid string, reader io.Reader) error { return err } - authConfigs := vm.getAuthFromDockerConfig() - startTime := time.Now() res, err := vm.Client.ImageBuild(context.Background(), reader, dcli.ImageBuildOptions{ Tags: []string{id}, PullParent: vm.ChaincodePull, NetworkMode: vm.NetworkMode, - AuthConfigs: authConfigs, + AuthConfigs: getAuthFromDockerConfig(), }) vm.BuildMetrics.ChaincodeImageBuildDuration.With( @@ -146,48 +144,94 @@ func (vm *DockerVM) buildImage(ccid string, reader io.Reader) error { return nil } +// dockerConfigFile is a minimal struct for unmarshalling authentication details +// from the Docker config file. type dockerConfigFile struct { Auths map[string]registry.AuthConfig `json:"auths,omitempty"` } -func (vm *DockerVM) getAuthFromDockerConfig() map[string]registry.AuthConfig { +// getAuthFromDockerConfig gets the authentication configuration that may be +// provided in the Docker config file. +func getAuthFromDockerConfig() map[string]registry.AuthConfig { emptyConfig := make(map[string]registry.AuthConfig) - homeEnv := os.Getenv("HOME") - if homeEnv == "" { + configFilePath := filepath.Join(getDockerConfigDir(), "config.json") + + file, err := os.Open(configFilePath) + if err != nil { + dockerLogger.Debugf("Error occurred while trying to read Docker config file from path \"%s\". Returning empty auth config.", configFilePath) return emptyConfig } + defer file.Close() - validPaths := []string{ - filepath.Join(homeEnv, ".docker", "config.json"), - filepath.Join(homeEnv, ".dockercfg"), + var currConfig dockerConfigFile + err = json.NewDecoder(file).Decode(&currConfig) + if err != nil { + return emptyConfig } - for _, p := range validPaths { - file, err := os.Open(p) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - dockerLogger.Debugf("Docker config file \"%s\" was not found", p) - continue - } - dockerLogger.Debugf("Unhandled error occurred while trying to read Docker config file \"%s\". Returning auth empty config.", p) - return emptyConfig - } - defer file.Close() + mappedAuths, err := mapDockerConfigAuth(currConfig.Auths) + if err != nil { + dockerLogger.Debugf("Error while decoding Docker auth: %s. Returning empty auth config.", err.Error()) + return emptyConfig + } - var currConfig dockerConfigFile - err = json.NewDecoder(file).Decode(&currConfig) - if err != nil { - return emptyConfig - } + return mappedAuths +} + +func getDockerConfigDir() string { + homeDir, _ := os.UserHomeDir() - if len(currConfig.Auths) > 0 { - return currConfig.Auths + // The value of DOCKER_CONFIG environment variable overrides the default + // config directory (~/.docker). This behaviour mimics that of Docker CLI. + configDir := os.Getenv("DOCKER_CONFIG") + if configDir == "" { + configDir = filepath.Join(homeDir, ".docker") + } + + return configDir +} + +// mapDockerConfigAuth maps the provided Docker auth configuration to the format +// expected by the Docker Engine (i.e. Username and Password based auth). +func mapDockerConfigAuth(auths map[string]registry.AuthConfig) (map[string]registry.AuthConfig, error) { + mappedAuth := make(map[string]registry.AuthConfig) + + for reg, auth := range auths { + if auth.Auth != "" { + username, password, err := decodeAuth(auth.Auth) + if err != nil { + return mappedAuth, err + } + + mappedAuth[reg] = registry.AuthConfig{ + Username: username, + Password: password, + } } } - dockerLogger.Debugf("No Docker config files were found. Returning empty auth config.") - return emptyConfig + return mappedAuth, nil +} + +// decodeAuth decodes the provided Docker authentication configuration as a +// Base64 encoded string and returns the username and password from it. +func decodeAuth(auth string) (string, string, error) { + decLen := base64.StdEncoding.DecodedLen(len(auth)) + decoded := make([]byte, decLen) + + n, err := base64.StdEncoding.Decode(decoded, []byte(auth)) + if err != nil { + return "", "", err + } + if n > decLen { + return "", "", errors.New("something went wrong decoding auth config") + } + userName, password, ok := strings.Cut(string(decoded), ":") + if !ok || userName == "" { + return "", "", errors.New("invalid auth configuration file") + } + return userName, strings.Trim(password, "\x00"), nil } // Build is responsible for building an image if it does not already exist. diff --git a/core/container/dockercontroller/dockercontroller_test.go b/core/container/dockercontroller/dockercontroller_test.go index 8aa55dc2eeb..8cfe24e810b 100644 --- a/core/container/dockercontroller/dockercontroller_test.go +++ b/core/container/dockercontroller/dockercontroller_test.go @@ -438,7 +438,7 @@ func Test_getAuthConfigFromDockerConfig(t *testing.T) { { "auths": { "repo.example.com": { - "auth": "exampleAuthString" + "auth": "dXNlcm5hbWU6cGFzc3dvcmQK" } } } @@ -449,40 +449,23 @@ func Test_getAuthConfigFromDockerConfig(t *testing.T) { client := &mock.DockerClient{} client.ImageBuildReturns(dcli.ImageBuildResult{Body: rc}, nil) - t.Run("when .dockercfg is available with auth config", func(t *testing.T) { - configFilePath := filepath.Join(tempDir, ".dockercfg") - err := os.WriteFile(configFilePath, configFile, 0o600) - require.NoError(t, err) - - dvm := DockerVM{ - BuildMetrics: NewBuildMetrics(&disabled.Provider{}), - Client: client, - NetworkMode: "network-mode", - } - - configs := dvm.getAuthFromDockerConfig() - require.Greater(t, len(configs), 0, "len of auth configs must be greater than 0") - }) - - t.Run("when config.json is available with auth config", func(t *testing.T) { - dockerDir := filepath.Join(tempDir, ".docker") - if _, err := os.Stat(dockerDir); err != nil { - _ = os.Mkdir(dockerDir, 0o700) - } + dockerDir := filepath.Join(tempDir, ".docker") + if _, err := os.Stat(dockerDir); err != nil { + _ = os.Mkdir(dockerDir, 0o700) + } - configFilePath := filepath.Join(dockerDir, "config.json") - err := os.WriteFile(configFilePath, configFile, 0o644) - require.NoError(t, err) + configFilePath := filepath.Join(dockerDir, "config.json") + err := os.WriteFile(configFilePath, configFile, 0o644) + require.NoError(t, err) - dvm := DockerVM{ - BuildMetrics: NewBuildMetrics(&disabled.Provider{}), - Client: client, - NetworkMode: "network-mode", - } + configs := getAuthFromDockerConfig() + require.Greater(t, len(configs), 0, "len of auth configs should not be 0") - configs := dvm.getAuthFromDockerConfig() - require.Greater(t, len(configs), 0, "len of auth configs should not be 0") - }) + repoAuth, ok := configs["repo.example.com"] + require.True(t, ok, "the key repo.example.com should be set") + require.Empty(t, repoAuth.Auth) + require.NotEmpty(t, repoAuth.Username) + require.NotEmpty(t, repoAuth.Password) } func TestBuild(t *testing.T) {