Skip to content

Commit c4b082e

Browse files
authored
fix: search $DOCKER_CONFIG if no base64 config is provided (#398)
1 parent aa73795 commit c4b082e

File tree

8 files changed

+363
-85
lines changed

8 files changed

+363
-85
lines changed

Diff for: Makefile

+4-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ develop:
2929
build: scripts/envbuilder-$(GOARCH)
3030
./scripts/build.sh
3131

32+
.PHONY: gen
33+
gen: docs/env-variables.md update-golden-files
34+
3235
.PHONY: update-golden-files
3336
update-golden-files: .gen-golden
3437

@@ -85,4 +88,4 @@ test-images-pull:
8588
docker push localhost:5000/envbuilder-test-ubuntu:latest
8689

8790
.registry-cache/docker/registry/v2/repositories/envbuilder-test-codercom-code-server:
88-
docker push localhost:5000/envbuilder-test-codercom-code-server:latest
91+
docker push localhost:5000/envbuilder-test-codercom-code-server:latest

Diff for: docs/container-registry-auth.md

+12-2
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,19 @@ After you have a configuration that resembles the following:
1414
}
1515
```
1616

17-
`base64` encode the JSON and provide it to envbuilder as the `ENVBUILDER_DOCKER_CONFIG_BASE64` environment variable.
17+
`base64` encode the JSON and provide it to envbuilder as the
18+
`ENVBUILDER_DOCKER_CONFIG_BASE64` environment variable.
1819

19-
Alternatively, if running `envbuilder` in Kubernetes, you can create an `ImagePullSecret` and
20+
Alternatively, the configuration file can be placed in `/.envbuilder/config.json`.
21+
The `DOCKER_CONFIG` environment variable can be used to define a custom path. The
22+
path must either be the path to a directory containing `config.json` or the full
23+
path to the JSON file itself.
24+
25+
> [!NOTE] Providing the docker configuration through other means than the
26+
> `ENVBUILDER_DOCKER_CONFIG_BASE64` environment variable will leave the
27+
> configuration file in the container filesystem. This may be a security risk.
28+
29+
When running `envbuilder` in Kubernetes, you can create an `ImagePullSecret` and
2030
pass it into the pod as a volume mount. This example will work for all registries.
2131

2232
```shell

Diff for: docs/env-variables.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
| `--dockerfile-path` | `ENVBUILDER_DOCKERFILE_PATH` | | The relative path to the Dockerfile that will be used to build the workspace. This is an alternative to using a devcontainer that some might find simpler. |
1616
| `--build-context-path` | `ENVBUILDER_BUILD_CONTEXT_PATH` | | Can be specified when a DockerfilePath is specified outside the base WorkspaceFolder. This path MUST be relative to the WorkspaceFolder path into which the repo is cloned. |
1717
| `--cache-ttl-days` | `ENVBUILDER_CACHE_TTL_DAYS` | | The number of days to use cached layers before expiring them. Defaults to 7 days. |
18-
| `--docker-config-base64` | `ENVBUILDER_DOCKER_CONFIG_BASE64` | | The base64 encoded Docker config file that will be used to pull images from private container registries. |
18+
| `--docker-config-base64` | `ENVBUILDER_DOCKER_CONFIG_BASE64` | | The base64 encoded Docker config file that will be used to pull images from private container registries. When this is set, Docker configuration set via the DOCKER_CONFIG environment variable is ignored. |
1919
| `--fallback-image` | `ENVBUILDER_FALLBACK_IMAGE` | | Specifies an alternative image to use when neither an image is declared in the devcontainer.json file nor a Dockerfile is present. If there's a build failure (from a faulty Dockerfile) or a misconfiguration, this image will be the substitute. Set ExitOnBuildFailure to true to halt the container if the build faces an issue. |
2020
| `--exit-on-build-failure` | `ENVBUILDER_EXIT_ON_BUILD_FAILURE` | | Terminates the container upon a build failure. This is handy when preferring the FALLBACK_IMAGE in cases where no devcontainer.json or image is provided. However, it ensures that the container stops if the build process encounters an error. |
2121
| `--force-safe` | `ENVBUILDER_FORCE_SAFE` | | Ignores any filesystem safety checks. This could cause serious harm to your system! This is used in cases where bypass is needed to unblock customers. |

