diff --git a/README.md b/README.md index 565f927..e2ab4da 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,12 @@ As the remote can be configured differently (e.g. by including or excluding sub- See [override-all.env](./override-all.env) for a file with all possible override variables. +The precedence for the overrides is: + +1. Value set via feature parameter +2. Value set via environment variable +3. Values from `DEV_FEATURE_OVERRIDE_LOCATION` + #### Special overrides There are a few sources which are used in multiple installations. For those sources, there is an override that globaly overrides all installations from this sources. Here is the list of those sources and their keys. @@ -82,6 +88,23 @@ There are a few sources which are used in multiple installations. For those sour ``` DEV_FEATURE_OVERRIDE_GITHUB_DOWNLOAD_URL=... ``` +#### Unset an Override via Parameter + +If an override is set, setting the corresponding parameter to `""` will not unset the override. To achieve this, set the parameter to `none`. + +**Example:** +This environment variable is set: `DOCKER_OUT_CONFIG_PATH=https://example.com/config.json` + +Then set this in your feature to explicitly unset it: + +```json +{ + "ghcr.io/postfinance/devcontainer-features/docker-out:0.3.0": { + "version": "28.3.3", + "configPath": "none" + } +} +``` ### Extend an existing feature diff --git a/features/src/docker-out/README.md b/features/src/docker-out/README.md index 76ca1bf..8eabf7f 100755 --- a/features/src/docker-out/README.md +++ b/features/src/docker-out/README.md @@ -6,10 +6,11 @@ Installs a Docker client which re-uses the host Docker socket. ```json "features": { - "ghcr.io/postfinance/devcontainer-features/docker-out:0.2.0": { + "ghcr.io/postfinance/devcontainer-features/docker-out:0.3.0": { "version": "latest", "composeVersion": "latest", "buildxVersion": "latest", + "configPath": "", "downloadUrl": "", "versionsUrl": "", "composeDownloadUrl": "", @@ -25,6 +26,7 @@ Installs a Docker client which re-uses the host Docker socket. | version | The version of the Docker CLI to install. | string | latest | latest, 28.3.3, 20.10 | | composeVersion | The version of the Compose plugin to install. | string | latest | latest, none, 2.39.1, 2.29 | | buildxVersion | The version of the buildx plugin to install. | string | latest | latest, none, 0.26.1, 0.10 | +| configPath | Path or URL to a custom Docker client config.json file to copy into the container. | string | <empty> | /home/user/.docker/config.json, https://raw.githubusercontent.com/devcontainers/features/main/src/docker-out/config.json, none | | downloadUrl | The download URL to use for Docker binaries. | string | <empty> | | | versionsUrl | The URL to use for checking available versions. | string | <empty> | | | composeDownloadUrl | The download URL to use for Docker Compose binaries. | string | <empty> | | diff --git a/features/src/docker-out/devcontainer-feature.json b/features/src/docker-out/devcontainer-feature.json index b2cd0c0..98f85c0 100644 --- a/features/src/docker-out/devcontainer-feature.json +++ b/features/src/docker-out/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "docker-out", - "version": "0.2.0", + "version": "0.3.0", "name": "Docker outside Docker", "description": "Installs a Docker client which re-uses the host Docker socket.", "options": { @@ -36,6 +36,16 @@ "default": "latest", "description": "The version of the buildx plugin to install." }, + "configPath": { + "type": "string", + "default": "", + "description": "Path or URL to a custom Docker client config.json file to copy into the container.", + "proposals": [ + "/home/user/.docker/config.json", + "https://raw.githubusercontent.com/devcontainers/features/main/src/docker-out/config.json", + "none" + ] + }, "downloadUrl": { "type": "string", "default": "", diff --git a/features/src/docker-out/install.sh b/features/src/docker-out/install.sh index 13eab5c..f0fa6c0 100755 --- a/features/src/docker-out/install.sh +++ b/features/src/docker-out/install.sh @@ -4,6 +4,7 @@ -version="${VERSION:-"latest"}" \ -composeVersion="${COMPOSEVERSION:-"latest"}" \ -buildxVersion="${BUILDXVERSION:-"latest"}" \ + -configPath="${CONFIGPATH:-""}" \ -downloadUrl="${DOWNLOADURL:-""}" \ -versionsUrl="${VERSIONSURL:-""}" \ -composeDownloadUrl="${COMPOSEDOWNLOADURL:-""}" \ diff --git a/features/src/docker-out/installer.go b/features/src/docker-out/installer.go index 0227328..66faafc 100644 --- a/features/src/docker-out/installer.go +++ b/features/src/docker-out/installer.go @@ -5,8 +5,10 @@ import ( "flag" "fmt" "os" + "os/user" "path/filepath" "regexp" + "strconv" "github.com/roemer/gotaskr/execr" "github.com/roemer/gover" @@ -36,10 +38,11 @@ func runMain() error { version := flag.String("version", "latest", "") composeVersion := flag.String("composeVersion", "latest", "") buildxVersion := flag.String("buildxVersion", "latest", "") + buildxDownloadUrl := flag.String("buildxDownloadUrl", "", "") downloadUrl := flag.String("downloadUrl", "", "") versionsUrl := flag.String("versionsUrl", "", "") composeDownloadUrl := flag.String("composeDownloadUrl", "", "") - buildxDownloadUrl := flag.String("buildxDownloadUrl", "", "") + configPath := flag.String("configPath", "", "") flag.Parse() // Load settings from an external file @@ -51,6 +54,7 @@ func runMain() error { installer.HandleOverride(versionsUrl, "https://download.docker.com/linux/static/stable", "docker-out-versions-url") installer.HandleGitHubOverride(composeDownloadUrl, "docker/compose", "docker-out-compose-download-url") installer.HandleGitHubOverride(buildxDownloadUrl, "docker/buildx", "docker-out-buildx-download-url") + installer.HandleOverride(configPath, "", "docker-out-config-path") // Create and process the feature feature := installer.NewFeature("Docker-Out", false, @@ -58,6 +62,7 @@ func runMain() error { ComponentBase: installer.NewComponentBase("Docker CLI", *version), DownloadUrl: *downloadUrl, VersionsUrl: *versionsUrl, + ConfigPath: *configPath, }, &dockerComposeComponent{ ComponentBase: installer.NewComponentBase("Docker Compose", *composeVersion), @@ -81,6 +86,7 @@ type dockerCliComponent struct { *installer.ComponentBase DownloadUrl string VersionsUrl string + ConfigPath string } func (c *dockerCliComponent) GetAllVersions() ([]*gover.Version, error) { @@ -136,6 +142,41 @@ func (c *dockerCliComponent) InstallVersion(version *gover.Version) error { if err := execr.Run(true, "cp", "-prf", "docker-init.sh", "/usr/local/share/docker-init.sh"); err != nil { return err } + // Copy the default config.json + if c.ConfigPath != "" { + fileContent, err := installer.ReadFileFromUrlOrLocal(c.ConfigPath) + if err != nil { + return err + } + userName := os.Getenv("_REMOTE_USER") + homeDir := os.Getenv("_REMOTE_USER_HOME") + if homeDir == "" { + homeDir = filepath.Join("/home", userName) + } + dockerDir := filepath.Join(homeDir, ".docker") + configDest := filepath.Join(dockerDir, "config.json") + // Ensure directory exists + if err := os.MkdirAll(dockerDir, 0700); err != nil { + return err + } + // Write config file + if err := os.WriteFile(configDest, fileContent, 0600); err != nil { + return err + } + // Set ownership + usr, err := user.Lookup(userName) + if err != nil { + return err + } + uid, _ := strconv.Atoi(usr.Uid) + gid, _ := strconv.Atoi(usr.Gid) + if err := os.Chown(dockerDir, uid, gid); err != nil { + return err + } + if err := os.Chown(configDest, uid, gid); err != nil { + return err + } + } return nil } diff --git a/features/test/docker-out/install-config.sh b/features/test/docker-out/install-config.sh new file mode 100755 index 0000000..dc54117 --- /dev/null +++ b/features/test/docker-out/install-config.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e + +[[ -f "$(dirname "$0")/../functions.sh" ]] && source "$(dirname "$0")/../functions.sh" +[[ -f "$(dirname "$0")/functions.sh" ]] && source "$(dirname "$0")/functions.sh" + +check_version "$(docker version -f '{{.Client.Version}}')" "28.3.3" +check_file_exists "/home/vscode/.docker/config.json" +cat /home/vscode/.docker/config.json | grep "######" >/dev/null 2>&1 || (echo "Custom Docker config.json has a wrong content!" && exit 1) diff --git a/features/test/docker-out/scenarios.json b/features/test/docker-out/scenarios.json index 2f75279..f68d1d5 100644 --- a/features/test/docker-out/scenarios.json +++ b/features/test/docker-out/scenarios.json @@ -28,5 +28,21 @@ "buildxVersion": "0.21.2" } } + }, + "install-config": { + "build": { + "dockerfile": "Dockerfile", + "options": [ + "--add-host=host.docker.internal:host-gateway" + ] + }, + "features": { + "./docker-out": { + "version": "28.3.3", + "composeVersion": "none", + "buildxVersion": "none", + "configPath": "https://raw.githubusercontent.com/postfinance/devcontainer-features/refs/heads/main/override-all.env" + } + } } } \ No newline at end of file diff --git a/installer/overrides.go b/installer/overrides.go index 8667537..ed31574 100644 --- a/installer/overrides.go +++ b/installer/overrides.go @@ -8,22 +8,10 @@ import ( // Loads the feature overrides from a specified location. func LoadOverrides() error { - var fileContent []byte - var err error if overrideLocation := os.Getenv("DEV_FEATURE_OVERRIDE_LOCATION"); overrideLocation != "" { - // Load the overrides from the specified location - if strings.HasPrefix(overrideLocation, "http://") || strings.HasPrefix(overrideLocation, "https://") { - // Load from URL - fileContent, err = Tools.Download.AsBytes(overrideLocation) - if err != nil { - return fmt.Errorf("error downloading override file: %v", err) - } - } else { - // Load from file - fileContent, err = os.ReadFile(overrideLocation) - if err != nil { - return fmt.Errorf("error reading override file: %v", err) - } + fileContent, err := ReadFileFromUrlOrLocal(overrideLocation) + if err != nil { + return err } if len(fileContent) > 0 { lines := strings.SplitSeq(strings.ReplaceAll(strings.TrimSpace(string(fileContent)), "\r\n", "\n"), "\n") @@ -49,6 +37,23 @@ func LoadOverrides() error { return nil } +// ReadFileFromUrlOrLocal loads a file from a URL or local path and returns its contents as bytes. +func ReadFileFromUrlOrLocal(location string) ([]byte, error) { + if strings.HasPrefix(location, "http://") || strings.HasPrefix(location, "https://") { + fileContent, err := Tools.Download.AsBytes(location) + if err != nil { + return nil, fmt.Errorf("error downloading file: %v", err) + } + return fileContent, nil + } else { + fileContent, err := os.ReadFile(location) + if err != nil { + return nil, fmt.Errorf("error reading file: %v", err) + } + return fileContent, nil + } +} + func HandleOverride(passedValue *string, defaultValue string, key string) { if *passedValue == "" { // Convert the key to a compatible format @@ -61,6 +66,12 @@ func HandleOverride(passedValue *string, defaultValue string, key string) { // Otherwise set to default value *passedValue = defaultValue } + // Handle "none" value to explicitly unset the value if an override env variable is set to a different value + // e.g. --configPath="none" and DOCKER_OUT_CONFIG_PATH=https://example.com/config.json + // If we do not explicitly handle this, you could not unset a value once an override env variable is set + if strings.ToLower(*passedValue) == "none" { + *passedValue = "" + } } func HandleGitHubOverride(downloadUrl *string, gitHubPath string, key string) { diff --git a/override-all.env b/override-all.env index ff76a3e..f529a22 100644 --- a/override-all.env +++ b/override-all.env @@ -17,6 +17,7 @@ DOCKER_OUT_DOWNLOAD_URL="" DOCKER_OUT_VERSIONS_URL="" DOCKER_OUT_COMPOSE_DOWNLOAD_URL="" DOCKER_OUT_BUILDX_DOWNLOAD_URL="" +DOCKER_OUT_CONFIG_PATH="" # git-lfs GIT_LFS_DOWNLOAD_URL=""