Skip to content

Commit 884b23a

Browse files
committed
Bind mount mounted as root on Kubernetes
1 parent 5a0efcb commit 884b23a

File tree

8 files changed

+246
-15
lines changed

8 files changed

+246
-15
lines changed

pkg/devcontainer/compose.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func (r *runner) stopDockerCompose(ctx context.Context, projectName string) erro
4545
return errors.Wrap(err, "find docker compose")
4646
}
4747

48-
parsedConfig, _, err := r.getSubstitutedConfig(r.WorkspaceConfig.CLIOptions)
48+
parsedConfig, _, err := r.getSubstitutedConfig(r.WorkspaceConfig.CLIOptions, map[string]string{})
4949
if err != nil {
5050
return errors.Wrap(err, "get parsed config")
5151
}
@@ -69,7 +69,7 @@ func (r *runner) deleteDockerCompose(ctx context.Context, projectName string) er
6969
return errors.Wrap(err, "find docker compose")
7070
}
7171

72-
parsedConfig, _, err := r.getSubstitutedConfig(r.WorkspaceConfig.CLIOptions)
72+
parsedConfig, _, err := r.getSubstitutedConfig(r.WorkspaceConfig.CLIOptions, map[string]string{})
7373
if err != nil {
7474
return errors.Wrap(err, "get parsed config")
7575
}

pkg/devcontainer/config.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,18 +89,19 @@ func (r *runner) getDefaultConfig(options provider2.CLIOptions) (*config.DevCont
8989
return defaultConfig, nil
9090
}
9191

