From 248c1ef443dcda27fe5526c81ae399c8639b4fe2 Mon Sep 17 00:00:00 2001 From: Roemer Date: Tue, 28 Jul 2026 10:38:26 +0200 Subject: [PATCH] feat: Add dind --- .github/workflows/ci.yml | 1 + README.md | 1 + build/build.go | 4 + features/src/docker-in/README.md | 39 ++++ .../src/docker-in/devcontainer-feature.json | 91 +++++++++ features/src/docker-in/dind-init.sh.tmpl | 76 ++++++++ features/src/docker-in/install.sh | 11 ++ features/src/docker-in/installer.go | 182 ++++++++++++++++++ features/src/docker-out/installer.go | 116 +---------- features/test/docker-in/install-exact.sh | 9 + features/test/docker-in/install.sh | 7 + features/test/docker-in/scenarios.json | 26 +++ features/test/docker-in/test-images.json | 6 + shared/docker/buildx.go | 45 +++++ shared/docker/compose.go | 45 +++++ shared/docker/config.go | 47 +++++ 16 files changed, 595 insertions(+), 111 deletions(-) create mode 100755 features/src/docker-in/README.md create mode 100644 features/src/docker-in/devcontainer-feature.json create mode 100755 features/src/docker-in/dind-init.sh.tmpl create mode 100755 features/src/docker-in/install.sh create mode 100644 features/src/docker-in/installer.go create mode 100755 features/test/docker-in/install-exact.sh create mode 100755 features/test/docker-in/install.sh create mode 100644 features/test/docker-in/scenarios.json create mode 100644 features/test/docker-in/test-images.json create mode 100644 shared/docker/buildx.go create mode 100644 shared/docker/compose.go create mode 100644 shared/docker/config.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53f005d..d9010c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,6 +83,7 @@ jobs: "build-essential", "claude-code", "cypress-deps", + "docker-in", "docker-out", "dotnet", "eclipse-deps", diff --git a/README.md b/README.md index 48b896b..13395f4 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Below is a list with included features, click on the link for more details. | [build-essential](./features/src/build-essential/README.md) | Installs build essentials like gcc. | | [claude-code](./features/src/claude-code/README.md) | Installs Claude Code, Anthropic's AI coding assistant CLI. | | [cypress-deps](./features/src/cypress-deps/README.md) | Installs all dependencies required to run Cypress. | +| [docker-in](./features/src/docker-in/README.md) | Installs and runs a full Docker daemon. | | [docker-out](./features/src/docker-out/README.md) | Installs a Docker client which re-uses the host Docker socket. | | [dotnet](./features/src/dotnet/README.md) | A package which installs .NET SDKs, runtimes and workloads. | | [eclipse-deps](./features/src/eclipse-deps/README.md) | Installs all dependencies required to run the Eclipse IDE. | diff --git a/build/build.go b/build/build.go index 1ead11a..92c7a20 100644 --- a/build/build.go +++ b/build/build.go @@ -102,6 +102,10 @@ func init() { gotaskr.Task("Feature:cypress-deps:Package", func() error { return packageFeature("cypress-deps") }) gotaskr.Task("Feature:cypress-deps:Test", func() error { return testFeature("cypress-deps") }) + ////////// docker-in + gotaskr.Task("Feature:docker-in:Package", func() error { return packageFeature("docker-in") }) + gotaskr.Task("Feature:docker-in:Test", func() error { return testFeature("docker-in") }) + ////////// docker-out gotaskr.Task("Feature:docker-out:Package", func() error { return packageFeature("docker-out") }) gotaskr.Task("Feature:docker-out:Test", func() error { return testFeature("docker-out") }) diff --git a/features/src/docker-in/README.md b/features/src/docker-in/README.md new file mode 100755 index 0000000..8f68072 --- /dev/null +++ b/features/src/docker-in/README.md @@ -0,0 +1,39 @@ +# Docker inside Docker (docker-in) + +Installs and runs a full Docker daemon. + +## Example Usage + +```json +"features": { + "ghcr.io/postfinance/devcontainer-features/docker-in:1.0.0": { + "version": "latest", + "composeVersion": "latest", + "buildxVersion": "latest", + "configPath": "", + "downloadUrl": "", + "versionsUrl": "", + "composeDownloadUrl": "", + "buildxDownloadUrl": "" + } +} +``` + +## Options + +| Option | Description | Type | Default Value | Proposals | +|-----|-----|-----|-----|-----| +| version | The version of the Docker Engine and 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-in/config.json, none | +| downloadUrl | The download URL to use for Docker binaries (engine, CLI, containerd). | 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> | | +| buildxDownloadUrl | The download URL to use for Docker Buildx binaries. | string | <empty> | | + +## Customizations + +### VS Code Extensions + +- `ms-azuretools.vscode-containers` diff --git a/features/src/docker-in/devcontainer-feature.json b/features/src/docker-in/devcontainer-feature.json new file mode 100644 index 0000000..334672f --- /dev/null +++ b/features/src/docker-in/devcontainer-feature.json @@ -0,0 +1,91 @@ +{ + "id": "docker-in", + "version": "1.0.0", + "name": "Docker inside Docker", + "description": "Installs and runs a full Docker daemon.", + "options": { + "version": { + "type": "string", + "proposals": [ + "latest", + "28.3.3", + "20.10" + ], + "default": "latest", + "description": "The version of the Docker Engine and CLI to install." + }, + "composeVersion": { + "type": "string", + "proposals": [ + "latest", + "none", + "2.39.1", + "2.29" + ], + "default": "latest", + "description": "The version of the Compose plugin to install." + }, + "buildxVersion": { + "type": "string", + "proposals": [ + "latest", + "none", + "0.26.1", + "0.10" + ], + "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-in/config.json", + "none" + ] + }, + "downloadUrl": { + "type": "string", + "default": "", + "description": "The download URL to use for Docker binaries (engine, CLI, containerd)." + }, + "versionsUrl": { + "type": "string", + "default": "", + "description": "The URL to use for checking available versions." + }, + "composeDownloadUrl": { + "type": "string", + "default": "", + "description": "The download URL to use for Docker Compose binaries." + }, + "buildxDownloadUrl": { + "type": "string", + "default": "", + "description": "The download URL to use for Docker Buildx binaries." + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-azuretools.vscode-containers" + ] + } + }, + "mounts": [ + { + "source": "dind-var-lib-docker-${devcontainerId}", + "target": "/var/lib/docker", + "type": "volume" + }, + { + "source": "dind-var-lib-containerd-${devcontainerId}", + "target": "/var/lib/containerd", + "type": "volume" + } + ], + "entrypoint": "/usr/local/share/dind-init.sh", + "privileged": true +} \ No newline at end of file diff --git a/features/src/docker-in/dind-init.sh.tmpl b/features/src/docker-in/dind-init.sh.tmpl new file mode 100755 index 0000000..08b5789 --- /dev/null +++ b/features/src/docker-in/dind-init.sh.tmpl @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +set -e + +# Wrapper function to only use sudo if not already root +sudoIf() { + if [ "$(id -u)" -ne 0 ]; then + sudo "$@" + else + "$@" + fi +} + +# Setup logging +LOG_DIR=/var/log/feat-docker-in +LOG_FILE=$LOG_DIR/entrypoint.log +sudoIf mkdir -p "$LOG_DIR" +sudoIf chmod 755 "$LOG_DIR" + +# Redirect all stdout/stderr to log +exec > >(sudoIf tee -a "$LOG_FILE") 2>&1 + +echo "=== Entrypoint Start $(date -Is) ===" +echo "[entrypoint] Running with args: $*" + +# Determine the correct user +USERNAME="{{ .User }}" +if [ "${USERNAME}" = "" ]; then + USERNAME=$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd) +fi + +echo "[entrypoint] Running for user: ${USERNAME}" + +# Ensure runtime directories exist +sudoIf mkdir -p /run/docker /var/lib/docker /var/lib/containerd +sudoIf rm -f /run/docker.pid + +# Start the Docker daemon +echo "[entrypoint] Starting dockerd..." +sudoIf dockerd \ + --host=unix:///var/run/docker.sock \ + --storage-driver=overlay2 \ + --log-level=debug & + +DOCKERD_PID=$! +echo "[entrypoint] dockerd pid $DOCKERD_PID" + +# Wait for daemon to become ready +TRIES=0 +until sudoIf docker info >/dev/null 2>&1 || [ $TRIES -ge 60 ]; do + sleep 1 + TRIES=$((TRIES+1)) + echo "[entrypoint] waiting for dockerd... ($TRIES)" +done + +# Verify the daemon is running +if sudoIf docker info >/dev/null 2>&1; then + echo "[entrypoint] Docker daemon is ready" +else + echo "[entrypoint] ERROR: Docker daemon never started!" + echo "[entrypoint] Check $LOG_FILE for details" + exit 1 +fi + +# Ensure group exists +getent group docker || sudoIf groupadd docker + +# Ensure user is in group +sudoIf usermod -aG docker dev || true + +# Finish up +echo "=== Entrypoint Complete $(date -Is) ===" + +# Execute whatever commands were passed in (if any). This allows us +# to set this script to ENTRYPOINT while still executing the default CMD. +set +e +exec "$@" diff --git a/features/src/docker-in/install.sh b/features/src/docker-in/install.sh new file mode 100755 index 0000000..f0fa6c0 --- /dev/null +++ b/features/src/docker-in/install.sh @@ -0,0 +1,11 @@ +. ./functions.sh + +"./installer_$(detect_arch)" \ + -version="${VERSION:-"latest"}" \ + -composeVersion="${COMPOSEVERSION:-"latest"}" \ + -buildxVersion="${BUILDXVERSION:-"latest"}" \ + -configPath="${CONFIGPATH:-""}" \ + -downloadUrl="${DOWNLOADURL:-""}" \ + -versionsUrl="${VERSIONSURL:-""}" \ + -composeDownloadUrl="${COMPOSEDOWNLOADURL:-""}" \ + -buildxDownloadUrl="${BUILDXDOWNLOADURL:-""}" diff --git a/features/src/docker-in/installer.go b/features/src/docker-in/installer.go new file mode 100644 index 0000000..80f2741 --- /dev/null +++ b/features/src/docker-in/installer.go @@ -0,0 +1,182 @@ +package main + +import ( + "builder/installer" + "builder/shared/docker" + "bytes" + "flag" + "fmt" + "os" + "path/filepath" + "regexp" + "text/template" + + "github.com/roemer/gover" +) + +////////// +// Configuration +////////// + +var dockerVersionRegexp *regexp.Regexp = regexp.MustCompile(`(?m)^(\d+)\.(\d+)\.(\d+)$`) + +////////// +// Main +////////// + +func main() { + if err := runMain(); err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } +} + +func runMain() error { + // Handle the flags + 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", "", "") + configPath := flag.String("configPath", "", "") + flag.Parse() + + // Load settings from an external file + if err := installer.LoadOverrides(); err != nil { + return err + } + + installer.HandleOverride(downloadUrl, "https://download.docker.com/linux/static/stable", "docker-in-download-url") + installer.HandleOverride(versionsUrl, "https://download.docker.com/linux/static/stable", "docker-in-versions-url") + installer.HandleGitHubOverride(composeDownloadUrl, "docker/compose", "docker-in-compose-download-url") + installer.HandleGitHubOverride(buildxDownloadUrl, "docker/buildx", "docker-in-buildx-download-url") + installer.HandleOverride(configPath, "", "docker-in-config-path") + + // Create and process the feature + feature := installer.NewFeature("Docker-In", false, + &dockerComponent{ + ComponentBase: installer.NewComponentBase("Docker", *version), + DownloadUrl: *downloadUrl, + VersionsUrl: *versionsUrl, + ConfigPath: *configPath, + }, + &docker.DockerComposeComponent{ + ComponentBase: installer.NewComponentBase("Docker Compose", *composeVersion), + DownloadUrl: *composeDownloadUrl, + }, + &docker.DockerBuildxComponent{ + ComponentBase: installer.NewComponentBase("Docker buildx", *buildxVersion), + DownloadUrl: *buildxDownloadUrl, + }, + ) + return feature.Process() +} + +////////// +// Implementation +////////// + +type dockerComponent struct { + *installer.ComponentBase + DownloadUrl string + VersionsUrl string + ConfigPath string +} + +func (c *dockerComponent) GetAllVersions() ([]*gover.Version, error) { + // Download the file + architecturePathPart, err := c.getArchitecturePathPart() + if err != nil { + return nil, err + } + url, err := installer.Tools.Http.BuildUrl(c.VersionsUrl, architecturePathPart) + if err != nil { + return nil, err + } + allVersions, err := installer.Tools.Http.GetVersionsFromHtmlIndex( + url, + regexp.MustCompile(`^.*.*$`), + dockerVersionRegexp) + if err != nil { + return nil, err + } + return allVersions, nil +} + +func (c *dockerComponent) InstallVersion(version *gover.Version) error { + // Install the system dependencies + // XZ for alpine might be just xz + if err := installer.Tools.System.InstallPackages("git", "procps", "iptables", "xz-utils"); err != nil { + return err + } + // Download the file + architecturePathPart, err := c.getArchitecturePathPart() + if err != nil { + return err + } + fileName := fmt.Sprintf("docker-%s.tgz", version.Raw) + downloadUrl, err := installer.Tools.Http.BuildUrl(c.DownloadUrl, architecturePathPart, fileName) + if err != nil { + return err + } + if err := installer.Tools.Download.ToFile(downloadUrl, fileName, "Docker"); err != nil { + return err + } + defer os.Remove(fileName) + // Extract it + tempDir, err := os.MkdirTemp("", "docker-extract") + if err != nil { + return err + } + defer os.RemoveAll(tempDir) + if err := installer.Tools.Compression.ExtractTarGz(fileName, tempDir, false); err != nil { + return err + } + // Install the binaries + dockerFiles, err := os.ReadDir(filepath.Join(tempDir, "docker")) + if err != nil { + return err + } + for _, file := range dockerFiles { + if file.IsDir() { + continue + } + srcPath := filepath.Join(tempDir, "docker", file.Name()) + if err := installer.Tools.System.InstallBinaryToUsrLocalBin(srcPath, file.Name()); err != nil { + return err + } + } + // Generate the startup script + t1, err := template.New("dind-init.sh.tmpl").ParseFiles("./dind-init.sh.tmpl") + if err != nil { + return err + } + data := struct { + User string + }{ + User: os.Getenv("_REMOTE_USER"), + } + var buf bytes.Buffer + if err := t1.Execute(&buf, data); err != nil { + return err + } + content := buf.String() + if err := os.WriteFile("/usr/local/share/dind-init.sh", []byte(content), os.ModePerm); err != nil { + return err + } + // Copy the default config.json + if err := docker.InstallClientConfig(c.ConfigPath); err != nil { + return err + } + + return nil +} + +func (c *dockerComponent) getArchitecturePathPart() (string, error) { + return installer.Tools.System.MapArchitecture(map[string]string{ + installer.AMD64: "x86_64", + installer.ARM64: "aarch64", + }) +} diff --git a/features/src/docker-out/installer.go b/features/src/docker-out/installer.go index 51618b0..7db0662 100644 --- a/features/src/docker-out/installer.go +++ b/features/src/docker-out/installer.go @@ -2,14 +2,13 @@ package main import ( "builder/installer" + "builder/shared/docker" "bytes" "flag" "fmt" "os" - "os/user" "path/filepath" "regexp" - "strconv" "text/template" "github.com/roemer/gover" @@ -20,8 +19,6 @@ import ( ////////// var dockerCliVersionRegexp *regexp.Regexp = regexp.MustCompile(`(?m)^(\d+)\.(\d+)\.(\d+)$`) -var dockerComposeVersionRegexp *regexp.Regexp = regexp.MustCompile(`(?m)^v(?P(\d+)\.(\d+)\.(\d+))$`) -var dockerBuildxVersionRegexp *regexp.Regexp = regexp.MustCompile(`(?m)^v(?P(\d+)\.(\d+)\.(\d+))$`) ////////// // Main @@ -65,11 +62,11 @@ func runMain() error { VersionsUrl: *versionsUrl, ConfigPath: *configPath, }, - &dockerComposeComponent{ + &docker.DockerComposeComponent{ ComponentBase: installer.NewComponentBase("Docker Compose", *composeVersion), DownloadUrl: *composeDownloadUrl, }, - &dockerBuildxComponent{ + &docker.DockerBuildxComponent{ ComponentBase: installer.NewComponentBase("Docker buildx", *buildxVersion), DownloadUrl: *buildxDownloadUrl, }, @@ -158,39 +155,8 @@ func (c *dockerCliComponent) InstallVersion(version *gover.Version) error { 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 - } + if err := docker.InstallClientConfig(c.ConfigPath); err != nil { + return err } return nil @@ -202,75 +168,3 @@ func (c *dockerCliComponent) getArchitecturePathPart() (string, error) { installer.ARM64: "aarch64", }) } - -// Docker Compose - -type dockerComposeComponent struct { - *installer.ComponentBase - DownloadUrl string -} - -func (c *dockerComposeComponent) GetAllVersions() ([]*gover.Version, error) { - allTags, err := installer.Tools.GitHub.GetTags("docker", "compose") - if err != nil { - return nil, err - } - return installer.Tools.Versioning.ParseVersionsFromList(allTags, dockerComposeVersionRegexp, true) -} - -func (c *dockerComposeComponent) InstallVersion(version *gover.Version) error { - // Download the file - archPart, err := installer.Tools.System.MapArchitecture(map[string]string{ - installer.AMD64: "x86_64", - installer.ARM64: "aarch64", - }) - if err != nil { - return err - } - // https://github.com/docker/compose/releases/download/v2.39.2/docker-compose-linux-x86_64 - versionPart := fmt.Sprintf("v%s", version.Raw) - downloadUrl, err := installer.Tools.Http.BuildUrl(c.DownloadUrl, versionPart, fmt.Sprintf("docker-compose-linux-%s", archPart)) - if err != nil { - return err - } - if err := installer.Tools.System.DownloadBinaryToDir(downloadUrl, "Docker Compose", "/usr/local/lib/docker/cli-plugins", "docker-compose"); err != nil { - return err - } - return nil -} - -// Docker buildx - -type dockerBuildxComponent struct { - *installer.ComponentBase - DownloadUrl string -} - -func (c *dockerBuildxComponent) GetAllVersions() ([]*gover.Version, error) { - allTags, err := installer.Tools.GitHub.GetTags("docker", "buildx") - if err != nil { - return nil, err - } - return installer.Tools.Versioning.ParseVersionsFromList(allTags, dockerBuildxVersionRegexp, true) -} - -func (c *dockerBuildxComponent) InstallVersion(version *gover.Version) error { - // Download the file - archPart, err := installer.Tools.System.MapArchitecture(map[string]string{ - installer.AMD64: "amd64", - installer.ARM64: "arm64", - }) - if err != nil { - return err - } - // https://github.com/docker/buildx/releases/download/v0.26.1/buildx-v0.26.1.linux-amd64 - versionPart := fmt.Sprintf("v%s", version.Raw) - downloadUrl, err := installer.Tools.Http.BuildUrl(c.DownloadUrl, versionPart, fmt.Sprintf("buildx-%s.linux-%s", versionPart, archPart)) - if err != nil { - return err - } - if err := installer.Tools.System.DownloadBinaryToDir(downloadUrl, "Docker buildx", "/usr/local/lib/docker/cli-plugins", "docker-buildx"); err != nil { - return err - } - return nil -} diff --git a/features/test/docker-in/install-exact.sh b/features/test/docker-in/install-exact.sh new file mode 100755 index 0000000..3ea6ab4 --- /dev/null +++ b/features/test/docker-in/install-exact.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_version "$(docker compose version)" "Docker Compose version v2.39.1" +check_version "$(docker buildx version)" "github.com/docker/buildx v0.21.2 1360a9e8d25a2c3d03c2776d53ae62e6ff0a843d" diff --git a/features/test/docker-in/install.sh b/features/test/docker-in/install.sh new file mode 100755 index 0000000..41389d2 --- /dev/null +++ b/features/test/docker-in/install.sh @@ -0,0 +1,7 @@ +#!/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}}')" "27.2.1" diff --git a/features/test/docker-in/scenarios.json b/features/test/docker-in/scenarios.json new file mode 100644 index 0000000..b5b2f34 --- /dev/null +++ b/features/test/docker-in/scenarios.json @@ -0,0 +1,26 @@ +{ + "install": { + "build": { + "dockerfile": "Dockerfile" + }, + "features": { + "./docker-in": { + "version": "27.2.1", + "composeVersion": "none", + "buildxVersion": "none" + } + } + }, + "install-exact": { + "build": { + "dockerfile": "Dockerfile" + }, + "features": { + "./docker-in": { + "version": "28.3.3", + "composeVersion": "2.39.1", + "buildxVersion": "0.21.2" + } + } + } +} diff --git a/features/test/docker-in/test-images.json b/features/test/docker-in/test-images.json new file mode 100644 index 0000000..7e091f0 --- /dev/null +++ b/features/test/docker-in/test-images.json @@ -0,0 +1,6 @@ +[ + "mcr.microsoft.com/devcontainers/base:debian12", + "mcr.microsoft.com/devcontainers/base:debian13", + "mcr.microsoft.com/devcontainers/base:ubuntu24.04", + "mcr.microsoft.com/devcontainers/base:ubuntu26.04" +] diff --git a/shared/docker/buildx.go b/shared/docker/buildx.go new file mode 100644 index 0000000..faf2cce --- /dev/null +++ b/shared/docker/buildx.go @@ -0,0 +1,45 @@ +package docker + +import ( + "builder/installer" + "fmt" + "regexp" + + "github.com/roemer/gover" +) + +var dockerBuildxVersionRegexp *regexp.Regexp = regexp.MustCompile(`(?m)^v(?P(\d+)\.(\d+)\.(\d+))$`) + +type DockerBuildxComponent struct { + *installer.ComponentBase + DownloadUrl string +} + +func (c *DockerBuildxComponent) GetAllVersions() ([]*gover.Version, error) { + allTags, err := installer.Tools.GitHub.GetTags("docker", "buildx") + if err != nil { + return nil, err + } + return installer.Tools.Versioning.ParseVersionsFromList(allTags, dockerBuildxVersionRegexp, true) +} + +func (c *DockerBuildxComponent) InstallVersion(version *gover.Version) error { + // Download the file + archPart, err := installer.Tools.System.MapArchitecture(map[string]string{ + installer.AMD64: "amd64", + installer.ARM64: "arm64", + }) + if err != nil { + return err + } + // https://github.com/docker/buildx/releases/download/v0.26.1/buildx-v0.26.1.linux-amd64 + versionPart := fmt.Sprintf("v%s", version.Raw) + downloadUrl, err := installer.Tools.Http.BuildUrl(c.DownloadUrl, versionPart, fmt.Sprintf("buildx-%s.linux-%s", versionPart, archPart)) + if err != nil { + return err + } + if err := installer.Tools.System.DownloadBinaryToDir(downloadUrl, "Docker buildx", "/usr/local/lib/docker/cli-plugins", "docker-buildx"); err != nil { + return err + } + return nil +} diff --git a/shared/docker/compose.go b/shared/docker/compose.go new file mode 100644 index 0000000..99cc30f --- /dev/null +++ b/shared/docker/compose.go @@ -0,0 +1,45 @@ +package docker + +import ( + "builder/installer" + "fmt" + "regexp" + + "github.com/roemer/gover" +) + +var dockerComposeVersionRegexp *regexp.Regexp = regexp.MustCompile(`(?m)^v(?P(\d+)\.(\d+)\.(\d+))$`) + +type DockerComposeComponent struct { + *installer.ComponentBase + DownloadUrl string +} + +func (c *DockerComposeComponent) GetAllVersions() ([]*gover.Version, error) { + allTags, err := installer.Tools.GitHub.GetTags("docker", "compose") + if err != nil { + return nil, err + } + return installer.Tools.Versioning.ParseVersionsFromList(allTags, dockerComposeVersionRegexp, true) +} + +func (c *DockerComposeComponent) InstallVersion(version *gover.Version) error { + // Download the file + archPart, err := installer.Tools.System.MapArchitecture(map[string]string{ + installer.AMD64: "x86_64", + installer.ARM64: "aarch64", + }) + if err != nil { + return err + } + // https://github.com/docker/compose/releases/download/v2.39.2/docker-compose-linux-x86_64 + versionPart := fmt.Sprintf("v%s", version.Raw) + downloadUrl, err := installer.Tools.Http.BuildUrl(c.DownloadUrl, versionPart, fmt.Sprintf("docker-compose-linux-%s", archPart)) + if err != nil { + return err + } + if err := installer.Tools.System.DownloadBinaryToDir(downloadUrl, "Docker Compose", "/usr/local/lib/docker/cli-plugins", "docker-compose"); err != nil { + return err + } + return nil +} diff --git a/shared/docker/config.go b/shared/docker/config.go new file mode 100644 index 0000000..80dec0b --- /dev/null +++ b/shared/docker/config.go @@ -0,0 +1,47 @@ +package docker + +import ( + "builder/installer" + "os" + "os/user" + "path/filepath" + "strconv" +) + +func InstallClientConfig(configPath string) error { + if configPath != "" { + fileContent, err := installer.ReadFileFromUrlOrLocal(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 +}