Skip to content

Commit

Permalink
copy config to protect against remount, fix docker perms
Browse files Browse the repository at this point in the history
  • Loading branch information
mafredri committed Oct 28, 2024
1 parent d559c1d commit 95c076d
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 70 deletions.
158 changes: 104 additions & 54 deletions envbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ 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())

cleanupDockerConfigOverride, err := initDockerConfigOverride(opts.Logger, workingDir, opts.DockerConfigBase64)
cleanupDockerConfigOverride, err := initDockerConfigOverride(opts.Filesystem, opts.Logger, workingDir, opts.DockerConfigBase64)
if err != nil {
return err
}
Expand Down Expand Up @@ -711,6 +711,11 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro
// Sanitize the environment of any opts!
options.UnsetEnv()

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

// Set the environment from /etc/environment first, so it can be
// overridden by the image and devcontainer settings.
err = setEnvFromEtcEnvironment(opts.Logger)
Expand Down Expand Up @@ -770,11 +775,6 @@ func run(ctx context.Context, opts options.Options, execArgs *execArgsInfo) erro
exportEnvFile.Close()
}

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

if runtimeData.ContainerUser == "" {
opts.Logger(log.LevelWarn, "#%d: no user specified, using root", stageNumber)
}
Expand Down Expand Up @@ -978,7 +978,7 @@ 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())

cleanupDockerConfigOverride, err := initDockerConfigOverride(opts.Logger, workingDir, opts.DockerConfigBase64)
cleanupDockerConfigOverride, err := initDockerConfigOverride(opts.Filesystem, opts.Logger, workingDir, opts.DockerConfigBase64)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -1571,6 +1571,20 @@ func fileExists(fs billy.Filesystem, path string) bool {
return err == nil
}

func readFile(fs billy.Filesystem, src string) ([]byte, error) {
f, err := fs.Open(src)
if err != nil {
return nil, fmt.Errorf("open src file: %w", err)
}
defer f.Close()

b, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("read src file: %w", err)
}
return b, nil
}

