Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions e2e/tests/up/provider_podman.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,38 @@ var _ = ginkgo.Describe(
},
ginkgo.SpecTimeout(framework.TimeoutShort()),
)

// Regression: rootless podman bind-mounts the workspace folder
// root-owned, so a non-root remoteUser couldn't chdir into it
// and the in-container SSH server failed with "fork/exec
// /usr/bin/bash: permission denied".
ginkgo.It(
"should ssh into a workspace with a non-root remoteUser",
func(ctx context.Context) {
tempDir, err := setupWorkspace(
"tests/up/testdata/docker-nonroot-user",
initialDir,
f,
)
framework.ExpectNoError(err)

err = f.DevsyUp(ctx, tempDir)
framework.ExpectNoError(err)

whoami, err := f.DevsySSH(ctx, tempDir, "whoami")
framework.ExpectNoError(err)
gomega.Expect(strings.TrimSpace(whoami)).To(gomega.Equal("devsyuser"))

// Assert ownership, not just chdir success: a
// world-searchable bind source would let chdir pass even
// without the chown.
owner, err := f.DevsySSH(ctx, tempDir, `stat -c %U "$PWD"`)
framework.ExpectNoError(err)
gomega.Expect(strings.TrimSpace(owner)).To(gomega.Equal("devsyuser"),
"workspace folder should be chowned to the remote user")
},
ginkgo.SpecTimeout(framework.TimeoutLong()),
)
})

ginkgo.Context("build", func() {
Expand Down
7 changes: 7 additions & 0 deletions e2e/tests/up/testdata/docker-nonroot-user/.devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "Non-root user",
"build": {
"dockerfile": "Dockerfile"
},
"remoteUser": "devsyuser"
}
4 changes: 4 additions & 0 deletions e2e/tests/up/testdata/docker-nonroot-user/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FROM ghcr.io/devsy-org/test-images/base:ubuntu

# Non-root user, used as remoteUser to exercise the podman workspace-chown path.
RUN useradd --create-home --shell /bin/bash devsyuser
2 changes: 1 addition & 1 deletion pkg/devcontainer/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ func mountSetConsistency(mount, value string) string {
}

func needsDefaultConsistency() bool {
return runtime.GOOS != "linux"
return runtime.GOOS != goosLinux
}

func getWorkspace(
Expand Down
21 changes: 20 additions & 1 deletion pkg/devcontainer/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/devsy-org/devsy/pkg/devcontainer/config"
"github.com/devsy-org/devsy/pkg/devcontainer/crane"
"github.com/devsy-org/devsy/pkg/devcontainer/sshtunnel"
"github.com/devsy-org/devsy/pkg/docker"
"github.com/devsy-org/devsy/pkg/driver"
"github.com/devsy-org/devsy/pkg/ide"
"github.com/devsy-org/devsy/pkg/log"
Expand All @@ -31,6 +32,10 @@ const (
stringTrue = "true"
stringFalse = "false"
containerRootUser = "root"

goosLinux = "linux"
goosDarwin = "darwin"
goosWindows = "windows"
)

// resolvePullFromInsideContainer: explicit override > crane+git > "".
Expand Down Expand Up @@ -331,11 +336,25 @@ func (r *runner) addSetupFlags(args *[]string) {
}

func (r *runner) addChownFlag(args *[]string, isDockerDriver bool) {
if runtime.GOOS == "linux" || !isDockerDriver {
if shouldChownWorkspace(runtime.GOOS, isDockerDriver, r.isPodmanRuntime()) {
*args = append(*args, "--chown-workspace")
}
}

// shouldChownWorkspace reports whether the agent should chown the workspace
// folder to the remote user during setup. Podman needs it on any host OS:
// its `podman machine` bind mounts are root-owned inside the container, so a
// non-root remote user can't enter the workspace folder otherwise.
func shouldChownWorkspace(goos string, isDockerDriver, isPodman bool) bool {
return goos == goosLinux || !isDockerDriver || isPodman
}

// isPodmanRuntime reports whether the docker driver is backed by the Podman
// runtime (agent.docker.runtime: podman).
func (r *runner) isPodmanRuntime() bool {
return strings.EqualFold(r.WorkspaceConfig.Agent.Docker.Runtime, string(docker.RuntimePodman))
}

func (r *runner) addDriverFlags(args *[]string, isDockerDriver bool) {
if !isDockerDriver {
*args = append(*args, "--stream-mounts")
Expand Down
79 changes: 79 additions & 0 deletions pkg/devcontainer/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,91 @@ import (
"testing"

"github.com/devsy-org/devsy/pkg/devcontainer/config"
"github.com/devsy-org/devsy/pkg/docker"
provider2 "github.com/devsy-org/devsy/pkg/provider"
"github.com/devsy-org/devsy/pkg/types"
)

func boolPtr(v bool) *bool { return &v }

func TestShouldChownWorkspace(t *testing.T) {
cases := []struct {
name string
goos string
isDockerDriver bool
isPodman bool
want bool
}{
{
name: "linux docker host always chowns",
goos: goosLinux, isDockerDriver: true, isPodman: false, want: true,
},
{
name: "non-docker driver always chowns (stream mounts)",
goos: goosDarwin, isDockerDriver: false, isPodman: false, want: true,
},
{
name: "docker desktop on macOS skips chown",
goos: goosDarwin, isDockerDriver: true, isPodman: false, want: false,
},
{
name: "docker desktop on windows skips chown",
goos: goosWindows, isDockerDriver: true, isPodman: false, want: false,
},
{
// Regression: previously skipped because podman uses the docker
// driver, breaking non-root remoteUser on macOS/Windows.
name: "podman on macOS chowns despite docker driver",
goos: goosDarwin, isDockerDriver: true, isPodman: true, want: true,
},
{
name: "podman on windows chowns despite docker driver",
goos: goosWindows, isDockerDriver: true, isPodman: true, want: true,
},
{
name: "podman on linux chowns",
goos: goosLinux, isDockerDriver: true, isPodman: true, want: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := shouldChownWorkspace(c.goos, c.isDockerDriver, c.isPodman)
if got != c.want {
t.Errorf("shouldChownWorkspace(%q, %v, %v) = %v, want %v",
c.goos, c.isDockerDriver, c.isPodman, got, c.want)
}
})
}
}

func TestRunnerIsPodmanRuntime(t *testing.T) {
cases := []struct {
name string
runtime string
want bool
}{
{name: "podman", runtime: string(docker.RuntimePodman), want: true},
{name: "podman mixed case", runtime: "Podman", want: true},
{name: "docker runtime", runtime: string(docker.RuntimeDocker), want: false},
{name: "empty defaults to non-podman", runtime: "", want: false},
{name: "nerdctl", runtime: string(docker.RuntimeNerdctl), want: false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
r := &runner{
WorkspaceConfig: &provider2.AgentWorkspaceInfo{
Agent: provider2.ProviderAgentConfig{
Docker: provider2.ProviderDockerDriverConfig{Runtime: c.runtime},
},
},
}
if got := r.isPodmanRuntime(); got != c.want {
t.Errorf("isPodmanRuntime() with runtime=%q = %v, want %v", c.runtime, got, c.want)
}
})
}
}

func TestResolvePullFromInsideContainer(t *testing.T) {
cases := []struct {
name string
Expand Down
Loading