Skip to content
86 changes: 57 additions & 29 deletions envbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,13 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro

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

cleanupDockerConfigJSON, err := initDockerConfigJSON(opts.Logger, workingDir, opts.DockerConfigBase64)
cleanupDockerConfigOverride, err := initDockerConfigOverride(opts.Logger, workingDir, opts.DockerConfigBase64)
if err != nil {
return err
}
defer func() {
if err := cleanupDockerConfigJSON(); err != nil {
opts.Logger(log.LevelError, "failed to cleanup docker config JSON: %w", err)
if err := cleanupDockerConfigOverride(); err != nil {
opts.Logger(log.LevelError, "failed to cleanup docker config override: %w", err)
}
}() // best effort

Expand Down Expand Up @@ -771,7 +771,7 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro
}

// Remove the Docker config secret file!
if err := cleanupDockerConfigJSON(); err != nil {
if err := cleanupDockerConfigOverride(); err != nil {
return err
}

Expand Down Expand Up @@ -978,13 +978,13 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error)

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

cleanupDockerConfigJSON, err := initDockerConfigJSON(opts.Logger, workingDir, opts.DockerConfigBase64)
cleanupDockerConfigOverride, err := initDockerConfigOverride(opts.Logger, workingDir, opts.DockerConfigBase64)
if err != nil {
return nil, err
}
defer func() {
if err := cleanupDockerConfigJSON(); err != nil {
opts.Logger(log.LevelError, "failed to cleanup docker config JSON: %w", err)
if err := cleanupDockerConfigOverride(); err != nil {
opts.Logger(log.LevelError, "failed to cleanup docker config override: %w", err)
}
}() // best effort

Expand Down Expand Up @@ -1315,7 +1315,7 @@ func RunCacheProbe(ctx context.Context, opts options.Options) (v1.Image, error)
options.UnsetEnv()

// Remove the Docker config secret file!
if err := cleanupDockerConfigJSON(); err != nil {
if err := cleanupDockerConfigOverride(); err != nil {
return nil, err
}

Expand Down Expand Up @@ -1627,55 +1627,83 @@ func parseMagicImageFile(fs billy.Filesystem, path string, v any) error {
return nil
}

func initDockerConfigJSON(logf log.Func, workingDir workingdir.WorkingDir, dockerConfigBase64 string) (func() error, error) {
var cleanupOnce sync.Once
noop := func() error { return nil }
func initDockerConfigOverride(logf log.Func, workingDir workingdir.WorkingDir, dockerConfigBase64 string) (func() error, error) {
var (
oldDockerConfig = os.Getenv("DOCKER_CONFIG")
newDockerConfig = workingDir.Path()
cfgPath = workingDir.Join("config.json")
restoreEnv = func() error { return nil } // noop.
)
if dockerConfigBase64 != "" || oldDockerConfig == "" {
err := os.Setenv("DOCKER_CONFIG", newDockerConfig)
if err != nil {
logf(log.LevelError, "Failed to set DOCKER_CONFIG: %s", err)
return nil, fmt.Errorf("set DOCKER_CONFIG: %w", err)
}
logf(log.LevelInfo, "Set DOCKER_CONFIG to %s", newDockerConfig)

restoreEnv = func() error {
// Restore the old DOCKER_CONFIG value.
if oldDockerConfig == "" {
err := os.Unsetenv("DOCKER_CONFIG")
if err != nil {
err = fmt.Errorf("unset DOCKER_CONFIG: %w", err)
}
return err
}
err := os.Setenv("DOCKER_CONFIG", oldDockerConfig)
if err != nil {
return fmt.Errorf("restore DOCKER_CONFIG: %w", err)
}
logf(log.LevelInfo, "Restored DOCKER_CONFIG to %s", oldDockerConfig)
return nil
}
} else {
logf(log.LevelInfo, "Using existing DOCKER_CONFIG set to %s", oldDockerConfig)
}

if dockerConfigBase64 == "" {
return noop, nil
return restoreEnv, nil
}
cfgPath := workingDir.Join("config.json")

decoded, err := base64.StdEncoding.DecodeString(dockerConfigBase64)
if err != nil {
return noop, fmt.Errorf("decode docker config: %w", err)
return restoreEnv, fmt.Errorf("decode docker config: %w", err)
}
var configFile DockerConfig
decoded, err = hujson.Standardize(decoded)
if err != nil {
return noop, fmt.Errorf("humanize json for docker config: %w", err)
return restoreEnv, fmt.Errorf("humanize json for docker config: %w", err)
}
err = json.Unmarshal(decoded, &configFile)
if err != nil {
return noop, fmt.Errorf("parse docker config: %w", err)
return restoreEnv, fmt.Errorf("parse docker config: %w", err)
}
for k := range configFile.AuthConfigs {
logf(log.LevelInfo, "Docker config contains auth for registry %q", k)
}
err = os.WriteFile(cfgPath, decoded, 0o644)
if err != nil {
return noop, fmt.Errorf("write docker config: %w", err)
return restoreEnv, fmt.Errorf("write docker config: %w", err)
}
logf(log.LevelInfo, "Wrote Docker config JSON to %s", cfgPath)
oldDockerConfig := os.Getenv("DOCKER_CONFIG")
_ = os.Setenv("DOCKER_CONFIG", workingDir.Path())
newDockerConfig := os.Getenv("DOCKER_CONFIG")
logf(log.LevelInfo, "Set DOCKER_CONFIG to %s", newDockerConfig)
cleanup := func() error {

var cleanupOnce sync.Once
return func() error {
var cleanupErr error
cleanupOnce.Do(func() {
// Restore the old DOCKER_CONFIG value.
os.Setenv("DOCKER_CONFIG", oldDockerConfig)
logf(log.LevelInfo, "Restored DOCKER_CONFIG to %s", oldDockerConfig)
cleanupErr = restoreEnv()
// Remove the Docker config secret file!
if cleanupErr = os.Remove(cfgPath); err != nil {
if err := os.Remove(cfgPath); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
cleanupErr = fmt.Errorf("remove docker config: %w", cleanupErr)
err = errors.Join(err, fmt.Errorf("remove docker config: %w", err))
}
logf(log.LevelError, "Failed to remove the Docker config secret file: %s", cleanupErr)
cleanupErr = errors.Join(cleanupErr, err)
}
})
return cleanupErr
}
return cleanup, err
}, nil
}

// Allows quick testing of layer caching using a local directory!
Expand Down
68 changes: 63 additions & 5 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -552,13 +552,20 @@ func TestBuildFromDockerfile(t *testing.T) {
"Dockerfile": "FROM " + testImageAlpine,
},
})
ctr, err := runEnvbuilder(t, runOpts{env: []string{
envbuilderEnv("GIT_URL", srv.URL),
envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
envbuilderEnv("DOCKER_CONFIG_BASE64", base64.StdEncoding.EncodeToString([]byte(`{"experimental": "enabled"}`))),
}})
logbuf := new(bytes.Buffer)
ctr, err := runEnvbuilder(t, runOpts{
env: []string{
envbuilderEnv("GIT_URL", srv.URL),
envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
envbuilderEnv("DOCKER_CONFIG_BASE64", base64.StdEncoding.EncodeToString([]byte(`{"experimental": "enabled"}`))),
"DOCKER_CONFIG=/config", // Ignored, because we're setting DOCKER_CONFIG_BASE64.
},
logbuf: logbuf,
})
require.NoError(t, err)