func copyFile(fs billy.Filesystem, src, dst string, mode fs.FileMode) error {
srcF, err := fs.Open(src)
if err != nil {
Expand All @@ -1595,6 +1609,21 @@ func copyFile(fs billy.Filesystem, src, dst string, mode fs.FileMode) error {
return nil
}

func writeFile(fs billy.Filesystem, name string, data []byte, perm fs.FileMode) error {
f, err := fs.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm)
if err != nil {
return fmt.Errorf("create file: %w", err)
}
_, err = f.Write(data)
if err != nil {
err = fmt.Errorf("write file: %w", err)
}
if err1 := f.Close(); err1 != nil && err == nil {
err = fmt.Errorf("close file: %w", err1)
}
return err
}

func writeMagicImageFile(fs billy.Filesystem, path string, v any) error {
file, err := fs.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
if err != nil {
Expand Down Expand Up @@ -1627,39 +1656,50 @@ func parseMagicImageFile(fs billy.Filesystem, path string, v any) 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)
func initDockerConfigOverride(bfs billy.Filesystem, logf log.Func, workingDir workingdir.WorkingDir, dockerConfigBase64 string) (func() error, error) {
configFile := "config.json"
oldDockerConfig := os.Getenv("DOCKER_CONFIG")
newDockerConfig := workingDir.Path()

restoreEnv = func() error {
// Restore the old DOCKER_CONFIG value.
if oldDockerConfig == "" {
err := os.Unsetenv("DOCKER_CONFIG")
if err != nil {
return fmt.Errorf("unset DOCKER_CONFIG: %w", err)
}
return nil
}
err := os.Setenv("DOCKER_CONFIG", 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 := onceErr(func() error {
// Restore the old DOCKER_CONFIG value.
if oldDockerConfig == "" {
err := os.Unsetenv("DOCKER_CONFIG")
if err != nil {
return fmt.Errorf("restore DOCKER_CONFIG: %w", err)
return fmt.Errorf("unset 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)

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
})

// If the user hasn't set the BASE64 encoded Docker config, we
// should respect the DOCKER_CONFIG environment variable.
oldDockerConfigFile := filepath.Join(oldDockerConfig, configFile)
if dockerConfigBase64 == "" && fileExists(bfs, oldDockerConfigFile) {
// It's possible that the target file is mounted and needs to
// be remounted later, so we should copy the file instead of
// hoping that the file is still there when we build.
logf(log.LevelInfo, "Using DOCKER_CONFIG at %s", oldDockerConfig)
b, err := readFile(bfs, oldDockerConfigFile)
if err != nil {
return nil, fmt.Errorf("read existing DOCKER_CONFIG: %w", err)
}
// Read into dockerConfigBase64 so we can keep the same logic.
dockerConfigBase64 = base64.StdEncoding.EncodeToString(b)
}

if dockerConfigBase64 == "" {
Expand All @@ -1670,40 +1710,50 @@ func initDockerConfigOverride(logf log.Func, workingDir workingdir.WorkingDir, d
if err != nil {
return restoreEnv, fmt.Errorf("decode docker config: %w", err)
}
var configFile DockerConfig
decoded, err = hujson.Standardize(decoded)
if err != nil {
return restoreEnv, fmt.Errorf("humanize json for docker config: %w", err)
}
err = json.Unmarshal(decoded, &configFile)
var dockerConfig DockerConfig
err = json.Unmarshal(decoded, &dockerConfig)
if err != nil {
return restoreEnv, fmt.Errorf("parse docker config: %w", err)
}
for k := range configFile.AuthConfigs {
for k := range dockerConfig.AuthConfigs {
logf(log.LevelInfo, "Docker config contains auth for registry %q", k)
}
err = os.WriteFile(cfgPath, decoded, 0o644)

newDockerConfigFile := filepath.Join(newDockerConfig, configFile)
err = writeFile(bfs, newDockerConfigFile, decoded, 0o644)
if err != nil {
return restoreEnv, fmt.Errorf("write docker config: %w", err)
}
logf(log.LevelInfo, "Wrote Docker config JSON to %s", cfgPath)
logf(log.LevelInfo, "Wrote Docker config JSON to %s", newDockerConfigFile)

var cleanupOnce sync.Once
return func() error {
var cleanupErr error
cleanupOnce.Do(func() {
cleanupErr = restoreEnv()
// Remove the Docker config secret file!
if err := os.Remove(cfgPath); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
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)
cleanup := onceErr(func() error {
err := restoreEnv()
// Remove the Docker config secret file!
if err2 := os.Remove(newDockerConfigFile); err2 != nil {
if !errors.Is(err2, fs.ErrNotExist) {
err2 = fmt.Errorf("remove docker config: %w", err2)
}
logf(log.LevelError, "Failed to remove the Docker config secret file: %s", err2)
err = errors.Join(err, err2)
}
return err
})
return cleanup, nil
}

func onceErr(f func() error) func() error {
var once sync.Once
return func() error {
var err error
once.Do(func() {
err = f()
})
return cleanupErr
}, nil
return err
}
}

// Allows quick testing of layer caching using a local directory!
Expand Down
52 changes: 36 additions & 16 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ func TestBuildFromDockerfile(t *testing.T) {
require.NoError(t, err)

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

output := execContainer(t, ctr, "echo hello")
require.Equal(t, "hello", strings.TrimSpace(output))
Expand All @@ -582,8 +583,17 @@ func TestBuildDockerConfigPathFromEnv(t *testing.T) {
"Dockerfile": "FROM " + testImageAlpine,
},
})
config, err := json.Marshal(envbuilder.DockerConfig{
AuthConfigs: map[string]clitypes.AuthConfig{
"mytestimage": {
Username: "user",
Password: "test",
},
},
})
require.NoError(t, err)
dir := t.TempDir()
err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(`{"experimental": "enabled"}`), 0o644)
err = os.WriteFile(filepath.Join(dir, "config.json"), config, 0o644)
require.NoError(t, err)

logbuf := new(bytes.Buffer)
Expand All @@ -593,12 +603,16 @@ func TestBuildDockerConfigPathFromEnv(t *testing.T) {
envbuilderEnv("DOCKERFILE_PATH", "Dockerfile"),
"DOCKER_CONFIG=/config",
},
binds: []string{fmt.Sprintf("%s:/config:ro", dir)},
logbuf: logbuf,
privileged: true,
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")
// Logs that the DOCKER_CONFIG is used.
require.Contains(t, logbuf.String(), "Using DOCKER_CONFIG at /config")
// Logs registry auth info from existing file.
require.Contains(t, logbuf.String(), "mytestimage")
}

func TestBuildDockerConfigDefaultPath(t *testing.T) {
Expand All @@ -619,6 +633,7 @@ func TestBuildDockerConfigDefaultPath(t *testing.T) {
require.NoError(t, err)

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

func TestBuildPrintBuildOutput(t *testing.T) {
Expand Down Expand Up @@ -2345,11 +2360,12 @@ func startContainerFromRef(ctx context.Context, t *testing.T, cli *client.Client
}

type runOpts struct {
image string
binds []string
env []string
volumes map[string]string
logbuf *bytes.Buffer
image string
privileged bool // Required for remounting.
binds []string
env []string
volumes map[string]string
logbuf *bytes.Buffer
}

// runEnvbuilder starts the envbuilder container with the given environment
Expand Down Expand Up @@ -2387,18 +2403,22 @@ func runEnvbuilder(t *testing.T, opts runOpts) (string, error) {
require.NoError(t, err, "failed to read image pull response")
img = opts.image
}
hostConfig := &container.HostConfig{
NetworkMode: container.NetworkMode("host"),
Binds: opts.binds,
Mounts: mounts,
}
if opts.privileged {
hostConfig.CapAdd = append(hostConfig.CapAdd, "SYS_ADMIN")
hostConfig.Privileged = true
}
ctr, err := cli.ContainerCreate(ctx, &container.Config{
Image: img,
Env: opts.env,
Labels: map[string]string{
testContainerLabel: "true",
},
}, &container.HostConfig{
CapAdd: []string{"SYS_ADMIN"}, // For remounting.
NetworkMode: container.NetworkMode("host"),
Binds: opts.binds,
Mounts: mounts,
}, nil, nil, "")
}, hostConfig, nil, nil, "")
require.NoError(t, err)
t.Cleanup(func() {
_ = cli.ContainerRemove(ctx, ctr.ID, container.RemoveOptions{
Expand All @@ -2413,7 +2433,7 @@ func runEnvbuilder(t *testing.T, opts runOpts) (string, error) {
go func() {
for log := range logChan {
if opts.logbuf != nil {
opts.logbuf.WriteString(log)
opts.logbuf.WriteString(log + "\n")
}
if strings.HasPrefix(log, "=== Running init command") {
errChan <- nil
Expand Down

0 comments on commit 95c076d

Please sign in to comment.