diff --git a/.circleci/config.yml b/.circleci/config.yml index 792840db5b..9ff8fbf35c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -765,9 +765,9 @@ jobs: false fi - run: - name: "Verify Kurtosis cleaned up all its volumes (except for the log storage and github auth storage volumes)" + name: "Verify Kurtosis cleaned up all its volumes (except for the log storage, github auth storage, docker auth config volumes)" command: | - if ! [ $(docker volume ls | grep -v kurtosis-logs-collector-vol | grep -v kurtosis-logs-db-vol | tail -n+2 | wc -l ) -eq 2 ]; then + if ! [ $(docker volume ls | grep -v kurtosis-logs-collector-vol | grep -v kurtosis-logs-db-vol | tail -n+2 | wc -l ) -eq 3 ]; then docker volume ls false fi @@ -1004,9 +1004,9 @@ jobs: false fi - run: - name: "Verify Kurtosis cleaned up all its volumes (except for the log storage and github auth storage volumes)" + name: "Verify Kurtosis cleaned up all its volumes (except for the log storage, github auth storage, docker auth config volumes)" command: | - if ! [ $(docker volume ls | grep -v kurtosis-logs-collector-vol | grep -v kurtosis-logs-db-vol | tail -n+2 | wc -l) -eq 2 ]; then + if ! [ $(docker volume ls | grep -v kurtosis-logs-collector-vol | grep -v kurtosis-logs-db-vol | tail -n+2 | wc -l) -eq 3 ]; then docker volume ls false fi diff --git a/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/backend_creator/backend_creator.go b/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/backend_creator/backend_creator.go index 86d787d599..3a473b1c0d 100644 --- a/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/backend_creator/backend_creator.go +++ b/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/backend_creator/backend_creator.go @@ -3,11 +3,12 @@ package backend_creator import ( "context" "fmt" - "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/database_accessors/enclave_db" "net" "os" "path" + "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/database_accessors/enclave_db" + "github.com/docker/docker/client" "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend" "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/logs_collector_functions" diff --git a/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/consts/consts.go b/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/consts/consts.go index a13632c54e..8bf4bbefae 100644 --- a/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/consts/consts.go +++ b/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/consts/consts.go @@ -32,7 +32,8 @@ const ( NameOfNetworkToStartEngineAndLogServiceContainersIn = "bridge" HttpApplicationProtocol = "http" - GitHubAuthStorageDirPath = "/kurtosis-data/github-auth/" + GitHubAuthStorageDirPath = "/kurtosis-data/github-auth/" + DockerConfigStorageDirPath = "/root/.docker/" EmptyApplicationURL = "" ) diff --git a/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/docker_kurtosis_backend.go b/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/docker_kurtosis_backend.go index d663de40a9..d2af3c2e81 100644 --- a/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/docker_kurtosis_backend.go +++ b/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/docker_kurtosis_backend.go @@ -618,3 +618,22 @@ func (backend *DockerKurtosisBackend) getGitHubAuthStorageVolume(ctx context.Con volume := foundVolumes[0] return volume.Name, nil } + +// Guaranteed to either return a Docker config storage volume name or throw an error +func (backend *DockerKurtosisBackend) getDockerConfigStorageVolume(ctx context.Context) (string, error) { + volumeSearchLabels := map[string]string{ + docker_label_key.VolumeTypeDockerLabelKey.GetString(): label_value_consts.DockerConfigStorageVolumeTypeDockerLabelValue.GetString(), + } + foundVolumes, err := backend.dockerManager.GetVolumesByLabels(ctx, volumeSearchLabels) + if err != nil { + return "", stacktrace.Propagate(err, "An error occurred getting Docker config storage volumes matching labels '%+v'", volumeSearchLabels) + } + if len(foundVolumes) > 1 { + return "", stacktrace.NewError("Found multiple Docker config storage volumes. This should never happen") + } + if len(foundVolumes) == 0 { + return "", stacktrace.NewError("No Docker config storage volume found.") + } + volume := foundVolumes[0] + return volume.Name, nil +} diff --git a/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/docker_kurtosis_backend_api_container_functions.go b/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/docker_kurtosis_backend_api_container_functions.go index a99b180b01..757b79f4ca 100644 --- a/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/docker_kurtosis_backend_api_container_functions.go +++ b/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/docker_kurtosis_backend_api_container_functions.go @@ -3,10 +3,11 @@ package docker_kurtosis_backend import ( "context" "encoding/json" - "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/image_registry_spec" "net" "time" + "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_interface/objects/image_registry_spec" + "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_impls/docker/object_attributes_provider/docker_label_key" "github.com/docker/go-connections/nat" @@ -81,6 +82,11 @@ func (backend *DockerKurtosisBackend) CreateAPIContainer( return nil, stacktrace.Propagate(err, "An error occurred getting the GitHub auth storage volume name.") } + dockerConfigStorageVolumeName, err := backend.getDockerConfigStorageVolume(ctx) + if err != nil { + return nil, stacktrace.Propagate(err, "An error occurred getting the Docker config storage volume name.") + } + // Get the Docker network ID where we'll start the new API container enclaveNetwork, err := backend.getEnclaveNetworkByEnclaveUuid(ctx, enclaveUuid) if err != nil { @@ -191,8 +197,9 @@ func (backend *DockerKurtosisBackend) CreateAPIContainer( } volumeMounts := map[string]string{ - enclaveDataVolumeName: enclaveDataVolumeDirpath, - githubAuthStorageVolumeName: consts.GitHubAuthStorageDirPath, + enclaveDataVolumeName: enclaveDataVolumeDirpath, + githubAuthStorageVolumeName: consts.GitHubAuthStorageDirPath, + dockerConfigStorageVolumeName: consts.DockerConfigStorageDirPath, } labelStrs := map[string]string{} diff --git a/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/engine_functions/create_engine.go b/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/engine_functions/create_engine.go index 4b46974fbc..3096451b91 100644 --- a/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/engine_functions/create_engine.go +++ b/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/engine_functions/create_engine.go @@ -3,9 +3,11 @@ package engine_functions import ( "context" "fmt" - "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/engine_functions/github_auth_storage_creator" "time" + "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/engine_functions/docker_config_storage_creator" + "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/engine_functions/github_auth_storage_creator" + "github.com/docker/go-connections/nat" "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/consts" "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/logs_aggregator_functions" @@ -249,14 +251,34 @@ func CreateEngine( return nil, stacktrace.Propagate(err, "An error occurred creating GitHub auth storage.") } + // Configure Docker Config by writing the provided config files to a volume that's accessible by the engine + dockerConfigStorageVolObjAttrs, err := objAttrsProvider.ForDockerConfigStorageVolume() + if err != nil { + return nil, stacktrace.Propagate(err, "An error occurred retrieving object attributes for GitHub auth storage.") + } + dockerConfigStorageVolNameStr := dockerConfigStorageVolObjAttrs.GetName().GetString() + dockerConfigStorageVolLabelStrs := map[string]string{} + for labelKey, labelValue := range dockerConfigStorageVolObjAttrs.GetLabels() { + dockerConfigStorageVolLabelStrs[labelKey.GetString()] = labelValue.GetString() + } + // This volume is created idempotently (like logs storage volume) and just write the token to the file everytime the engine starts + if err = dockerManager.CreateVolume(ctx, dockerConfigStorageVolNameStr, dockerConfigStorageVolLabelStrs); err != nil { + return nil, stacktrace.Propagate(err, "An error occurred creating Docker config storage volume.") + } + err = docker_config_storage_creator.CreateDockerConfigStorage(ctx, targetNetworkId, dockerConfigStorageVolNameStr, consts.DockerConfigStorageDirPath, dockerManager) + if err != nil { + return nil, stacktrace.Propagate(err, "An error occurred creating Docker config storage.") + } + bindMounts := map[string]string{ // Necessary so that the engine server can interact with the Docker engine consts.DockerSocketFilepath: consts.DockerSocketFilepath, } volumeMounts := map[string]string{ - logsStorageVolNameStr: logsStorageDirPath, - githubAuthStorageVolNameStr: consts.GitHubAuthStorageDirPath, + logsStorageVolNameStr: logsStorageDirPath, + githubAuthStorageVolNameStr: consts.GitHubAuthStorageDirPath, + dockerConfigStorageVolNameStr: consts.DockerConfigStorageDirPath, } if serverArgs.OnBastionHost { diff --git a/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/engine_functions/docker_config_storage_creator/docker_config_storage_creator.go b/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/engine_functions/docker_config_storage_creator/docker_config_storage_creator.go new file mode 100644 index 0000000000..47a4ffd962 --- /dev/null +++ b/container-engine-lib/lib/backend_impls/docker/docker_kurtosis_backend/engine_functions/docker_config_storage_creator/docker_config_storage_creator.go @@ -0,0 +1,175 @@ +package docker_config_storage_creator + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "time" + + "github.com/docker/docker/api/types/registry" + "github.com/kurtosis-tech/kurtosis/container-engine-lib/lib/backend_impls/docker/docker_manager" + "github.com/kurtosis-tech/stacktrace" + "github.com/sirupsen/logrus" +) + +const ( + // We use this image and version because we already are using this in other projects so there is a high probability + // that the image is in the local machine's cache + creatorContainerImage = "alpine:3.17" + creatorContainerName = "kurtosis-docker-config-storage-creator" + + shBinaryFilepath = "/bin/sh" + shCmdFlag = "-c" + printfCmdName = "printf" + + creationSuccessExitCode = 0 + + creationCmdMaxRetries = 2 + creationCmdDelayInRetries = 200 * time.Millisecond + + configFilePath = "config.json" + + sleepSeconds = 1800 +) + +func CreateDockerConfigStorage( + ctx context.Context, + targetNetworkId string, + volumeName string, + storageDirPath string, + dockerManager *docker_manager.DockerManager, +) error { + entrypointArgs := []string{ + shBinaryFilepath, + shCmdFlag, + fmt.Sprintf("sleep %v", sleepSeconds), + } + + volumeMounts := map[string]string{ + volumeName: storageDirPath, + } + + createAndStartArgs := docker_manager.NewCreateAndStartContainerArgsBuilder( + creatorContainerImage, + creatorContainerName, + targetNetworkId, + ).WithEntrypointArgs( + entrypointArgs, + ).WithVolumeMounts( + volumeMounts, + ).Build() + + containerId, _, err := dockerManager.CreateAndStartContainer(ctx, createAndStartArgs) + if err != nil { + return stacktrace.Propagate(err, "An error occurred starting the Docker Config Storage Creator container with these args '%+v'", createAndStartArgs) + } + //The killing step has to be executed always in the success and also in the failed case + defer func() { + if err = dockerManager.RemoveContainer(context.Background(), containerId); err != nil { + logrus.Errorf( + "Launching the Docker Config Creator container with container ID '%v' didn't complete successfully so we "+ + "tried to remove the container we started, but doing so exited with an error:\n%v", + containerId, + err) + logrus.Errorf("ACTION REQUIRED: You'll need to manually remove the container with ID '%v'!!!!!!", containerId) + } + }() + + if err := storeConfigInVolume( + ctx, + dockerManager, + containerId, + creationCmdMaxRetries, + creationCmdDelayInRetries, + storageDirPath, + ); err != nil { + return stacktrace.Propagate(err, "An error occurred creating Docker config storage in volume.") + } + + return nil +} + +func storeConfigInVolume( + ctx context.Context, + dockerManager *docker_manager.DockerManager, + containerId string, + maxRetries uint, + timeBetweenRetries time.Duration, + storageDirPath string, +) error { + // Get all the registries from the Docker config + registries, err := docker_manager.GetAllRegistriesFromDockerConfig() + if err != nil { + return stacktrace.NewError("An error occurred getting all registries from Docker config: %v", err) + } + + cfg := struct { + Auths map[string]registry.AuthConfig `json:"auths"` + }{ + Auths: make(map[string]registry.AuthConfig), + } + + // Add the auths for each registry + for _, registry := range registries { + creds, err := docker_manager.GetAuthFromDockerConfig(registry) + if err != nil { + return stacktrace.NewError("An error occurred getting auth for registry '%v' from Docker config: %v", registry, err) + } + cfg.Auths[registry] = *creds + } + + cfgJsonStr, err := json.Marshal(cfg) + if err != nil { + return stacktrace.NewError("An error occurred marshalling the Docker config into JSON: %v", err) + } + + // Write the config.json to the volume + commandStr := fmt.Sprintf( + "%v '%v' > %v", + printfCmdName, + string(cfgJsonStr), + fmt.Sprintf("%s/%s", storageDirPath, configFilePath), + ) + + execCmd := []string{ + shBinaryFilepath, + shCmdFlag, + commandStr, + } + for i := uint(0); i < maxRetries; i++ { + outputBuffer := &bytes.Buffer{} + exitCode, err := dockerManager.RunExecCommand(ctx, containerId, execCmd, outputBuffer) + if err == nil { + if exitCode == creationSuccessExitCode { + logrus.Debugf("The Docker config file was successfully added into the volume.") + return nil + } + logrus.Debugf( + "Docker config storage creation command '%v' returned without a Docker error, but exited with non-%v exit code '%v' and logs:\n%v", + commandStr, + creationSuccessExitCode, + exitCode, + outputBuffer.String(), + ) + } else { + logrus.Debugf( + "Docker config storage creation command '%v' experienced a Docker error:\n%v", + commandStr, + err, + ) + } + + // Tiny optimization to not sleep if we're not going to run the loop again + if i < maxRetries { + time.Sleep(timeBetweenRetries) + } + } + + return stacktrace.NewError( + "The Docker config storage creation didn't return success (as measured by the command '%v') even after retrying %v times with %v between retries", + commandStr, + maxRetries, + timeBetweenRetries, + ) +} 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..e956126d92 --- /dev/null +++ b/container-engine-lib/lib/backend_impls/docker/docker_manager/docker_auth.go @@ -0,0 +1,201 @@ +package docker_manager + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "github.com/sirupsen/logrus" + "os" + "os/exec" + "strings" + + "github.com/docker/docker/api/types/registry" + dockerregistry "github.com/docker/docker/registry" + "github.com/kurtosis-tech/stacktrace" +) + +const ( + ENV_DOCKER_CONFIG string = "DOCKER_CONFIG" +) + +// 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(ENV_DOCKER_CONFIG) + if configFilePath == "" { + configFilePath = os.Getenv("HOME") + "/.docker/config.json" + } else { + configFilePath = configFilePath + "/config.json" + } + + file, err := os.ReadFile(configFilePath) + if errors.Is(err, os.ErrNotExist) { + // If the auth config doesn't exist, return an empty auth config + logrus.Debugf("No docker config found at '%s'. Returning empty registry auth config.", configFilePath) + return emptyRegistryAuthConfig(), nil + } else if err != nil { + return emptyRegistryAuthConfig(), stacktrace.Propagate(err, "error reading Docker config file at '%s'", configFilePath) + } + + var authConfig RegistryAuthConfig + if err := json.Unmarshal(file, &authConfig); err != nil { + return emptyRegistryAuthConfig(), stacktrace.Propagate(err, "error unmarshalling Docker config file at '%s'", configFilePath) + } + + return authConfig, nil +} + +func emptyRegistryAuthConfig() RegistryAuthConfig { + return RegistryAuthConfig{ + Auths: map[string]registry.AuthConfig{}, + CredHelpers: map[string]string{}, + CredsStore: "", + } +} + +// getRegistriesFromCredsStore fetches all registries from a Docker credential helper (credStore) +func getRegistriesFromCredsStore(credHelper string) ([]string, error) { + credHelperCmd := "docker-credential-" + credHelper + + cmd := exec.Command(credHelperCmd, "list") + + var out bytes.Buffer + cmd.Stdout = &out + var stderr bytes.Buffer + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + return nil, stacktrace.Propagate(err, "error executing credential helper '%s': %s", cmd.String(), stderr.String()) + } + // Output will look like this: {"https://index.docker.io/v1/":"username"} + var result map[string]string + outStr := out.String() + err := json.Unmarshal([]byte(outStr), &result) + if err != nil { + return nil, stacktrace.Propagate(err, "error unmarshaling credential helper list output '%s': %s", cmd.String(), outStr) + } + + registries := []string{} + for k := range result { + registries = append(registries, k) + } + return registries, 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, stacktrace.Propagate(err, "error executing credential helper '%s' for '%s': %s", cmd.String(), registryURL, 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"` + }{ + Username: "", + Secret: "", + ServerURL: "", + } + + if err := json.Unmarshal(out.Bytes(), &creds); err != nil { + return nil, stacktrace.Propagate(err, "error parsing credentials from store") + } + + 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 +} + +// GetAllRegistriesFromDockerConfig retrieves all registries from the Docker config.json file +func GetAllRegistriesFromDockerConfig() ([]string, error) { + authConfig, err := loadDockerAuth() + if err != nil { + return nil, err + } + + var registries []string + for registry := range authConfig.Auths { + registries = append(registries, registry) + } + + for registry := range authConfig.CredHelpers { + registries = append(registries, registry) + } + + if authConfig.CredsStore != "" { + r, err := getRegistriesFromCredsStore(authConfig.CredsStore) + if err != nil { + return nil, err + } + registries = append(registries, r...) + } + + return registries, 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..1f328911e2 --- /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(ENV_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 b76e691028..cff367eb32 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(), @@ -2296,6 +2313,7 @@ func pullImage(dockerClient *client.Client, imageName string, registrySpec *imag imagePullOptions.RegistryAuth = encodedAuthConfig } + out, err := dockerClient.ImagePull(pullImageCtx, imageName, imagePullOptions) if err != nil { return stacktrace.Propagate(err, "Tried pulling image '%v' with platform '%v' but failed", imageName, platform), false diff --git a/container-engine-lib/lib/backend_impls/docker/object_attributes_provider/label_value_consts/label_value_consts.go b/container-engine-lib/lib/backend_impls/docker/object_attributes_provider/label_value_consts/label_value_consts.go index c30e9d1a46..9e48d3ccdb 100644 --- a/container-engine-lib/lib/backend_impls/docker/object_attributes_provider/label_value_consts/label_value_consts.go +++ b/container-engine-lib/lib/backend_impls/docker/object_attributes_provider/label_value_consts/label_value_consts.go @@ -28,6 +28,7 @@ const ( logsStorageVolumeTypeLabelValueStr = "kurtosis-logs-storage" logsCollectorVolumeTypeLabelValueStr = "logs-collector-data" githubAuthStorageVolumeTypeLabelValueStr = "github-auth-storage" + dockerConfigStorageVolumeTypeLabelValueStr = "docker-config-storage" ) // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! DO NOT CHANGE THESE VALUES !!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -54,3 +55,4 @@ var PersistentDirectoryVolumeTypeDockerLabelValue = docker_label_value.MustCreat var LogsStorageVolumeTypeDockerLabelValue = docker_label_value.MustCreateNewDockerLabelValue(logsStorageVolumeTypeLabelValueStr) var LogsCollectorVolumeTypeDockerLabelValue = docker_label_value.MustCreateNewDockerLabelValue(logsCollectorVolumeTypeLabelValueStr) var GitHubAuthStorageVolumeTypeDockerLabelValue = docker_label_value.MustCreateNewDockerLabelValue(githubAuthStorageVolumeTypeLabelValueStr) +var DockerConfigStorageVolumeTypeDockerLabelValue = docker_label_value.MustCreateNewDockerLabelValue(dockerConfigStorageVolumeTypeLabelValueStr) diff --git a/container-engine-lib/lib/backend_impls/docker/object_attributes_provider/object_attributes_provider.go b/container-engine-lib/lib/backend_impls/docker/object_attributes_provider/object_attributes_provider.go index 5c9af7fada..20da73b2be 100644 --- a/container-engine-lib/lib/backend_impls/docker/object_attributes_provider/object_attributes_provider.go +++ b/container-engine-lib/lib/backend_impls/docker/object_attributes_provider/object_attributes_provider.go @@ -17,12 +17,13 @@ import ( ) const ( - engineServerNamePrefix = "kurtosis-engine" - logsAggregatorName = "kurtosis-logs-aggregator" - logsStorageVolumeName = "kurtosis-logs-storage" - githubAuthStorageVolumeName = "kurtosis-github-auth-storage" - engineRESTAPIPortStr = "engine-rest-api" - reverseProxyNamePrefix = "kurtosis-reverse-proxy" + engineServerNamePrefix = "kurtosis-engine" + logsAggregatorName = "kurtosis-logs-aggregator" + logsStorageVolumeName = "kurtosis-logs-storage" + githubAuthStorageVolumeName = "kurtosis-github-auth-storage" + dockerConfigStorageVolumeName = "kurtosis-docker-config-storage" + engineRESTAPIPortStr = "engine-rest-api" + reverseProxyNamePrefix = "kurtosis-reverse-proxy" ) type DockerObjectAttributesProvider interface { @@ -38,6 +39,7 @@ type DockerObjectAttributesProvider interface { ForLogsStorageVolume() (DockerObjectAttributes, error) ForReverseProxy(engineGuid engine.EngineGUID) (DockerObjectAttributes, error) ForGitHubAuthStorageVolume() (DockerObjectAttributes, error) + ForDockerConfigStorageVolume() (DockerObjectAttributes, error) } func GetDockerObjectAttributesProvider() DockerObjectAttributesProvider { @@ -172,6 +174,23 @@ func (provider *dockerObjectAttributesProviderImpl) ForGitHubAuthStorageVolume() return objectAttributes, nil } +func (provider *dockerObjectAttributesProviderImpl) ForDockerConfigStorageVolume() (DockerObjectAttributes, error) { + name, err := docker_object_name.CreateNewDockerObjectName(dockerConfigStorageVolumeName) + if err != nil { + return nil, stacktrace.Propagate(err, "An error occurred creating a Docker object name object from string '%v'", dockerConfigStorageVolumeName) + } + + labels := map[*docker_label_key.DockerLabelKey]*docker_label_value.DockerLabelValue{ + docker_label_key.VolumeTypeDockerLabelKey: label_value_consts.DockerConfigStorageVolumeTypeDockerLabelValue, + } + + objectAttributes, err := newDockerObjectAttributesImpl(name, labels) + if err != nil { + return nil, stacktrace.Propagate(err, "An error occurred while creating the ObjectAttributesImpl with the name '%s' and labels '%+v'", name, labels) + } + return objectAttributes, nil +} + func (provider *dockerObjectAttributesProviderImpl) ForReverseProxy(engineGuid engine.EngineGUID) (DockerObjectAttributes, error) { nameStr := strings.Join(