diff --git a/packages/orchestrator/pkg/template/build/commands/workdir.go b/packages/orchestrator/pkg/template/build/commands/workdir.go index 592754b89a..4029443b45 100644 --- a/packages/orchestrator/pkg/template/build/commands/workdir.go +++ b/packages/orchestrator/pkg/template/build/commands/workdir.go @@ -102,34 +102,53 @@ func saveWorkdirMeta( cmdMetadata metadata.Context, workdir string, ) (m metadata.Context, e error) { - var outputErr error + var stdout strings.Builder + var stderr strings.Builder err := sandboxtools.RunCommandWithOutput( ctx, proxy, sandboxID, - fmt.Sprintf(`cd "%s" && pwd`, workdir), + fmt.Sprintf(`cd "%s" && printf '\n%s%%s\n' "$PWD"`, workdir, workdirOutputMarker), metadata.Context{ - User: "root", + User: "root", + EnvVars: cmdMetadata.EnvVars, // Workdir can't be set here to not error when current workdir is deleted }, - func(stdout, stderr string) { - if stderr != "" { - outputErr = fmt.Errorf("error getting absolute path of workdir: %s", stderr) - - return - } - workdir = strings.TrimSpace(stdout) + func(out, errOut string) { + _, _ = stdout.WriteString(out) + _, _ = stderr.WriteString(errOut) }, ) - if outputErr != nil { - return metadata.Context{}, outputErr - } + return saveWorkdirMetaFromOutput(cmdMetadata, workdir, stdout.String(), stderr.String(), err) +} + +const workdirOutputMarker = "__E2B_WORKDIR__:" +func saveWorkdirMetaFromOutput( + cmdMetadata metadata.Context, + workdir, stdout, stderr string, + commandErr error, +) (metadata.Context, error) { + resolved, err := absoluteWorkdirFromOutput(stdout, stderr, commandErr) if err != nil { return metadata.Context{}, fmt.Errorf("failed to save workdir %s: %w", workdir, err) } - - cmdMetadata.WorkDir = &workdir - + cmdMetadata.WorkDir = &resolved return cmdMetadata, nil } + +// absoluteWorkdirFromOutput uses a marked line so unrelated login-hook output +// cannot be mistaken for pwd. The caller collects all stream chunks first. +func absoluteWorkdirFromOutput(stdout, stderr string, commandErr error) (string, error) { + if commandErr != nil { + return "", fmt.Errorf("workdir command failed: %w", commandErr) + } + marker := strings.LastIndex(stdout, workdirOutputMarker) + if marker >= 0 { + path := strings.TrimSpace(strings.SplitN(stdout[marker+len(workdirOutputMarker):], "\n", 2)[0]) + if filepath.IsAbs(path) { + return path, nil + } + } + return "", fmt.Errorf("error getting absolute path of workdir: stdout=%q stderr=%q", stdout, stderr) +} diff --git a/packages/orchestrator/pkg/template/build/commands/workdir_parse_test.go b/packages/orchestrator/pkg/template/build/commands/workdir_parse_test.go new file mode 100644 index 0000000000..ecfe91b71a --- /dev/null +++ b/packages/orchestrator/pkg/template/build/commands/workdir_parse_test.go @@ -0,0 +1,61 @@ +package commands + +import ( + "errors" + "reflect" + "strings" + "testing" + + "github.com/e2b-dev/infra/packages/orchestrator/pkg/template/metadata" +) + +func TestSaveWorkdirMetaFromOutputPreservesImageMetadata(t *testing.T) { + oldWorkdir := "/image-default" + envs := map[string]string{"MAMBA_EXE": "/bin/micromamba", "APP_MODE": "freecad"} + image := metadata.Context{User: "app-user", WorkDir: &oldWorkdir, EnvVars: envs} + + got, err := saveWorkdirMetaFromOutput( + image, + "/app", + "login hook stdout: /noise/path\n"+workdirOutputMarker+"/app\n", + "_activate_current_env.sh: line 8: : command not found\ncritical libmamba Shell not initialized\n", + nil, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got.User != image.User { + t.Fatalf("user changed: got %q want %q", got.User, image.User) + } + if !reflect.DeepEqual(got.EnvVars, image.EnvVars) { + t.Fatalf("environment changed: got %#v want %#v", got.EnvVars, image.EnvVars) + } + if got.WorkDir == nil || *got.WorkDir != "/app" { + t.Fatalf("got workdir %v want /app", got.WorkDir) + } + if image.WorkDir == nil || *image.WorkDir != oldWorkdir { + t.Fatalf("input metadata was mutated: %#v", image) + } +} + +func TestSaveWorkdirMetaFromOutputRejectsMissingAbsolutePath(t *testing.T) { + _, err := saveWorkdirMetaFromOutput( + metadata.Context{}, "/app", "hook output only", "login hook diagnostic", nil, + ) + if err == nil { + t.Fatal("expected an error without an absolute marked path") + } + if !strings.Contains(err.Error(), "login hook diagnostic") { + t.Fatalf("error omits stderr context: %v", err) + } +} + +func TestSaveWorkdirMetaFromOutputPreservesCommandFailure(t *testing.T) { + commandErr := errors.New("exit status 1") + _, err := saveWorkdirMetaFromOutput( + metadata.Context{}, "/app", workdirOutputMarker+"/app\n", "", commandErr, + ) + if !errors.Is(err, commandErr) { + t.Fatalf("got %v, want wrapped command error", err) + } +}