diff --git a/pkg/agent/delivery/local_docker.go b/pkg/agent/delivery/local_docker.go index e236b78b8..62e365042 100644 --- a/pkg/agent/delivery/local_docker.go +++ b/pkg/agent/delivery/local_docker.go @@ -149,6 +149,10 @@ func (d *LocalDockerDelivery) populateVolumeDirectCopy( volumeName string, data []byte, ) error { + if d.isRootlessPodman(ctx) { + return d.populateVolumeUnshare(ctx, volumeName, data) + } + mountpoint, err := d.volumeMountpoint(ctx, volumeName) if err != nil { return fmt.Errorf("inspect volume mountpoint: %w", err) @@ -167,6 +171,28 @@ func (d *LocalDockerDelivery) populateVolumeDirectCopy( return nil } +func (d *LocalDockerDelivery) populateVolumeUnshare( + ctx context.Context, + volumeName string, + data []byte, +) error { + mountpoint, err := d.volumeMountpoint(ctx, volumeName) + if err != nil { + return fmt.Errorf("inspect volume mountpoint: %w", err) + } + + destPath := filepath.Join(mountpoint, binaryName()) + + cmd := d.cmd(ctx, "unshare", "sh", "-c", `cat > "$1" && chmod 755 "$1"`, "--", destPath) + cmd.Stdin = bytes.NewReader(data) + + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("podman unshare write: %s: %w", string(out), err) + } + return nil +} + func (d *LocalDockerDelivery) volumeMountpoint( ctx context.Context, volumeName string, @@ -191,6 +217,25 @@ func (d *LocalDockerDelivery) removeVolume(ctx context.Context, workspaceID stri return nil } +func (d *LocalDockerDelivery) isPodman(ctx context.Context) bool { + out, err := d.cmd(ctx, "--version").CombinedOutput() + if err != nil { + return false + } + return strings.Contains(string(out), "podman") +} + +func (d *LocalDockerDelivery) isRootlessPodman(ctx context.Context) bool { + if !d.isPodman(ctx) { + return false + } + out, err := d.cmd(ctx, "info", "--format", "{{.Host.Security.Rootless}}").CombinedOutput() + if err != nil { + return false + } + return strings.TrimSpace(string(out)) == "true" +} + func (d *LocalDockerDelivery) cmd(ctx context.Context, args ...string) *exec.Cmd { // #nosec G204 -- args are constructed internally, not from user input cmd := exec.CommandContext(ctx, d.dockerCommand(), args...) diff --git a/pkg/agent/delivery/local_docker_test.go b/pkg/agent/delivery/local_docker_test.go index 6d9861707..512d62d7c 100644 --- a/pkg/agent/delivery/local_docker_test.go +++ b/pkg/agent/delivery/local_docker_test.go @@ -124,3 +124,142 @@ func TestPopulateVolume_FallbackToDirectCopy(t *testing.T) { require.NoError(t, err) assert.Equal(t, os.FileMode(0o755), info.Mode().Perm()) } + +func TestIsPodman_Docker(t *testing.T) { + tmpDir := t.TempDir() + scriptPath := filepath.Join(tmpDir, "fake-docker.sh") + script := "#!/bin/sh\necho 'Docker version 24.0.7, build afdd53b'\n" + require.NoError(t, os.WriteFile(scriptPath, []byte(script), 0o600)) + // #nosec G302 -- test script must be executable + require.NoError(t, os.Chmod(scriptPath, 0o755)) + + d := &LocalDockerDelivery{DockerCommand: scriptPath} + assert.False(t, d.isPodman(context.Background())) +} + +func TestIsPodman_Podman(t *testing.T) { + tmpDir := t.TempDir() + scriptPath := filepath.Join(tmpDir, "fake-podman.sh") + script := "#!/bin/sh\necho 'podman version 4.9.3'\n" + require.NoError(t, os.WriteFile(scriptPath, []byte(script), 0o600)) + // #nosec G302 -- test script must be executable + require.NoError(t, os.Chmod(scriptPath, 0o755)) + + d := &LocalDockerDelivery{DockerCommand: scriptPath} + assert.True(t, d.isPodman(context.Background())) +} + +func TestIsRootlessPodman_DockerRuntime(t *testing.T) { + tmpDir := t.TempDir() + scriptPath := filepath.Join(tmpDir, "fake-docker.sh") + script := "#!/bin/sh\necho 'Docker version 24.0.7, build afdd53b'\n" + require.NoError(t, os.WriteFile(scriptPath, []byte(script), 0o600)) + // #nosec G302 -- test script must be executable + require.NoError(t, os.Chmod(scriptPath, 0o755)) + + d := &LocalDockerDelivery{DockerCommand: scriptPath} + assert.False(t, d.isRootlessPodman(context.Background())) +} + +func TestIsRootlessPodman_PodmanRootful(t *testing.T) { + tmpDir := t.TempDir() + scriptPath := filepath.Join(tmpDir, "fake-podman.sh") + script := "#!/bin/sh\n" + + "case \"$1\" in\n" + + " --version) echo 'podman version 4.9.3' ;;\n" + + " info) echo 'false' ;;\n" + + " *) exit 1 ;;\n" + + "esac\n" + require.NoError(t, os.WriteFile(scriptPath, []byte(script), 0o600)) + // #nosec G302 -- test script must be executable + require.NoError(t, os.Chmod(scriptPath, 0o755)) + + d := &LocalDockerDelivery{DockerCommand: scriptPath} + assert.False(t, d.isRootlessPodman(context.Background())) +} + +func TestIsRootlessPodman_PodmanRootless(t *testing.T) { + tmpDir := t.TempDir() + scriptPath := filepath.Join(tmpDir, "fake-podman.sh") + script := "#!/bin/sh\n" + + "case \"$1\" in\n" + + " --version) echo 'podman version 4.9.3' ;;\n" + + " info) echo 'true' ;;\n" + + " *) exit 1 ;;\n" + + "esac\n" + require.NoError(t, os.WriteFile(scriptPath, []byte(script), 0o600)) + // #nosec G302 -- test script must be executable + require.NoError(t, os.Chmod(scriptPath, 0o755)) + + d := &LocalDockerDelivery{DockerCommand: scriptPath} + assert.True(t, d.isRootlessPodman(context.Background())) +} + +func TestPopulateVolumeDirectCopy_RootlessPodman_UsesUnshare(t *testing.T) { + tmpDir := t.TempDir() + mountDir := filepath.Join(tmpDir, "mount") + require.NoError(t, os.MkdirAll(mountDir, 0o750)) + + scriptPath := filepath.Join(tmpDir, "fake-podman.sh") + script := "#!/bin/sh\n" + + "case \"$1\" in\n" + + " --version) echo 'podman version 4.9.3' ;;\n" + + " info) echo 'true' ;;\n" + + " volume) echo '" + mountDir + "' ;;\n" + + " unshare) shift; exec \"$@\" ;;\n" + + " *) exit 1 ;;\n" + + "esac\n" + require.NoError(t, os.WriteFile(scriptPath, []byte(script), 0o600)) + // #nosec G302 -- test script must be executable + require.NoError(t, os.Chmod(scriptPath, 0o755)) + + binaryContent := []byte("fake-agent-binary-rootless") + + d := &LocalDockerDelivery{DockerCommand: scriptPath} + + err := d.populateVolumeDirectCopy(context.Background(), "test-vol", binaryContent) + require.NoError(t, err) + + destPath := filepath.Join(mountDir, binaryName()) + data, err := os.ReadFile(destPath) //nolint:gosec // test reads from a temp directory we control + require.NoError(t, err) + assert.Equal(t, binaryContent, data) + + info, err := os.Stat(destPath) + require.NoError(t, err) + assert.Equal(t, os.FileMode(0o755), info.Mode().Perm()) +} + +func TestPopulateVolumeDirectCopy_Docker_SkipsUnshare(t *testing.T) { + tmpDir := t.TempDir() + mountDir := filepath.Join(tmpDir, "mount") + require.NoError(t, os.MkdirAll(mountDir, 0o750)) + + scriptPath := filepath.Join(tmpDir, "fake-docker.sh") + script := "#!/bin/sh\n" + + "case \"$1\" in\n" + + " --version) echo 'Docker version 24.0.7, build afdd53b' ;;\n" + + " volume) echo '" + mountDir + "' ;;\n" + + " unshare) echo 'unshare should not be called' >&2; exit 1 ;;\n" + + " *) exit 1 ;;\n" + + "esac\n" + require.NoError(t, os.WriteFile(scriptPath, []byte(script), 0o600)) + // #nosec G302 -- test script must be executable + require.NoError(t, os.Chmod(scriptPath, 0o755)) + + binaryContent := []byte("fake-agent-binary-docker") + + d := &LocalDockerDelivery{DockerCommand: scriptPath} + + err := d.populateVolumeDirectCopy(context.Background(), "test-vol", binaryContent) + require.NoError(t, err) + + destPath := filepath.Join(mountDir, binaryName()) + data, err := os.ReadFile(destPath) //nolint:gosec // test reads from a temp directory we control + require.NoError(t, err) + assert.Equal(t, binaryContent, data) + + info, err := os.Stat(destPath) + require.NoError(t, err) + assert.Equal(t, os.FileMode(0o755), info.Mode().Perm()) +}