diff --git a/internal/cli/create.go b/internal/cli/create.go index d6e9bb55..643a35b1 100644 --- a/internal/cli/create.go +++ b/internal/cli/create.go @@ -17,6 +17,19 @@ import ( //nolint:funlen // function length is acceptable for CLI command definition func newCreateCommand(cfg *config.Values) *cli.Command { + imageDefault := cfg.DefaultContainerImage + if cfg.ContainerImage != "" { + imageDefault = cfg.ContainerImage + } + nameDefault := cfg.DefaultContainerName + if cfg.ContainerName != "" { + nameDefault = cfg.ContainerName + } + hostnameDefault := cfg.ContainerHostname + if hostnameDefault == "" { + hostnameDefault = defaultHostname() + } + return &cli.Command{ Name: "create", @@ -40,37 +53,30 @@ Examples: &cli.StringFlag{ Name: "image", Aliases: []string{"i"}, - Sources: cli.EnvVars("DBX_CONTAINER_IMAGE"), - Usage: fmt.Sprintf( - "image to use for the container (default: %s)", - cfg.DefaultContainerImage, - ), - // No Value default: leaving it empty lets makeContainerImage's - // "no clone & no image" branch select cfg.DefaultContainerImage, - // which is what triggers the default-name fallback. Setting - // Value here would defeat that check (shell parity). + Value: cfg.ContainerImage, + Usage: fmt.Sprintf("image to use for the container (default: %s)", imageDefault), }, &cli.StringFlag{ Name: "name", Aliases: []string{"n"}, - Sources: cli.EnvVars("DBX_CONTAINER_NAME"), - Usage: fmt.Sprintf("name for the distrobox (default: %s)", cfg.DefaultContainerName), + Value: cfg.ContainerName, + Usage: fmt.Sprintf("name for the distrobox (default: %s)", nameDefault), }, &cli.StringFlag{ - Name: "hostname", - Sources: cli.EnvVars("DBX_CONTAINER_HOSTNAME"), - Usage: fmt.Sprintf("hostname for the distrobox (default: %s)", defaultHostname()), + Name: "hostname", + Value: cfg.ContainerHostname, + Usage: fmt.Sprintf("hostname for the distrobox (default: %s)", hostnameDefault), }, &cli.BoolFlag{ Name: "pull", Aliases: []string{"p"}, - Sources: cli.EnvVars("DBX_CONTAINER_ALWAYS_PULL"), + Value: cfg.ContainerAlwaysPull, Usage: "pull the image even if it exists locally (implies --yes)", }, &cli.BoolFlag{ Name: "yes", Aliases: []string{"Y"}, - Sources: cli.EnvVars("DBX_NON_INTERACTIVE"), + Value: cfg.NonInteractive, Usage: "non-interactive, pull images without asking", }, &cli.StringFlag{ @@ -83,7 +89,7 @@ of the same environment.`, &cli.StringFlag{ Name: "home", Aliases: []string{"H"}, - Sources: cli.EnvVars("DBX_CONTAINER_CUSTOM_HOME"), + Value: cfg.ContainerCustomHome, Usage: "select a custom HOME directory for the container. Useful to avoid host's home littering with temp files.", }, &cli.StringSliceFlag{ @@ -191,18 +197,13 @@ func createAction(ctx context.Context, cmd *cli.Command, cfg *config.Values) err return errors.New("container manager not found in context") } - // DBX_CONTAINER_GENERATE_ENTRY=0 disables entry generation (shell parity); - // DBX_CONTAINER_HOME_PREFIX seeds a per-box custom home when no --home is set. - generateEntry := !cmd.Bool("no-entry") - if v := os.Getenv("DBX_CONTAINER_GENERATE_ENTRY"); v == "0" || v == "false" { - generateEntry = false - } + generateEntry := cfg.GenerateEntry && !cmd.Bool("no-entry") opts := commands.CreateOptions{ ContainerImage: cmd.String("image"), ContainerName: cmd.String("name"), ContainerHostname: cmd.String("hostname"), - ContainerHomePrefix: os.Getenv("DBX_CONTAINER_HOME_PREFIX"), + ContainerHomePrefix: cfg.ContainerHomePrefix, ContainerClone: cmd.String("clone"), UnshareNetNs: cmd.Bool("unshare-netns") || cmd.Bool("unshare-all"), UnshareDevsys: cmd.Bool("unshare-devsys") || cmd.Bool("unshare-all"), diff --git a/internal/cli/enter.go b/internal/cli/enter.go index 5b155a11..d371e735 100644 --- a/internal/cli/enter.go +++ b/internal/cli/enter.go @@ -23,7 +23,7 @@ func newEnterCommand(cfg *config.Values) *cli.Command { &cli.StringFlag{ Name: "name", Aliases: []string{"n"}, - Sources: cli.EnvVars("DBX_CONTAINER_NAME"), + Value: cfg.ContainerName, Usage: "name for the distrobox", }, &cli.BoolFlag{ @@ -41,7 +41,7 @@ func newEnterCommand(cfg *config.Values) *cli.Command { &cli.BoolFlag{ Name: "clean-path", Aliases: []string{"c"}, - Sources: cli.EnvVars("DBX_CONTAINER_CLEAN_PATH"), + Value: cfg.CleanPath, Usage: "reset PATH inside the container to FHS standard", }, &cli.StringFlag{ @@ -52,7 +52,7 @@ func newEnterCommand(cfg *config.Values) *cli.Command { &cli.BoolFlag{ Name: "yes", Aliases: []string{"y"}, - Sources: cli.EnvVars("DBX_NON_INTERACTIVE"), + Value: cfg.NonInteractive, Usage: "non-interactive, do not ask questions", }, &cli.BoolFlag{ @@ -63,7 +63,7 @@ func newEnterCommand(cfg *config.Values) *cli.Command { &cli.BoolFlag{ Name: "no-workdir", Aliases: []string{"nw"}, - Sources: cli.EnvVars("DBX_SKIP_WORKDIR"), + Value: cfg.SkipWorkDir, Usage: "always start the container from container's home directory", }, }, diff --git a/internal/cli/rm.go b/internal/cli/rm.go index 01ef20cd..21b1410d 100644 --- a/internal/cli/rm.go +++ b/internal/cli/rm.go @@ -33,13 +33,13 @@ func newRmCommand(cfg *config.Values) *cli.Command { &cli.BoolFlag{ Name: "yes", Aliases: []string{"Y"}, - Sources: cli.EnvVars("DBX_NON_INTERACTIVE"), + Value: cfg.NonInteractive, Usage: "non-interactive mode", }, &cli.BoolFlag{ - Name: "rm-home", - Sources: cli.EnvVars("DBX_CONTAINER_RM_CUSTOM_HOME"), - Usage: "Remove container's home directory", + Name: "rm-home", + Value: cfg.RmCustomHome, + Usage: "Remove container's home directory", }, }, diff --git a/internal/cli/root.go b/internal/cli/root.go index b8f8070e..37fa2466 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -268,6 +268,7 @@ func withContainerManager(cfg *config.Values, cmd *cli.Command) *cli.Command { c.String("sudo-command"), c.Bool("verbose"), c.Bool("root") || os.Getuid() == 0, + cfg.UsernsNoLimit, ) if err != nil { return nil, err @@ -283,6 +284,7 @@ func buildContainerManager( sudoCommand string, verbose bool, root bool, + usernsNoLimit bool, ) (containermanager.ContainerManager, error) { errPrinter := ui.NewPrinter(os.Stderr, true) @@ -290,11 +292,11 @@ func buildContainerManager( case "docker": return providers.NewDocker(root, sudoCommand, verbose), nil case "podman": - return providers.NewPodman(root, sudoCommand, verbose), nil + return providers.NewPodman(root, sudoCommand, verbose, usernsNoLimit), nil case "podman-launcher": - return providers.NewPodmanLauncher(root, sudoCommand, verbose), nil + return providers.NewPodmanLauncher(root, sudoCommand, verbose, usernsNoLimit), nil case "autodetect", "": - cm, err := providers.NewAutoDetect(root, sudoCommand, verbose) + cm, err := providers.NewAutoDetect(root, sudoCommand, verbose, usernsNoLimit) if err != nil { if errors.Is(err, providers.ErrNoContainerManager) { printMissingContainerManager(errPrinter) diff --git a/internal/cli/stop.go b/internal/cli/stop.go index e291168c..b43c6ff3 100644 --- a/internal/cli/stop.go +++ b/internal/cli/stop.go @@ -35,7 +35,7 @@ Examples: &cli.BoolFlag{ Name: "yes", Aliases: []string{"Y"}, - Sources: cli.EnvVars("DBX_NON_INTERACTIVE"), + Value: cfg.NonInteractive, Usage: "non-interactive, stop without asking", }, }, @@ -55,12 +55,13 @@ func stopAction(ctx context.Context, cmd *cli.Command, cfg *config.Values) error nonInteractive := cmd.Bool("yes") containerNames := cmd.Args().Slice() - // Shell distrobox-stop seeds container_name from DBX_CONTAINER_NAME - // (distrobox-stop:90, 200-202) when no positional and not --all. - if !all && len(containerNames) == 0 { - if envName := os.Getenv("DBX_CONTAINER_NAME"); envName != "" { - containerNames = []string{envName} - } + // Mirror shell distrobox-stop:90,200-202: when no positional and not --all, + // fall back to the env-set container name. The env value comes from + // cfg.ContainerName (resolved by pkg/config from DBX_CONTAINER_NAME); we + // only consult it here, never read the env directly. An unset env leaves + // containerNames empty so StopCommand applies its default-name fallback. + if !all && len(containerNames) == 0 && cfg.ContainerName != "" { + containerNames = []string{cfg.ContainerName} } options := &commands.StopOptions{ diff --git a/pkg/config/config.go b/pkg/config/config.go index d49155b7..40bd67da 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "gopkg.in/ini.v1" ) @@ -14,6 +15,22 @@ type Values struct { Verbose bool DefaultContainerImage string DefaultContainerName string + + // Env-set values; empty when the corresponding DBX_* var is not exported. + // Consumers prefer these over the Default* counterparts when non-empty, + // matching the shell's `[ -n "$DBX_X" ] && var=$DBX_X` pattern. + ContainerImage string + ContainerName string + ContainerHostname string + ContainerCustomHome string + ContainerHomePrefix string + ContainerAlwaysPull bool + NonInteractive bool + GenerateEntry bool + CleanPath bool + SkipWorkDir bool + UsernsNoLimit bool + RmCustomHome bool } func defaultsMap() map[string]string { @@ -22,8 +39,15 @@ func defaultsMap() map[string]string { "sudo_program": "sudo", "verbose": "false", // container_image Fedora toolbox is a sensitive default - "container_image": "registry.fedoraproject.org/fedora-toolbox:latest", - "container_name": "my-distrobox", + "container_image": "registry.fedoraproject.org/fedora-toolbox:latest", + "container_name": "my-distrobox", + "container_always_pull": "false", + "non_interactive": "false", + "container_generate_entry": "true", + "container_clean_path": "false", + "container_skip_workdir": "false", + "userns_nolimit": "false", + "rm_home": "false", } } @@ -60,11 +84,32 @@ func toStruct(configMap map[string]string) *Values { Verbose: toBool(configMap["verbose"]), DefaultContainerImage: configMap["container_image"], DefaultContainerName: configMap["container_name"], + ContainerImage: configMap["container_image_env"], + ContainerName: configMap["container_name_env"], + ContainerHostname: configMap["container_hostname"], + ContainerCustomHome: configMap["container_user_custom_home"], + ContainerHomePrefix: configMap["container_home_prefix"], + ContainerAlwaysPull: toBool(configMap["container_always_pull"]), + NonInteractive: toBool(configMap["non_interactive"]), + GenerateEntry: toBool(configMap["container_generate_entry"]), + CleanPath: toBool(configMap["container_clean_path"]), + SkipWorkDir: toBool(configMap["container_skip_workdir"]), + UsernsNoLimit: toBool(configMap["userns_nolimit"]), + RmCustomHome: toBool(configMap["rm_home"]), } } +// toBool recognizes the values the shell distrobox treats as truthy +// (`1`, `true`, `yes`, `on`, case-insensitive). Anything else, including the +// empty string, is false. The previous narrower `value == "true"` silently +// ignored `DBX_VERBOSE=1` and similar — this fixes it. func toBool(value string) bool { - return value == "true" + switch strings.ToLower(strings.TrimSpace(value)) { + case "true", "1", "yes", "on": + return true + default: + return false + } } // getConfigFilePaths returns a list of configuration file paths in order of priority. @@ -157,6 +202,42 @@ func readEnv() map[string]string { if value, exists := os.LookupEnv("DBX_VERBOSE"); exists { envConfig["verbose"] = value } + if value, exists := os.LookupEnv("DBX_CONTAINER_IMAGE"); exists { + envConfig["container_image_env"] = value + } + if value, exists := os.LookupEnv("DBX_CONTAINER_NAME"); exists { + envConfig["container_name_env"] = value + } + if value, exists := os.LookupEnv("DBX_CONTAINER_HOSTNAME"); exists { + envConfig["container_hostname"] = value + } + if value, exists := os.LookupEnv("DBX_CONTAINER_CUSTOM_HOME"); exists { + envConfig["container_user_custom_home"] = value + } + if value, exists := os.LookupEnv("DBX_CONTAINER_HOME_PREFIX"); exists { + envConfig["container_home_prefix"] = value + } + if value, exists := os.LookupEnv("DBX_CONTAINER_ALWAYS_PULL"); exists { + envConfig["container_always_pull"] = value + } + if value, exists := os.LookupEnv("DBX_NON_INTERACTIVE"); exists { + envConfig["non_interactive"] = value + } + if value, exists := os.LookupEnv("DBX_CONTAINER_GENERATE_ENTRY"); exists { + envConfig["container_generate_entry"] = value + } + if value, exists := os.LookupEnv("DBX_CONTAINER_CLEAN_PATH"); exists { + envConfig["container_clean_path"] = value + } + if value, exists := os.LookupEnv("DBX_SKIP_WORKDIR"); exists { + envConfig["container_skip_workdir"] = value + } + if value, exists := os.LookupEnv("DBX_USERNS_NOLIMIT"); exists { + envConfig["userns_nolimit"] = value + } + if value, exists := os.LookupEnv("DBX_CONTAINER_RM_CUSTOM_HOME"); exists { + envConfig["rm_home"] = value + } return envConfig } diff --git a/pkg/config/config_internal_test.go b/pkg/config/config_internal_test.go index 5c1824bf..7a5b59df 100644 --- a/pkg/config/config_internal_test.go +++ b/pkg/config/config_internal_test.go @@ -11,11 +11,23 @@ import ( func TestToStruct(t *testing.T) { input := map[string]string{ - "container_manager": "docker", - "sudo_program": "doas", - "verbose": "true", - "container_image": "ubuntu:latest", - "container_name": "mybox", + "container_manager": "docker", + "sudo_program": "doas", + "verbose": "true", + "container_image": "ubuntu:latest", + "container_name": "mybox", + "container_image_env": "alpine:3.21", + "container_name_env": "envbox", + "container_hostname": "myhost", + "container_user_custom_home": "/tmp/home", + "container_home_prefix": "/data/boxes", + "container_always_pull": "1", + "non_interactive": "yes", + "container_generate_entry": "false", + "container_clean_path": "on", + "container_skip_workdir": "true", + "userns_nolimit": "1", + "rm_home": "yes", } cfg := toStruct(input) @@ -23,8 +35,23 @@ func TestToStruct(t *testing.T) { assert.Equal(t, "docker", cfg.ContainerManagerType) assert.Equal(t, "doas", cfg.SudoProgram) assert.True(t, cfg.Verbose) + // Default* keep distrobox.conf semantics; *_env keys are env-only and + // stay separate so the create command can distinguish "user has set X" + // from "X is the hardcoded default". assert.Equal(t, "ubuntu:latest", cfg.DefaultContainerImage) assert.Equal(t, "mybox", cfg.DefaultContainerName) + assert.Equal(t, "alpine:3.21", cfg.ContainerImage) + assert.Equal(t, "envbox", cfg.ContainerName) + assert.Equal(t, "myhost", cfg.ContainerHostname) + assert.Equal(t, "/tmp/home", cfg.ContainerCustomHome) + assert.Equal(t, "/data/boxes", cfg.ContainerHomePrefix) + assert.True(t, cfg.ContainerAlwaysPull) + assert.True(t, cfg.NonInteractive) + assert.False(t, cfg.GenerateEntry) + assert.True(t, cfg.CleanPath) + assert.True(t, cfg.SkipWorkDir) + assert.True(t, cfg.UsernsNoLimit) + assert.True(t, cfg.RmCustomHome) } func TestToStruct_MissingKeys(t *testing.T) { @@ -35,6 +62,33 @@ func TestToStruct_MissingKeys(t *testing.T) { assert.False(t, cfg.Verbose) assert.Empty(t, cfg.DefaultContainerImage) assert.Empty(t, cfg.DefaultContainerName) + assert.Empty(t, cfg.ContainerImage) + assert.Empty(t, cfg.ContainerName) + assert.Empty(t, cfg.ContainerHostname) + assert.Empty(t, cfg.ContainerCustomHome) + assert.Empty(t, cfg.ContainerHomePrefix) + assert.False(t, cfg.ContainerAlwaysPull) + assert.False(t, cfg.NonInteractive) + assert.False(t, cfg.GenerateEntry) + assert.False(t, cfg.CleanPath) + assert.False(t, cfg.SkipWorkDir) + assert.False(t, cfg.UsernsNoLimit) + assert.False(t, cfg.RmCustomHome) +} + +// toBool must accept every truthy form the shell does (`1`, `true`, `yes`, +// `on`) — case-insensitive, surrounding whitespace ignored — and reject +// everything else, including the empty string. Anchors the +// `DBX_VERBOSE=1`/`DBX_USERNS_NOLIMIT=1` fix-in-passing. +func TestToBool(t *testing.T) { + truthy := []string{"1", "true", "TRUE", "True", "yes", "Yes", "on", "ON", " 1 ", "\ttrue\n"} + for _, v := range truthy { + assert.Truef(t, toBool(v), "toBool(%q) should be true", v) + } + falsy := []string{"", "0", "false", "FALSE", "no", "off", "nope", "garbage"} + for _, v := range falsy { + assert.Falsef(t, toBool(v), "toBool(%q) should be false", v) + } } func TestMergeConfigMaps(t *testing.T) { diff --git a/pkg/containermanager/providers/autodetect.go b/pkg/containermanager/providers/autodetect.go index b755641e..707e6051 100644 --- a/pkg/containermanager/providers/autodetect.go +++ b/pkg/containermanager/providers/autodetect.go @@ -12,12 +12,12 @@ var ErrNoContainerManager = errors.New("no container manager found") // NewAutoDetect returns a ContainerManager for the first available container runtime. // Priority order: podman > podman-launcher > docker. -func NewAutoDetect(root bool, sudoCommand string, verbose bool) (containermanager.ContainerManager, error) { +func NewAutoDetect(root bool, sudoCommand string, verbose, usernsNoLimit bool) (containermanager.ContainerManager, error) { if _, err := exec.LookPath("podman"); err == nil { - return NewPodman(root, sudoCommand, verbose), nil + return NewPodman(root, sudoCommand, verbose, usernsNoLimit), nil } if _, err := exec.LookPath("podman-launcher"); err == nil { - return NewPodmanLauncher(root, sudoCommand, verbose), nil + return NewPodmanLauncher(root, sudoCommand, verbose, usernsNoLimit), nil } if _, err := exec.LookPath("docker"); err == nil { return NewDocker(root, sudoCommand, verbose), nil diff --git a/pkg/containermanager/providers/autodetect_test.go b/pkg/containermanager/providers/autodetect_test.go index 552a8297..2676eec1 100644 --- a/pkg/containermanager/providers/autodetect_test.go +++ b/pkg/containermanager/providers/autodetect_test.go @@ -54,7 +54,7 @@ func TestNewAutoDetect(t *testing.T) { } t.Setenv("PATH", dir) - cm, err := providers.NewAutoDetect(false, "sudo", false) + cm, err := providers.NewAutoDetect(false, "sudo", false, false) if tt.expectError { require.Error(t, err) assert.Nil(t, cm) diff --git a/pkg/containermanager/providers/clone_internal_test.go b/pkg/containermanager/providers/clone_internal_test.go index 5333792e..d6a1cde3 100644 --- a/pkg/containermanager/providers/clone_internal_test.go +++ b/pkg/containermanager/providers/clone_internal_test.go @@ -8,7 +8,7 @@ import ( ) func TestPodman_CloneAsRoot_PreservesFieldsAndFlipsRoot(t *testing.T) { - original := newPodman(podmanCommandPodman, false, "doas", true) + original := newPodman(podmanCommandPodman, false, "doas", true, true) cloned := original.CloneAsRoot() @@ -21,12 +21,13 @@ func TestPodman_CloneAsRoot_PreservesFieldsAndFlipsRoot(t *testing.T) { assert.Equal(t, original.command, clone.command) assert.Equal(t, original.sudoCommand, clone.sudoCommand) assert.Equal(t, original.verbose, clone.verbose) + assert.Equal(t, original.usernsNoLimit, clone.usernsNoLimit, "CloneAsRoot must preserve usernsNoLimit") assert.False(t, original.root, "original should remain non-root") } func TestPodman_CloneAsRoot_AlreadyRootStillReturnsCopy(t *testing.T) { - original := newPodman(podmanCommandLauncher, true, "sudo", false) + original := newPodman(podmanCommandLauncher, true, "sudo", false, false) cloned := original.CloneAsRoot() diff --git a/pkg/containermanager/providers/podman.go b/pkg/containermanager/providers/podman.go index 8ed6ec3a..14218ce5 100644 --- a/pkg/containermanager/providers/podman.go +++ b/pkg/containermanager/providers/podman.go @@ -21,10 +21,11 @@ import ( ) type Podman struct { - command podmanCommand - root bool - sudoCommand string - verbose bool + command podmanCommand + root bool + sudoCommand string + verbose bool + usernsNoLimit bool } // podmanCommand represents the executable name for the Podman provider. @@ -37,21 +38,22 @@ const ( var _ containermanager.ContainerManager = &Podman{} -func newPodman(command podmanCommand, root bool, sudoCommand string, verbose bool) *Podman { +func newPodman(command podmanCommand, root bool, sudoCommand string, verbose, usernsNoLimit bool) *Podman { return &Podman{ - command: command, - sudoCommand: sudoCommand, - root: root, - verbose: verbose, + command: command, + sudoCommand: sudoCommand, + root: root, + verbose: verbose, + usernsNoLimit: usernsNoLimit, } } -func NewPodman(root bool, sudoCommand string, verbose bool) *Podman { - return newPodman(podmanCommandPodman, root, sudoCommand, verbose) +func NewPodman(root bool, sudoCommand string, verbose, usernsNoLimit bool) *Podman { + return newPodman(podmanCommandPodman, root, sudoCommand, verbose, usernsNoLimit) } -func NewPodmanLauncher(root bool, sudoCommand string, verbose bool) *Podman { - return newPodman(podmanCommandLauncher, root, sudoCommand, verbose) +func NewPodmanLauncher(root bool, sudoCommand string, verbose, usernsNoLimit bool) *Podman { + return newPodman(podmanCommandLauncher, root, sudoCommand, verbose, usernsNoLimit) } func (p *Podman) CloneAsRoot() containermanager.ContainerManager { @@ -408,11 +410,11 @@ func (p *Podman) makeCreateCommand( options = append(options, "--systemd=always") } - // Use keep-id only if going rootless. DBX_USERNS_NOLIMIT (set to a non-zero - // value) drops the :size cap, matching the shell (distrobox-create:962-977). + // Use keep-id only if going rootless. The usernsNoLimit struct field + // (resolved from cfg.UsernsNoLimit / DBX_USERNS_NOLIMIT in the config + // layer) drops the :size cap, matching the shell (distrobox-create:962-977). if !p.root { - usernsNoLimit := usernsNoLimitEnabled() - if !usernsNoLimit && (dryRun || p.supportsKeepIDSize(ctx, containerImage)) { + if !p.usernsNoLimit && (dryRun || p.supportsKeepIDSize(ctx, containerImage)) { options = append(options, "--userns", "keep-id:size=65536") } else { options = append(options, "--userns", "keep-id") @@ -682,14 +684,6 @@ func commandExists(cmd string) bool { return err == nil } -// usernsNoLimitEnabled reports whether DBX_USERNS_NOLIMIT requests dropping the -// keep-id:size cap (any non-empty, non-"0" value), mirroring the shell's -// userns_nolimit (distrobox-create:159,962-977). -func usernsNoLimitEnabled() bool { - v := os.Getenv("DBX_USERNS_NOLIMIT") - return v != "" && v != "0" && v != "false" -} - // supportsKeepIDSize tests whether podman supports the keep-id:size= userns option // by attempting a quick container run. Older podman versions do not support the size suboption. func (p *Podman) supportsKeepIDSize(ctx context.Context, image string) bool { diff --git a/pkg/containermanager/providers/podman_internal_test.go b/pkg/containermanager/providers/podman_internal_test.go index 1a28aa21..21785bcb 100644 --- a/pkg/containermanager/providers/podman_internal_test.go +++ b/pkg/containermanager/providers/podman_internal_test.go @@ -16,21 +16,11 @@ import ( "github.com/89luca89/distrobox/pkg/ui" ) -// DBX_USERNS_NOLIMIT (non-zero) drops the keep-id:size cap. -func TestUsernsNoLimitEnabled(t *testing.T) { - cases := map[string]bool{"": false, "0": false, "false": false, "1": true, "true": true, "yes": true} - for v, want := range cases { - t.Setenv("DBX_USERNS_NOLIMIT", v) - if got := usernsNoLimitEnabled(); got != want { - t.Errorf("DBX_USERNS_NOLIMIT=%q: got %v, want %v", v, got, want) - } - } -} - -// with DBX_USERNS_NOLIMIT set, rootless create uses plain keep-id (no :size). +// with usernsNoLimit set, rootless create uses plain keep-id (no :size). +// The env var (DBX_USERNS_NOLIMIT) is resolved by pkg/config and surfaces +// here only as the constructor argument. func TestPodman_makeCreateCommandUsernsNoLimit(t *testing.T) { - t.Setenv("DBX_USERNS_NOLIMIT", "1") - podman := NewPodman(false, "sudo", false) + podman := NewPodman(false, "sudo", false, true) userEnv := &userenv.UserEnvironment{User: "user", UserID: "1000", GroupID: "1000", Home: "/home/user", Shell: "/bin/sh"} cmd := podman.makeCreateCommand( @@ -51,7 +41,7 @@ func TestPodman_makeCreateCommandUsernsNoLimit(t *testing.T) { } func TestPodman_makeCreateCommand(t *testing.T) { - podman := NewPodman(false, "sudo", false) + podman := NewPodman(false, "sudo", false, false) userEnv := &userenv.UserEnvironment{ User: "user", @@ -137,7 +127,7 @@ func TestPodman_makeCreateCommand(t *testing.T) { func TestPodman_makeCreateCommandRootful(t *testing.T) { // Test rootful mode - should NOT have --userns keep-id - podman := NewPodman(true, "sudo", false) + podman := NewPodman(true, "sudo", false, false) userEnv := &userenv.UserEnvironment{ User: "user", @@ -187,7 +177,7 @@ func TestPodman_makeCreateCommandRootful(t *testing.T) { func TestPodman_makeCreateCommandNoInit(t *testing.T) { // Test without init - should NOT have --systemd=always - podman := NewPodman(false, "sudo", false) + podman := NewPodman(false, "sudo", false, false) userEnv := &userenv.UserEnvironment{ User: "user", @@ -237,7 +227,7 @@ func TestPodman_makeCreateCommandWithCrun(t *testing.T) { // This test checks that if crun exists, --runtime=crun is added // Note: This test will pass or fail depending on whether crun is installed // on the test system. In a real scenario, you might want to mock commandExists() - podman := NewPodman(false, "sudo", false) + podman := NewPodman(false, "sudo", false, false) userEnv := &userenv.UserEnvironment{ User: "user", @@ -285,7 +275,7 @@ func TestPodman_makeCreateCommandWithCrun(t *testing.T) { } func TestPodman_makeCreateCommandWithPlatform(t *testing.T) { - podman := NewPodman(false, "sudo", false) + podman := NewPodman(false, "sudo", false, false) userEnv := &userenv.UserEnvironment{ User: "user", @@ -328,7 +318,7 @@ func TestPodman_makeCreateCommandWithPlatform(t *testing.T) { } func TestPodman_makeCreateCommandWithCustomHome(t *testing.T) { - podman := NewPodman(false, "sudo", false) + podman := NewPodman(false, "sudo", false, false) userEnv := &userenv.UserEnvironment{ User: "user", @@ -381,7 +371,7 @@ func TestPodman_makeCreateCommandWithCustomHome(t *testing.T) { } func TestPodman_makeCreateCommandWithAdditionalFlags(t *testing.T) { - podman := NewPodman(false, "sudo", false) + podman := NewPodman(false, "sudo", false, false) userEnv := &userenv.UserEnvironment{ User: "user", @@ -430,7 +420,7 @@ func TestPodman_makeCreateCommandWithAdditionalFlags(t *testing.T) { } func TestPodman_makeCreateCommandWithAdditionalPackages(t *testing.T) { - podman := NewPodman(false, "sudo", false) + podman := NewPodman(false, "sudo", false, false) userEnv := &userenv.UserEnvironment{ User: "user", @@ -537,7 +527,7 @@ func TestPodman_makeCreateCommandUnshareOptions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - podman := NewPodman(false, "sudo", false) + podman := NewPodman(false, "sudo", false, false) userEnv := &userenv.UserEnvironment{ User: "user", @@ -598,10 +588,10 @@ func TestPodman_makeCreateCommandUnshareOptions(t *testing.T) { } func TestPodman_Name(t *testing.T) { - podman := NewPodman(false, "sudo", false) + podman := NewPodman(false, "sudo", false, false) assert.Equal(t, "podman", podman.Name()) - launcher := NewPodmanLauncher(false, "sudo", false) + launcher := NewPodmanLauncher(false, "sudo", false, false) assert.Equal(t, "podman-launcher", launcher.Name()) } @@ -678,12 +668,12 @@ func TestPodman_runUsesCorrectBinary(t *testing.T) { }{ { name: "NewPodman uses podman binary", - constructor: func() *Podman { return NewPodman(false, "sudo", false) }, + constructor: func() *Podman { return NewPodman(false, "sudo", false, false) }, expectedPrefix: "podman ", }, { name: "NewPodmanLauncher uses podman-launcher binary", - constructor: func() *Podman { return NewPodmanLauncher(false, "sudo", false) }, + constructor: func() *Podman { return NewPodmanLauncher(false, "sudo", false, false) }, expectedPrefix: "podman-launcher ", }, } @@ -728,7 +718,7 @@ func TestPodmanEnterPropagatesStartError(t *testing.T) { t.Setenv("FAKE_START_EXIT", "9") t.Setenv("FAKE_START_STDERR", "start failed") - err := NewPodman(false, "sudo", false).Enter( + err := NewPodman(false, "sudo", false, false).Enter( t.Context(), containermanager.EnterOptions{ ContainerName: "box", @@ -749,7 +739,7 @@ func TestPodmanEnterPropagatesExecError(t *testing.T) { t.Setenv("FAKE_EXEC_EXIT", "7") t.Setenv("FAKE_EXEC_STDERR", "exec failed") - err := NewPodman(false, "sudo", false).Enter( + err := NewPodman(false, "sudo", false, false).Enter( t.Context(), containermanager.EnterOptions{ ContainerName: "box",