Skip to content

fix: search $DOCKER_CONFIG if no base64 config is provided #398

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Oct 30, 2024
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