Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support auth from docker config #2560

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -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-<store>)
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 &registry.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
}
Original file line number Diff line number Diff line change
@@ -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")
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Loading