From e422d9b39bee3925435eaad31f1973be9d862149 Mon Sep 17 00:00:00 2001 From: mailaenderli Date: Tue, 4 Nov 2025 15:06:11 +0000 Subject: [PATCH 1/5] feat(docker-out): add configPath option --- features/src/docker-out/README.md | 4 +- .../src/docker-out/devcontainer-feature.json | 11 +++++- features/src/docker-out/install.sh | 1 + features/src/docker-out/installer.go | 39 ++++++++++++++++++- features/test/docker-out/install-config.sh | 9 +++++ features/test/docker-out/scenarios.json | 16 ++++++++ installer/overrides.go | 35 ++++++++++------- 7 files changed, 97 insertions(+), 18 deletions(-) create mode 100755 features/test/docker-out/install-config.sh diff --git a/features/src/docker-out/README.md b/features/src/docker-out/README.md index 76ca1bf..f6c7825 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 | | 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..f14faca 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,15 @@ "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" + ] + }, "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..d1ea084 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,37 @@ 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.ReadConfigFile(c.ConfigPath) + if err != nil { + return err + } + userName := os.Getenv("_REMOTE_USER") + dockerDir := fmt.Sprintf("/home/%s/.docker", userName) + configDest := fmt.Sprintf("%s/config.json", dockerDir) + // 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..67d7cdd 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 := ReadConfigFile(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 } +// ReadConfigFile loads a file from a URL or local path and returns its contents as bytes. +func ReadConfigFile(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 From b7eaae9d17fd9a44015efcb236651aa710bc2a28 Mon Sep 17 00:00:00 2001 From: mailaenderli Date: Wed, 5 Nov 2025 11:46:10 +0000 Subject: [PATCH 2/5] fix: handle none value in overrides --- features/src/docker-out/README.md | 2 +- features/src/docker-out/devcontainer-feature.json | 3 ++- installer/overrides.go | 3 +++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/features/src/docker-out/README.md b/features/src/docker-out/README.md index f6c7825..8eabf7f 100755 --- a/features/src/docker-out/README.md +++ b/features/src/docker-out/README.md @@ -26,7 +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 | +| 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 f14faca..98f85c0 100644 --- a/features/src/docker-out/devcontainer-feature.json +++ b/features/src/docker-out/devcontainer-feature.json @@ -42,7 +42,8 @@ "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" + "https://raw.githubusercontent.com/devcontainers/features/main/src/docker-out/config.json", + "none" ] }, "downloadUrl": { diff --git a/installer/overrides.go b/installer/overrides.go index 67d7cdd..d0b1143 100644 --- a/installer/overrides.go +++ b/installer/overrides.go @@ -66,6 +66,9 @@ func HandleOverride(passedValue *string, defaultValue string, key string) { // Otherwise set to default value *passedValue = defaultValue } + if strings.ToLower(*passedValue) == "none" { + *passedValue = "" + } } func HandleGitHubOverride(downloadUrl *string, gitHubPath string, key string) { From 64d133531a1cda97db743fdfbcf4c18f9b9abde4 Mon Sep 17 00:00:00 2001 From: mailaenderli Date: Wed, 5 Nov 2025 13:07:06 +0000 Subject: [PATCH 3/5] chore: mr feedback - filepath.Join instead of sprintf - use _REMOTE_USER_HOME - better name for ReadConfigFile -> ReadFileFromUrlOrLocal --- features/src/docker-out/installer.go | 10 +++++++--- installer/overrides.go | 6 +++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/features/src/docker-out/installer.go b/features/src/docker-out/installer.go index d1ea084..66faafc 100644 --- a/features/src/docker-out/installer.go +++ b/features/src/docker-out/installer.go @@ -144,13 +144,17 @@ func (c *dockerCliComponent) InstallVersion(version *gover.Version) error { } // Copy the default config.json if c.ConfigPath != "" { - fileContent, err := installer.ReadConfigFile(c.ConfigPath) + fileContent, err := installer.ReadFileFromUrlOrLocal(c.ConfigPath) if err != nil { return err } userName := os.Getenv("_REMOTE_USER") - dockerDir := fmt.Sprintf("/home/%s/.docker", userName) - configDest := fmt.Sprintf("%s/config.json", dockerDir) + 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 diff --git a/installer/overrides.go b/installer/overrides.go index d0b1143..dc4fb12 100644 --- a/installer/overrides.go +++ b/installer/overrides.go @@ -9,7 +9,7 @@ import ( // Loads the feature overrides from a specified location. func LoadOverrides() error { if overrideLocation := os.Getenv("DEV_FEATURE_OVERRIDE_LOCATION"); overrideLocation != "" { - fileContent, err := ReadConfigFile(overrideLocation) + fileContent, err := ReadFileFromUrlOrLocal(overrideLocation) if err != nil { return err } @@ -37,8 +37,8 @@ func LoadOverrides() error { return nil } -// ReadConfigFile loads a file from a URL or local path and returns its contents as bytes. -func ReadConfigFile(location string) ([]byte, error) { +// 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 { From 8847e6b3261d4d366e5521ced5252191d2d587c0 Mon Sep 17 00:00:00 2001 From: mailaenderli Date: Tue, 11 Nov 2025 11:28:14 +0000 Subject: [PATCH 4/5] chore: add parameter to override file --- override-all.env | 1 + 1 file changed, 1 insertion(+) 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="" From 753fd2ab91debd32c77b36d1de329fc13126ddb2 Mon Sep 17 00:00:00 2001 From: mailaenderli Date: Tue, 11 Nov 2025 11:28:58 +0000 Subject: [PATCH 5/5] docs: describe how overides can be unset using none --- README.md | 23 +++++++++++++++++++++++ installer/overrides.go | 3 +++ 2 files changed, 26 insertions(+) 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/installer/overrides.go b/installer/overrides.go index dc4fb12..ed31574 100644 --- a/installer/overrides.go +++ b/installer/overrides.go @@ -66,6 +66,9 @@ 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 = "" }