Diff for: envbuilder.go

+187-51
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
"github.com/distribution/distribution/v3/configuration"
4242
"github.com/distribution/distribution/v3/registry/handlers"
4343
_ "github.com/distribution/distribution/v3/registry/storage/driver/filesystem"
44+
dockerconfig "github.com/docker/cli/cli/config"
4445
"github.com/docker/cli/cli/config/configfile"
4546
"github.com/fatih/color"
4647
v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -56,7 +57,7 @@ import (
5657
var ErrNoFallbackImage = errors.New("no fallback image has been specified")
5758

5859
// DockerConfig represents the Docker configuration file.
59-
type DockerConfig configfile.ConfigFile
60+
type DockerConfig = configfile.ConfigFile
6061

6162
type runtimeDataStore struct {
6263
// Runtime data.
@@ -154,13 +155,13 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro
154155

155156
opts.Logger(log.LevelInfo, "%s %s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder"), buildinfo.Version())
156157

157-
cleanupDockerConfigJSON, err := initDockerConfigJSON(opts.Logger, workingDir, opts.DockerConfigBase64)
158+
cleanupDockerConfigOverride, err := initDockerConfigOverride(opts.Filesystem, opts.Logger, workingDir, opts.DockerConfigBase64)
158159
if err != nil {
159160
return err
160161
}
161162
defer func() {
162-
if err := cleanupDockerConfigJSON(); err != nil {
163-
opts.Logger(log.LevelError, "failed to cleanup docker config JSON: %w", err)
163+
if err := cleanupDockerConfigOverride(); err != nil {
164+
opts.Logger(log.LevelError, "failed to cleanup docker config override: %w", err)
164165
}
165166
}() // best effort
166167

@@ -717,6 +718,11 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro
717718
// Sanitize the environment of any opts!
718719
options.UnsetEnv()
719720

721+
// Remove the Docker config secret file!
722+
if err := cleanupDockerConfigOverride(); err != nil {
723+
return err
724+
}
725+
720726
// Set the environment from /etc/environment first, so it can be
721727
// overridden by the image and devcontainer settings.
722728
err = setEnvFromEtcEnvironment(opts.Logger)
@@ -776,11 +782,6 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro
776782
exportEnvFile.Close()
777783
}
778784

779-
// Remove the Docker config secret file!
780-
if err := cleanupDockerConfigJSON(); err != nil {
781-
return err
782-
}
783-
784785
if runtimeData.ContainerUser == "" {
785786
opts.Logger(log.LevelWarn, "#%d: no user specified, using root", stageNumber)
786787
}
@@ -984,13 +985,13 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error)
984985

985986
opts.Logger(log.LevelInfo, "%s %s - Build development environments from repositories in a container", newColor(color.Bold).Sprintf("envbuilder"), buildinfo.Version())
986987

987-
cleanupDockerConfigJSON, err := initDockerConfigJSON(opts.Logger, workingDir, opts.DockerConfigBase64)
988+
cleanupDockerConfigOverride, err := initDockerConfigOverride(opts.Filesystem, opts.Logger, workingDir, opts.DockerConfigBase64)
988989
if err != nil {
989990
return nil, err
990991
}
991992
defer func() {
992-
if err := cleanupDockerConfigJSON(); err != nil {
993-
opts.Logger(log.LevelError, "failed to cleanup docker config JSON: %w", err)
993+
if err := cleanupDockerConfigOverride(); err != nil {
994+
opts.Logger(log.LevelError, "failed to cleanup docker config override: %w", err)
994995
}
995996
}() // best effort
996997

@@ -1321,7 +1322,7 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error)
13211322
options.UnsetEnv()
13221323

