diff --git a/container-engine-lib/lib/backend_impls/docker/docker_manager/docker_auth.go b/container-engine-lib/lib/backend_impls/docker/docker_manager/docker_auth.go new file mode 100644 index 0000000000..13098d3bda --- /dev/null +++ b/container-engine-lib/lib/backend_impls/docker/docker_manager/docker_auth.go @@ -0,0 +1,122 @@ +package docker_manager + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/docker/docker/api/types/registry" + dockerregistry "github.com/docker/docker/registry" +) + +// RegistryAuthConfig holds authentication configuration for a container registry +type RegistryAuthConfig struct { + Auths map[string]registry.AuthConfig `json:"auths"` + CredHelpers map[string]string `json:"credHelpers"` + CredsStore string `json:"credsStore"` +} + +// loadDockerAuth loads the authentication configuration from the config.json file located in $DOCKER_CONFIG or ~/.docker +func loadDockerAuth() (RegistryAuthConfig, error) { + configFilePath := os.Getenv("DOCKER_CONFIG") + if configFilePath == "" { + configFilePath = os.Getenv("HOME") + "/.docker/config.json" + } else { + configFilePath = configFilePath + "/config.json" + } + + file, err := os.ReadFile(configFilePath) + if err != nil { + return RegistryAuthConfig{}, fmt.Errorf("error reading Docker config file: %v", err) + } + + var authConfig RegistryAuthConfig + if err := json.Unmarshal(file, &authConfig); err != nil { + return RegistryAuthConfig{}, fmt.Errorf("error unmarshalling Docker config file: %v", err) + } + + return authConfig, nil +} + +// getCredentialsFromStore fetches credentials from a Docker credential helper (credStore) +func getCredentialsFromStore(credHelper string, registryURL string) (*registry.AuthConfig, error) { + // Prepare the helper command (docker-credential-) + credHelperCmd := "docker-credential-" + credHelper + + // Execute the credential helper to get credentials for the registry + cmd := exec.Command(credHelperCmd, "get") + cmd.Stdin = strings.NewReader(registryURL) + + var out bytes.Buffer + cmd.Stdout = &out + var stderr bytes.Buffer + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("error executing credential helper %s: %v, %s", credHelperCmd, err, stderr.String()) + } + + // Parse the output (it should return JSON containing "Username", "Secret" and "ServerURL") + creds := struct { + Username string `json:"Username"` + Secret string `json:"Secret"` + ServerURL string `json:"ServerURL"` + }{} + + if err := json.Unmarshal(out.Bytes(), &creds); err != nil { + return nil, fmt.Errorf("error parsing credentials from store: %v", err) + } + + return ®istry.AuthConfig{ + Username: creds.Username, + Password: creds.Secret, + ServerAddress: creds.ServerURL, + Auth: base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", creds.Username, creds.Secret))), + Email: "", + IdentityToken: "", + RegistryToken: "", + }, nil +} + +// GetAuthFromDockerConfig retrieves the auth configuration for a given repository +// by checking the Docker config.json file and Docker credential helpers. +// Returns nil if no credentials were found. +func GetAuthFromDockerConfig(repo string) (*registry.AuthConfig, error) { + authConfig, err := loadDockerAuth() + if err != nil { + return nil, err + } + + registryHost := dockerregistry.ConvertToHostname(repo) + + if !strings.Contains(registryHost, ".") || registryHost == "docker.io" || registryHost == "registry-1.docker.io" { + registryHost = "https://index.docker.io/v1/" + } + + // Check if the URL contains "://", meaning it already has a protocol + if !strings.Contains(registryHost, "://") { + registryHost = "https://" + registryHost + } + + // 1. Check if there is a credHelper for this specific registry + if credHelper, exists := authConfig.CredHelpers[registryHost]; exists { + return getCredentialsFromStore(credHelper, registryHost) + } + + // 2. Check if there is a default credStore for all registries + if authConfig.CredsStore != "" { + return getCredentialsFromStore(authConfig.CredsStore, registryHost) + } + + // 3. Fallback to credentials in "auths" if no credStore is available + if auth, exists := authConfig.Auths[registryHost]; exists { + return &auth, nil + } + + // Return no AuthConfig if no credentials were found + return nil, nil +} diff --git a/container-engine-lib/lib/backend_impls/docker/docker_manager/docker_auth_test.go b/container-engine-lib/lib/backend_impls/docker/docker_manager/docker_auth_test.go new file mode 100644 index 0000000000..ae800a7bb0 --- /dev/null +++ b/container-engine-lib/lib/backend_impls/docker/docker_manager/docker_auth_test.go @@ -0,0 +1,97 @@ +package docker_manager + +import ( + "encoding/base64" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +// WriteStaticConfig writes a static Docker config.json file to a temporary directory +func WriteStaticConfig(t *testing.T, configContent string) string { + tmpDir, err := os.MkdirTemp("", "docker-config") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + + configPath := tmpDir + "/config.json" + err = os.WriteFile(configPath, []byte(configContent), 0600) + if err != nil { + t.Fatalf("Failed to write config.json: %v", err) + } + + // Set the DOCKER_CONFIG environment variable to the temp directory + os.Setenv("DOCKER_CONFIG", tmpDir) + return tmpDir +} + +func TestGetAuthConfigForRepoPlain(t *testing.T) { + expectedUser := "user" + expectedPassword := "password" + + encodedAuth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", expectedUser, expectedPassword))) + + cfg := fmt.Sprintf(` + { + "auths": { + "https://index.docker.io/v1/": { + "auth": "%s" + } + } + }`, encodedAuth) + + tmpDir := WriteStaticConfig(t, cfg) + defer os.RemoveAll(tmpDir) + + // Test 1: Retrieve auth config for Docker Hub using docker.io domain + authConfig, err := GetAuthFromDockerConfig("docker.io/my-repo/my-image:latest") + assert.NoError(t, err) + assert.Equal(t, encodedAuth, authConfig.Auth, "Auth for Docker Hub should match") + + // Test 2: Retrieve auth config for Docker Hub using no domain + authConfig, err = GetAuthFromDockerConfig("my-repo/my-image:latest") + assert.NoError(t, err) + assert.Equal(t, encodedAuth, authConfig.Auth, "Auth for Docker Hub should match when using no host prefix") + + // Test 3: Retrieve auth config for Docker Hub using full domain and https:// prefix + authConfig, err = GetAuthFromDockerConfig("https://registry-1.docker.io/my-repo/my-image:latest") + assert.NoError(t, err) + assert.Equal(t, encodedAuth, authConfig.Auth, "Auth for Docker Hub should match when using no host prefix") + +} + +func TestGetAuthConfigForRepoOSX(t *testing.T) { + t.Skip("Skipping test that requires macOS keychain") + + cfg := `{ + "auths": { + "https://index.docker.io/v1/": {} + }, + "credsStore": "osxkeychain" + }` + tmpDir := WriteStaticConfig(t, cfg) + defer os.RemoveAll(tmpDir) + + authConfig, err := GetAuthFromDockerConfig("my-repo/my-image:latest") + assert.NoError(t, err) + assert.NotNil(t, authConfig, "Auth config should not be nil") +} + +func TestGetAuthConfigForRepoUnix(t *testing.T) { + t.Skip("Skipping test that requires unix `pass` password manager") + + cfg := `{ + "auths": { + "https://index.docker.io/v1/": {} + }, + "credsStore": "pass" + }` + tmpDir := WriteStaticConfig(t, cfg) + defer os.RemoveAll(tmpDir) + + authConfig, err := GetAuthFromDockerConfig("my-repo/my-image:latest") + assert.NoError(t, err) + assert.NotNil(t, authConfig, "Auth config should not be nil") +} diff --git a/container-engine-lib/lib/backend_impls/docker/docker_manager/docker_manager.go b/container-engine-lib/lib/backend_impls/docker/docker_manager/docker_manager.go index bd4e771c04..cad0cbc6d9 100644 --- a/container-engine-lib/lib/backend_impls/docker/docker_manager/docker_manager.go +++ b/container-engine-lib/lib/backend_impls/docker/docker_manager/docker_manager.go @@ -2279,6 +2279,23 @@ func pullImage(dockerClient *client.Client, imageName string, registrySpec *imag PrivilegeFunc: nil, Platform: platform, } + + // Try to obtain the auth configuration from the docker config file + authConfig, err := GetAuthFromDockerConfig(imageName) + if err != nil { + logrus.Errorf("An error occurred while getting auth config for image: %s: %s", imageName, err.Error()) + } + + if authConfig != nil { + authFromConfig, err := registry.EncodeAuthConfig(*authConfig) + if err != nil { + logrus.Errorf("An error occurred while encoding auth config for image: %s: %s", imageName, err.Error()) + } else { + imagePullOptions.RegistryAuth = authFromConfig + } + } + + // If the registry spec is defined, use that for authentication if registrySpec != nil { authConfig := registry.AuthConfig{ Username: registrySpec.GetUsername(),