require.Contains(t, logbuf.String(), "Set DOCKER_CONFIG to /.envbuilder")

output := execContainer(t, ctr, "echo hello")
require.Equal(t, "hello", strings.TrimSpace(output))

Expand All @@ -568,6 +575,52 @@ func TestBuildFromDockerfile(t *testing.T) {
require.Contains(t, output, "No such file or directory")
}

func TestBuildDockerConfigPathFromEnv(t *testing.T) {
// Ensures that a Git repository with a Dockerfile is cloned and built.
srv := gittest.CreateGitServer(t, gittest.Options{
Files: map[string]string{
"Dockerfile": "FROM " + testImageAlpine,
},
})
dir := t.TempDir()
err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(`{"experimental": "enabled"}`), 0o644)
require.NoError(t, err)

logbuf := new(bytes.Buffer)
_, err = runEnvbuilder(t, runOpts{
env: []string{
envbuilderEnv("GIT_URL", srv.URL),
envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
"DOCKER_CONFIG=/config",
},
binds: []string{fmt.Sprintf("%s:/config:ro", dir)},
logbuf: logbuf,
})
require.NoError(t, err)

require.Contains(t, logbuf.String(), "Using existing DOCKER_CONFIG set to /config")
}

func TestBuildDockerConfigDefaultPath(t *testing.T) {
// Ensures that a Git repository with a Dockerfile is cloned and built.
srv := gittest.CreateGitServer(t, gittest.Options{
Files: map[string]string{
"Dockerfile": "FROM " + testImageAlpine,
},
})
logbuf := new(bytes.Buffer)
_, err := runEnvbuilder(t, runOpts{
env: []string{
envbuilderEnv("GIT_URL", srv.URL),
envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
},
logbuf: logbuf,
})
require.NoError(t, err)

require.Contains(t, logbuf.String(), "Set DOCKER_CONFIG to /.envbuilder")
}

func TestBuildPrintBuildOutput(t *testing.T) {
// Ensures that a Git repository with a Dockerfile is cloned and built.
srv := gittest.CreateGitServer(t, gittest.Options{
Expand Down Expand Up @@ -2296,6 +2349,7 @@ type runOpts struct {
binds []string
env []string
volumes map[string]string
logbuf *bytes.Buffer
}

// runEnvbuilder starts the envbuilder container with the given environment
Expand Down Expand Up @@ -2340,6 +2394,7 @@ func runEnvbuilder(t *testing.T, opts runOpts) (string, error) {
testContainerLabel: "true",
},
}, &container.HostConfig{
CapAdd: []string{"SYS_ADMIN"}, // For remounting.
NetworkMode: container.NetworkMode("host"),
Binds: opts.binds,
Mounts: mounts,
Expand All @@ -2357,6 +2412,9 @@ func runEnvbuilder(t *testing.T, opts runOpts) (string, error) {
logChan, errChan := streamContainerLogs(t, cli, ctr.ID)
go func() {
for log := range logChan {
if opts.logbuf != nil {
opts.logbuf.WriteString(log)
}
if strings.HasPrefix(log, "=== Running init command") {
errChan <- nil
return
Expand Down
Loading