mirror of
https://github.com/89luca89/distrobox.git
synced 2026-08-17 16:34:42 -05:00
refactor(config): centralize DBX_* env-var resolution
Env vars were read in three layers — cli.EnvVars on flags, os.Getenv in CLI actions, and the podman provider — leaving library users no single override point. Funnelling every DBX_* through pkg/config gives consumers one injection point, lets the container manager receive its configuration instead of sniffing for it, and fixes the create flags whose Usage advertised a default the env-set value was already shadowing. Signed-off-by: Luca Di Maio <luca.dimaio1@gmail.com>
This commit is contained in:
+25
-24
@@ -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"),
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
|
||||
+4
-4
@@ -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",
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
+84
-3
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user