Skip to content

Commit cec3f2c

Browse files
committed
Merge remote-tracking branch 'origin/main' into cj/385-push-image-fail
2 parents c53b7ce + c4b082e commit cec3f2c

File tree

10 files changed

+390
-88
lines changed

10 files changed

+390
-88
lines changed

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

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

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. |

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

@@ -719,6 +720,11 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro
719720
// Sanitize the environment of any opts!
720721
options.UnsetEnv()
721722

723+
// Remove the Docker config secret file!
724+
if err := cleanupDockerConfigOverride(); err != nil {
725+
return err
726+
}
727+
722728
// Set the environment from /etc/environment first, so it can be
723729
// overridden by the image and devcontainer settings.
724730
err = setEnvFromEtcEnvironment(opts.Logger)
@@ -778,11 +784,6 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro
778784
exportEnvFile.Close()
779785
}
780786

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

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

989-
cleanupDockerConfigJSON, err := initDockerConfigJSON(opts.Logger, workingDir, opts.DockerConfigBase64)
990+
cleanupDockerConfigOverride, err := initDockerConfigOverride(opts.Filesystem, opts.Logger, workingDir, opts.DockerConfigBase64)
990991
if err != nil {
991992
return nil, err
992993
}
993994
defer func() {
994-
if err := cleanupDockerConfigJSON(); err != nil {
995-
opts.Logger(log.LevelError, "failed to cleanup docker config JSON: %w", err)
995+
if err := cleanupDockerConfigOverride(); err != nil {
996+
opts.Logger(log.LevelError, "failed to cleanup docker config override: %w", err)
996997
}
997998
}() // best effort
998999

@@ -1323,7 +1324,7 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error)
13231324
options.UnsetEnv()
13241325

13251326
// Remove the Docker config secret file!
1326-
if err := cleanupDockerConfigJSON(); err != nil {
1327+
if err := cleanupDockerConfigOverride(); err != nil {
13271328
return nil, err
13281329
}
13291330

@@ -1575,8 +1576,22 @@ func maybeDeleteFilesystem(logger log.Func, force bool) error {
15751576
}
15761577

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

15821597
func copyFile(fs billy.Filesystem, src, dst string, mode fs.FileMode) error {
@@ -1603,6 +1618,21 @@ func copyFile(fs billy.Filesystem, src, dst string, mode fs.FileMode) error {
16031618
return nil
16041619
}
16051620

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

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

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

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ require (
1616
github.com/chainguard-dev/git-urls v1.0.2
1717
github.com/coder/coder/v2 v2.10.1-0.20240704130443-c2d44d16a352
1818
github.com/coder/retry v1.5.1
19-
github.com/coder/serpent v0.7.0
19+
github.com/coder/serpent v0.8.0
2020
github.com/containerd/platforms v0.2.1
2121
github.com/distribution/distribution/v3 v3.0.0-alpha.1
2222
github.com/docker/cli v27.2.1+incompatible

0 commit comments

Comments
 (0)