13231324
// Remove the Docker config secret file!
1324-
if err := cleanupDockerConfigJSON(); err != nil {
1325+
if err := cleanupDockerConfigOverride(); err != nil {
13251326
return nil, err
13261327
}
13271328

@@ -1573,8 +1574,22 @@ func maybeDeleteFilesystem(logger log.Func, force bool) error {
15731574
}
15741575

15751576
func fileExists(fs billy.Filesystem, path string) bool {
1576-
_, err := fs.Stat(path)
1577-
return err == nil
1577+
fi, err := fs.Stat(path)
1578+
return err == nil && !fi.IsDir()
1579+
}
1580+
1581+
func readFile(fs billy.Filesystem, name string) ([]byte, error) {
1582+
f, err := fs.Open(name)
1583+
if err != nil {
1584+
return nil, fmt.Errorf("open file: %w", err)
1585+
}
1586+
defer f.Close()
1587+
1588+
b, err := io.ReadAll(f)
1589+
if err != nil {
1590+
return nil, fmt.Errorf("read file: %w", err)
1591+
}
1592+
return b, nil
15781593
}
15791594

15801595
func copyFile(fs billy.Filesystem, src, dst string, mode fs.FileMode) error {
@@ -1601,6 +1616,21 @@ func copyFile(fs billy.Filesystem, src, dst string, mode fs.FileMode) error {
16011616
return nil
16021617
}
16031618

1619+
func writeFile(fs billy.Filesystem, name string, data []byte, perm fs.FileMode) error {
1620+
f, err := fs.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm)
1621+
if err != nil {
1622+
return fmt.Errorf("create file: %w", err)
1623+
}
1624+
_, err = f.Write(data)
1625+
if err != nil {
1626+
err = fmt.Errorf("write file: %w", err)
1627+
}
1628+
if err2 := f.Close(); err2 != nil && err == nil {
1629+
err = fmt.Errorf("close file: %w", err2)
1630+
}
1631+
return err
1632+
}
1633+
16041634
func writeMagicImageFile(fs billy.Filesystem, path string, v any) error {
16051635
file, err := fs.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
16061636
if err != nil {
@@ -1633,55 +1663,161 @@ func parseMagicImageFile(fs billy.Filesystem, path string, v any) error {
16331663
return nil
16341664
}
16351665

1636-
func initDockerConfigJSON(logf log.Func, workingDir workingdir.WorkingDir, dockerConfigBase64 string) (func() error, error) {
1637-
var cleanupOnce sync.Once
1638-
noop := func() error { return nil }
1639-
if dockerConfigBase64 == "" {
1640-
return noop, nil
1666+
const (
1667+
dockerConfigFile = dockerconfig.ConfigFileName
1668+
dockerConfigEnvKey = dockerconfig.EnvOverrideConfigDir
1669+
)
1670+
1671+
// initDockerConfigOverride sets the DOCKER_CONFIG environment variable
1672+
// to a path within the working directory. If a base64 encoded Docker
1673+
// config is provided, it is written to the path/config.json and the
1674+
// DOCKER_CONFIG environment variable is set to the path. If no base64
1675+
// encoded Docker config is provided, the following paths are checked in
1676+
// order:
1677+
//
1678+
// 1. $DOCKER_CONFIG/config.json
1679+
// 2. $DOCKER_CONFIG
1680+
// 3. /.envbuilder/config.json
1681+
//
1682+
// If a Docker config file is found, its path is set as DOCKER_CONFIG.
1683+
func initDockerConfigOverride(bfs billy.Filesystem, logf log.Func, workingDir workingdir.WorkingDir, dockerConfigBase64 string) (func() error, error) {
1684+
// If dockerConfigBase64 is set, it will have priority over file
1685+
// detection.
1686+
var dockerConfigJSON []byte
1687+
var err error
1688+
if dockerConfigBase64 != "" {
1689+
logf(log.LevelInfo, "Using base64 encoded Docker config")
1690+
1691+
dockerConfigJSON, err = base64.StdEncoding.DecodeString(dockerConfigBase64)
1692+
if err != nil {
1693+
return nil, fmt.Errorf("decode docker config: %w", err)
1694+
}
1695+
}
1696+
1697+
oldDockerConfig := os.Getenv(dockerConfigEnvKey)
1698+
var oldDockerConfigFile string
1699+
if oldDockerConfig != "" {
1700+
oldDockerConfigFile = filepath.Join(oldDockerConfig, dockerConfigFile)
1701+
}
1702+
for _, path := range []string{
1703+
oldDockerConfigFile, // $DOCKER_CONFIG/config.json
1704+
oldDockerConfig, // $DOCKER_CONFIG
1705+
workingDir.Join(dockerConfigFile), // /.envbuilder/config.json
1706+
} {
1707+
if path == "" || !fileExists(bfs, path) {
1708+
continue
1709+
}
1710+
1711+
logf(log.LevelWarn, "Found Docker config at %s, this file will remain after the build", path)
1712+
1713+
if dockerConfigJSON == nil {
1714+
logf(log.LevelInfo, "Using Docker config at %s", path)
1715+
1716+
dockerConfigJSON, err = readFile(bfs, path)
1717+
if err != nil {
1718+
return nil, fmt.Errorf("read docker config: %w", err)
1719+
}
1720+
} else {
1721+
logf(log.LevelWarn, "Ignoring Docker config at %s, using base64 encoded Docker config instead", path)
1722+
}
1723+
break
1724+
}
1725+
1726+
if dockerConfigJSON == nil {
1727+
// No user-provided config available.
1728+
return func() error { return nil }, nil
1729+
}
1730+
1731+
dockerConfigJSON, err = hujson.Standardize(dockerConfigJSON)
1732+
if err != nil {
1733+
return nil, fmt.Errorf("humanize json for docker config: %w", err)
16411734
}
1642-
cfgPath := workingDir.Join("config.json")
1643-
decoded, err := base64.StdEncoding.DecodeString(dockerConfigBase64)
1735+
1736+
if err = logDockerAuthConfigs(logf, dockerConfigJSON); err != nil {
1737+
return nil, fmt.Errorf("log docker auth configs: %w", err)
1738+
}
1739+
1740+
// We're going to set the DOCKER_CONFIG environment variable to a
1741+
// path within the working directory so that Kaniko can pick it up.
1742+
// A user should not mount a file directly to this path as we will
1743+
// write to the file.
1744+
newDockerConfig := workingDir.Join(".docker")
1745+
newDockerConfigFile := filepath.Join(newDockerConfig, dockerConfigFile)
1746+
err = bfs.MkdirAll(newDockerConfig, 0o700)
1747+
if err != nil {
1748+
return nil, fmt.Errorf("create docker config dir: %w", err)
1749+
}
1750+
1751+
if fileExists(bfs, newDockerConfigFile) {
1752+
return nil, fmt.Errorf("unable to write Docker config file, file already exists: %s", newDockerConfigFile)
1753+
}
1754+
1755+
restoreEnv, err := setAndRestoreEnv(logf, dockerConfigEnvKey, newDockerConfig)
16441756
if err != nil {
1645-
return noop, fmt.Errorf("decode docker config: %w", err)
1757+
return nil, fmt.Errorf("set docker config override: %w", err)
16461758
}
1647-
var configFile DockerConfig
1648-
decoded, err = hujson.Standardize(decoded)
1759+
1760+
err = writeFile(bfs, newDockerConfigFile, dockerConfigJSON, 0o600)
16491761
if err != nil {
1650-
return noop, fmt.Errorf("humanize json for docker config: %w", err)
1762+
_ = restoreEnv() // Best effort.
1763+
return nil, fmt.Errorf("write docker config: %w", err)
16511764
}
1652-
err = json.Unmarshal(decoded, &configFile)
1765+
logf(log.LevelInfo, "Wrote Docker config JSON to %s", newDockerConfigFile)
1766+
1767+
cleanupFile := onceErrFunc(func() error {
1768+
// Remove the Docker config secret file!
1769+
if err := bfs.Remove(newDockerConfigFile); err != nil {
1770+
logf(log.LevelError, "Failed to remove the Docker config secret file: %s", err)
1771+
return fmt.Errorf("remove docker config: %w", err)
1772+
}
1773+
return nil
1774+
})
1775+
return func() error { return errors.Join(cleanupFile(), restoreEnv()) }, nil
1776+
}
1777+
1778+
func logDockerAuthConfigs(logf log.Func, dockerConfigJSON []byte) error {
1779+
dc := new(DockerConfig)
1780+
err := dc.LoadFromReader(bytes.NewReader(dockerConfigJSON))
16531781
if err != nil {
1654-
return noop, fmt.Errorf("parse docker config: %w", err)
1782+
return fmt.Errorf("load docker config: %w", err)
16551783
}
1656-
for k := range configFile.AuthConfigs {
1784+
for k := range dc.AuthConfigs {
16571785
logf(log.LevelInfo, "Docker config contains auth for registry %q", k)
16581786
}
1659-
err = os.WriteFile(cfgPath, decoded, 0o644)
1787+
return nil
1788+
}
1789+
1790+
func setAndRestoreEnv(logf log.Func, key, value string) (restore func() error, err error) {
1791+
old := os.Getenv(key)
1792+
err = os.Setenv(key, value)
16601793
if err != nil {
1661-
return noop, fmt.Errorf("write docker config: %w", err)
1662-
}
1663-
logf(log.LevelInfo, "Wrote Docker config JSON to %s", cfgPath)
1664-
oldDockerConfig := os.Getenv("DOCKER_CONFIG")
1665-
_ = os.Setenv("DOCKER_CONFIG", workingDir.Path())
1666-
newDockerConfig := os.Getenv("DOCKER_CONFIG")
1667-
logf(log.LevelInfo, "Set DOCKER_CONFIG to %s", newDockerConfig)
1668-
cleanup := func() error {
1669-
var cleanupErr error
1670-
cleanupOnce.Do(func() {
1671-
// Restore the old DOCKER_CONFIG value.
1672-
os.Setenv("DOCKER_CONFIG", oldDockerConfig)
1673-
logf(log.LevelInfo, "Restored DOCKER_CONFIG to %s", oldDockerConfig)
1674-
// Remove the Docker config secret file!
1675-
if cleanupErr = os.Remove(cfgPath); err != nil {
1676-
if !errors.Is(err, fs.ErrNotExist) {
1677-
cleanupErr = fmt.Errorf("remove docker config: %w", cleanupErr)
1678-
}
1679-
logf(log.LevelError, "Failed to remove the Docker config secret file: %s", cleanupErr)
1794+
logf(log.LevelError, "Failed to set %s: %s", key, err)
1795+
return nil, fmt.Errorf("set %s: %w", key, err)
1796+
}
1797+
logf(log.LevelInfo, "Set %s to %s", key, value)
1798+
return onceErrFunc(func() error {
1799+
if err := func() error {
1800+
if old == "" {
1801+
return os.Unsetenv(key)
16801802
}
1803+
return os.Setenv(key, old)
1804+
}(); err != nil {
1805+
return fmt.Errorf("restore %s: %w", key, err)
1806+
}
1807+
logf(log.LevelInfo, "Restored %s to %s", key, old)
1808+
return nil
1809+
}), nil
1810+
}
1811+
1812+
func onceErrFunc(f func() error) func() error {
1813+
var once sync.Once
1814+
return func() error {
1815+
var err error
1816+
once.Do(func() {
1817+
err = f()
16811818
})
1682-
return cleanupErr
1819+
return err
16831820
}
1684-
return cleanup, err
16851821
}
16861822

16871823
// Allows quick testing of layer caching using a local directory!

0 commit comments

Comments
 (0)