92-
func (r *runner) getSubstitutedConfig(options provider2.CLIOptions) (*config.SubstitutedConfig, *config.SubstitutionContext, error) {
92+
func (r *runner) getSubstitutedConfig(options provider2.CLIOptions, probedEnv map[string]string) (*config.SubstitutedConfig, *config.SubstitutionContext, error) {
9393
rawConfig, err := r.getRawConfig(options)
9494
if err != nil {
9595
return nil, nil, err
9696
}
9797

98-
return r.substitute(options, rawConfig)
98+
return r.substitute(options, rawConfig, probedEnv)
9999
}
100100

101101
func (r *runner) substitute(
102102
options provider2.CLIOptions,
103103
rawParsedConfig *config.DevContainerConfig,
104+
probedEnv map[string]string,
104105
) (*config.SubstitutedConfig, *config.SubstitutionContext, error) {
105106
configFile := rawParsedConfig.Origin
106107

@@ -110,11 +111,17 @@ func (r *runner) substitute(
110111
r.WorkspaceConfig.Workspace.ID,
111112
rawParsedConfig,
112113
)
114+
// merge probed environment with os.Environ()
115+
env := config.ListToObject(os.Environ())
116+
for k, v := range probedEnv {
117+
env[k] = v
118+
}
119+
113120
substitutionContext := &config.SubstitutionContext{
114121
DevContainerID: r.ID,
115122
LocalWorkspaceFolder: r.LocalWorkspaceFolder,
116123
ContainerWorkspaceFolder: containerWorkspaceFolder,
117-
Env: config.ListToObject(os.Environ()),
124+
Env: env,
118125

119126
WorkspaceMount: workspaceMount,
120127
}

pkg/devcontainer/config/userenvprobe.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ func NewUserEnvProbe(probe string) (UserEnvProbe, error) {
4848
}
4949

5050
func ProbeUserEnv(ctx context.Context, probe string, userName string, log log.Logger) (map[string]string, error) {
51+
return ProbeUserEnvWithUserSwitch(ctx, probe, userName, true, log)
52+
}
53+
54+
func ProbeUserEnvWithUserSwitch(ctx context.Context, probe string, userName string, switchUser bool, log log.Logger) (map[string]string, error) {
5155
userEnvProbe, err := NewUserEnvProbe(probe)
5256
if err != nil {
5357
log.Warnf("Get user env probe: %v", err)
@@ -66,12 +70,12 @@ func ProbeUserEnv(ctx context.Context, probe string, userName string, log log.Lo
6670
log.Debugf("running user env probe with shell \"%s\", probe \"%s\", user \"%s\" and command \"%s\"",
6771
strings.Join(preferredShell, " "), string(userEnvProbe), userName, "cat /proc/self/environ")
6872

69-
probedEnv, err := doProbe(ctx, userEnvProbe, preferredShell, userName, "cat /proc/self/environ", '\x00', log)
73+
probedEnv, err := doProbeWithUserSwitch(ctx, userEnvProbe, preferredShell, userName, "cat /proc/self/environ", '\x00', switchUser, log)
7074
if err != nil {
7175
log.Debugf("running user env probe with shell \"%s\", probe \"%s\", user \"%s\" and command \"%s\"",
7276
strings.Join(preferredShell, " "), string(userEnvProbe), userName, "printenv")
7377

74-
newProbedEnv, newErr := doProbe(ctx, userEnvProbe, preferredShell, userName, "printenv", '\n', log)
78+
newProbedEnv, newErr := doProbeWithUserSwitch(ctx, userEnvProbe, preferredShell, userName, "printenv", '\n', switchUser, log)
7579
if newErr != nil {
7680
log.Warnf("failed to probe user environment variables: %v, %v", err, newErr)
7781
} else {
@@ -86,16 +90,22 @@ func ProbeUserEnv(ctx context.Context, probe string, userName string, log log.Lo
8690
}
8791

8892
func doProbe(ctx context.Context, userEnvProbe UserEnvProbe, preferredShell []string, userName string, probeCmd string, sep byte, log log.Logger) (map[string]string, error) {
93+
return doProbeWithUserSwitch(ctx, userEnvProbe, preferredShell, userName, probeCmd, sep, true, log)
94+
}
95+
96+
func doProbeWithUserSwitch(ctx context.Context, userEnvProbe UserEnvProbe, preferredShell []string, userName string, probeCmd string, sep byte, switchUser bool, log log.Logger) (map[string]string, error) {
8997
args := preferredShell
9098
args = append(args, getShellArgs(userEnvProbe, userName, probeCmd)...)
9199

92100
timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
93101
defer cancel()
94102
cmd := exec.CommandContext(timeoutCtx, args[0], args[1:]...)
95103

96-
err := PrepareCmdUser(cmd, userName)
97-
if err != nil {
98-
return nil, fmt.Errorf("prepare probe: %w", err)
104+
if switchUser {
105+
err := PrepareCmdUser(cmd, userName)
106+
if err != nil {
107+
return nil, fmt.Errorf("prepare probe: %w", err)
108+
}
99109
}
100110

101111
out, err := cmd.Output()
@@ -114,7 +124,7 @@ func doProbe(ctx context.Context, userEnvProbe UserEnvProbe, preferredShell []st
114124
log.Debugf("failed to split env var: %s", line)
115125
continue
116126
}
117-
retEnv[tokens[0]] = tokens[1]
127+
retEnv[tokens[0]] = strings.Join(tokens[1:], "=")
118128
}
119129
if scanner.Err() != nil {
120130
return nil, fmt.Errorf("scan shell output: %w", err)

pkg/devcontainer/prebuild.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func (r *runner) Build(ctx context.Context, options provider.BuildOptions) (stri
2121
return "", fmt.Errorf("building only supported with docker driver")
2222
}
2323

24-
substitutedConfig, substitutionContext, err := r.getSubstitutedConfig(options.CLIOptions)
24+
substitutedConfig, substitutionContext, err := r.getSubstitutedConfig(options.CLIOptions, map[string]string{})
2525
if err != nil {
2626
return "", err
2727
}

pkg/devcontainer/run.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,14 @@ type UpOptions struct {
9292
func (r *runner) Up(ctx context.Context, options UpOptions, timeout time.Duration) (*config.Result, error) {
9393
r.Log.Debugf("Up devcontainer for workspace '%s' with timeout %s", r.WorkspaceConfig.Workspace.ID, timeout)
9494

95-
substitutedConfig, substitutionContext, err := r.getSubstitutedConfig(options.CLIOptions)
95+
// probe local user environment for localEnv substitution
96+
probedEnv, err := config.ProbeUserEnvWithUserSwitch(ctx, string(config.DefaultUserEnvProbe), "", false, r.Log)
97+
if err != nil {
98+
r.Log.Warnf("failed to probe local user environment, localEnv variables may not work: %v", err)
99+
probedEnv = map[string]string{}
100+
}
101+
102+
substitutedConfig, substitutionContext, err := r.getSubstitutedConfig(options.CLIOptions, probedEnv)
96103
if err != nil {
97104
return nil, err
98105
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package setup
2+
3+
import (
4+
"os"
5+
"os/user"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/loft-sh/devpod/pkg/devcontainer/config"
10+
"github.com/loft-sh/log"
11+
)
12+
13+
func TestChownMounts(t *testing.T) {
14+
// Create temp directories for mounts
15+
tempDir := t.TempDir()
16+
mountTarget1 := filepath.Join(tempDir, "mount1")
17+
mountTarget2 := filepath.Join(tempDir, "mount2")
18+
err := os.Mkdir(mountTarget1, 0755)
19+
if err != nil {
20+
t.Fatalf("Failed to create temp dir mount1: %v", err)
21+
}
22+
err = os.Mkdir(mountTarget2, 0755)
23+
if err != nil {
24+
t.Fatalf("Failed to create temp dir mount2: %v", err)
25+
}
26+
27+
// Set MarkerBaseDir to temp dir to avoid permission issues
28+
oldMarkerBaseDir := MarkerBaseDir
29+
MarkerBaseDir = t.TempDir()
30+
defer func() { MarkerBaseDir = oldMarkerBaseDir }()
31+
32+
// Get current user
33+
currentUser, err := user.Current()
34+
if err != nil {
35+
t.Fatalf("Failed to get current user: %v", err)
36+
}
37+
38+
// Create a mock result with bind mounts
39+
result := &config.Result{
40+
MergedConfig: &config.MergedDevContainerConfig{
41+
DevContainerConfigBase: config.DevContainerConfigBase{
42+
RemoteUser: currentUser.Username,
43+
},
44+
NonComposeBase: config.NonComposeBase{
45+
Mounts: []*config.Mount{
46+
{
47+
Source: "/local/path",
48+
Target: mountTarget1,
49+
Type: "bind",
50+
},
51+
},
52+
},
53+
},
54+
SubstitutionContext: &config.SubstitutionContext{
55+
WorkspaceMount: "source=/ws/src,target=" + mountTarget2 + ",type=bind",
56+
ContainerWorkspaceFolder: mountTarget2,
57+
},
58+
}
59+
60+
// Mock logger
61+
logger := log.Discard
62+
63+
// Call ChownMounts
64+
// We expect it to succeed for the existing directories with current user
65+
err = ChownMounts(result, logger)
66+
if err != nil {
67+
t.Errorf("ChownMounts failed: %v", err)
68+
}
69+
70+
// Verify marker file created
71+
markerPath := filepath.Join(MarkerBaseDir, "chownMounts.marker")
72+
if _, err := os.Stat(markerPath); os.IsNotExist(err) {
73+
t.Errorf("Marker file not created at %s", markerPath)
74+
}
75+
76+
// Call again, should be skipped (log logic inside, but we rely on function returning nil and not erroring)
77+
err = ChownMounts(result, logger)
78+
if err != nil {
79+
t.Errorf("ChownMounts second call failed: %v", err)
80+
}
81+
}

pkg/devcontainer/setup/setup.go

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ import (
2626
)
2727

2828
const (
29-
ResultLocation = "/var/run/devpod/result.json"
29+
DefaultResultLocation = "/var/run/devpod/result.json"
30+
)
31+
32+
var (
33+
ResultLocation = DefaultResultLocation
34+
MarkerBaseDir = "/var/devpod"
3035
)
3136

3237
func SetupContainer(ctx context.Context, setupInfo *config.Result, extraWorkspaceEnv []string, chownProjects bool, platformOptions *devpod.PlatformOptions, tunnelClient tunnel.TunnelClient, log log.Logger) error {
@@ -39,6 +44,12 @@ func SetupContainer(ctx context.Context, setupInfo *config.Result, extraWorkspac
3944
return errors.Wrap(err, "chown workspace")
4045
}
4146

47+
// chown mounts
48+
err = ChownMounts(setupInfo, log)
49+
if err != nil {
50+
log.Warnf("Error chowning mounts: %v", err)
51+
}
52+
4253
// patch remote env
4354
log.Debugf("Patch etc environment & profile...")
4455
err = PatchEtcEnvironment(setupInfo.MergedConfig, log)
@@ -179,6 +190,40 @@ func ChownWorkspace(setupInfo *config.Result, recursive bool, log log.Logger) er
179190
return nil
180191
}
181192

193+
func ChownMounts(setupInfo *config.Result, log log.Logger) error {
194+
user := config.GetRemoteUser(setupInfo)
195+
exists, err := markerFileExists("chownMounts", "")
196+
if err != nil {
197+
return err
198+
} else if exists {
199+
return nil
200+
}
201+
202+
// check if we have any mounts to chown
203+
mounts := config.GetMounts(setupInfo)
204+
if len(mounts) == 0 {
205+
return nil
206+
}
207+
208+
log.Infof("Chown mounts...")
209+
for _, m := range mounts {
210+
if m.Type == "bind" && m.Target != "" {
211+
// check if it is the workspace folder
212+
if strings.HasPrefix(m.Target, setupInfo.SubstitutionContext.ContainerWorkspaceFolder) {
213+
continue
214+
}
215+
216+
log.Debugf("Chown mount %s...", m.Target)
217+
err = copy2.Chown(m.Target, user)
218+
if err != nil {
219+
// Just log warning as some mounts might not be chownable or might not exist in the same way
220+
log.Debugf("Failed to chown mount %s: %v", m.Target, err)
221+
}
222+
}
223+
}
224+
return nil
225+
}
226+
182227
func PatchEtcProfile() error {
183228
exists, err := markerFileExists("patchEtcProfile", "")
184229
if err != nil {
@@ -330,7 +375,7 @@ func SetupKubeConfig(ctx context.Context, setupInfo *config.Result, tunnelClient
330375
}
331376

332377
func markerFileExists(markerName string, markerContent string) (bool, error) {
333-
markerName = filepath.Join("/var/devpod", markerName+".marker")
378+
markerName = filepath.Join(MarkerBaseDir, markerName+".marker")
334379
t, err := os.ReadFile(markerName)
335380
if err != nil && !os.IsNotExist(err) {
336381
return false, err
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package setup
2+
3+
import (
4+
"os"
5+
"os/user"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/loft-sh/devpod/pkg/devcontainer/config"
10+
"github.com/loft-sh/log"
11+
)
12+
13+
func TestChownMounts(t *testing.T) {
14+
// Create temp directories for mounts
15+
tempDir := t.TempDir()
16+
mountTarget1 := filepath.Join(tempDir, "mount1")
17+
mountTarget2 := filepath.Join(tempDir, "mount2")
18+
err := os.Mkdir(mountTarget1, 0755)
19+
if err != nil {
20+
t.Fatalf("Failed to create temp dir mount1: %v", err)
21+
}
22+
err = os.Mkdir(mountTarget2, 0755)
23+
if err != nil {
24+
t.Fatalf("Failed to create temp dir mount2: %v", err)
25+
}
26+
27+
// Set MarkerBaseDir to temp dir to avoid permission issues
28+
oldMarkerBaseDir := MarkerBaseDir
29+
MarkerBaseDir = t.TempDir()
30+
defer func() { MarkerBaseDir = oldMarkerBaseDir }()
31+
32+
// Get current user
33+
currentUser, err := user.Current()
34+
if err != nil {
35+
t.Fatalf("Failed to get current user: %v", err)
36+
}
37+
38+
// Create a mock result with bind mounts
39+
result := &config.Result{
40+
MergedConfig: &config.MergedDevContainerConfig{
41+
DevContainerConfigBase: config.DevContainerConfigBase{
42+
RemoteUser: currentUser.Username,
43+
},
44+
NonComposeBase: config.NonComposeBase{
45+
Mounts: []*config.Mount{
46+
{
47+
Source: "/local/path",
48+
Target: mountTarget1,
49+
Type: "bind",
50+
},
51+
},
52+
},
53+
},
54+
SubstitutionContext: &config.SubstitutionContext{
55+
WorkspaceMount: "source=/ws/src,target=" + mountTarget2 + ",type=bind",
56+
ContainerWorkspaceFolder: mountTarget2,
57+
},
58+
}
59+
60+
// Mock logger
61+
logger := log.Discard
62+
63+
// Call ChownMounts
64+
// We expect it to succeed for the existing directories with current user
65+
err = ChownMounts(result, logger)
66+
if err != nil {
67+
t.Errorf("ChownMounts failed: %v", err)
68+
}
69+
70+
// Verify marker file created
71+
markerPath := filepath.Join(MarkerBaseDir, "chownMounts.marker")
72+
if _, err := os.Stat(markerPath); os.IsNotExist(err) {
73+
t.Errorf("Marker file not created at %s", markerPath)
74+
}
75+
76+
// Call again, should be skipped (log logic inside, but we rely on function returning nil and not erroring)
77+
err = ChownMounts(result, logger)
78+
if err != nil {
79+
t.Errorf("ChownMounts second call failed: %v", err)
80+
}
81+
}

0 commit comments

Comments
 (0)