Files
Luca Di Maio 1308ae11ce chore(license): add GPL-3.0 SPDX header to Go sources (#2173)
The legacy shell tree carried a per-file license header; the Go rewrite
shipped without one. Add it to all files and enforce it going forward
via golangci-lint's goheader linter, so the notice can't be dropped.

Signed-off-by: Luca Di Maio <luca.dimaio1@gmail.com>
2026-07-24 18:08:51 +02:00

328 lines
11 KiB
Go

// SPDX-License-Identifier: GPL-3.0-only
//
// This file is part of the distrobox project:
// https://github.com/89luca89/distrobox
//
// Copyright (C) 2021 distrobox contributors
//
// distrobox is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License version 3
// as published by the Free Software Foundation.
//
// distrobox is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with distrobox; if not, see <http://www.gnu.org/licenses/>.
package cli
import (
"bufio"
"context"
"errors"
"fmt"
"os"
"github.com/urfave/cli/v3"
"github.com/89luca89/distrobox/pkg/commands"
"github.com/89luca89/distrobox/pkg/config"
"github.com/89luca89/distrobox/pkg/containermanager"
"github.com/89luca89/distrobox/pkg/ui"
)
//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",
Usage: "create a new distrobox container",
UsageText: `distrobox create [options] [container_name]
Examples:
distrobox create --image alpine:latest --name test --init-hooks "touch /var/tmp/test1 && touch /var/tmp/test2"
distrobox create --image fedora:39 --name test --additional-flags "--env MY_VAR=value"
distrobox create --image fedora:39 --name test --volume /opt/my-dir:/usr/local/my-dir:rw --additional-flags "--pids-limit 100"
distrobox create -i docker.io/almalinux/8-init --init --name test --pre-init-hooks "dnf config-manager --enable powertools && dnf -y install epel-release"
distrobox create --clone fedora-39 --name fedora-39-copy
distrobox create --image alpine my-alpine-container
distrobox create --image registry.fedoraproject.org/fedora-toolbox:latest --name fedora-toolbox-latest
distrobox create --pull --image centos:stream9 --home ~/distrobox/centos9
distrobox create --image alpine:latest --name test2 --additional-packages "git tmux vim"
distrobox create --image ubuntu:22.04 --name ubuntu-nvidia --nvidia
DBX_NON_INTERACTIVE=1 DBX_CONTAINER_NAME=test-alpine DBX_CONTAINER_IMAGE=alpine distrobox-create`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "image",
Aliases: []string{"i"},
Value: imageDefault,
Usage: fmt.Sprintf("image to use for the container (default: %s)", imageDefault),
},
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Value: nameDefault,
Usage: fmt.Sprintf("name for the distrobox (default: %s)", nameDefault),
},
&cli.StringFlag{
Name: "hostname",
Value: hostnameDefault,
Usage: fmt.Sprintf("hostname for the distrobox (default: %s)", hostnameDefault),
},
&cli.BoolFlag{
Name: "pull",
Aliases: []string{"p"},
Value: cfg.ContainerAlwaysPull,
Usage: "pull the image even if it exists locally (implies --yes)",
},
&cli.BoolFlag{
Name: "yes",
Aliases: []string{"Y"},
Value: cfg.NonInteractive,
Usage: "non-interactive, pull images without asking",
},
&cli.StringFlag{
Name: "clone",
Aliases: []string{"c"},
Usage: `name of the distrobox container to use as base for a new container
this will be useful to either rename an existing distrobox or have multiple copies
of the same environment.`,
},
&cli.StringFlag{
Name: "home",
Aliases: []string{"H"},
Value: cfg.ContainerCustomHome,
Usage: "select a custom HOME directory for the container. Useful to avoid host's home littering with temp files.",
},
&cli.StringSliceFlag{
Name: "volume",
Usage: "additional volumes to add to the container",
},
&cli.StringSliceFlag{
Name: "additional-flags",
Aliases: []string{"a"},
Usage: "additional flags to pass to the container manager command",
},
&cli.StringSliceFlag{
Name: "additional-packages",
Aliases: []string{"ap"},
Usage: "additional packages to install during initial container setup",
},
&cli.StringFlag{
Name: "init-hooks",
Usage: "additional commands to execute at the end of container initialization",
},
&cli.StringFlag{
Name: "pre-init-hooks",
Usage: "additional commands to execute at the start of container initialization",
},
&cli.BoolFlag{
Name: "init",
Aliases: []string{"I"},
Usage: `use init system (like systemd) inside the container.
this will make host's processes not visible from within the container. (assumes --unshare-process)
may require additional packages depending on the container image: https://github.com/89luca89/distrobox/blob/main/docs/useful_tips.md#using-init-system-inside-a-distrobox`,
},
&cli.BoolFlag{
Name: "nvidia",
Usage: "try to integrate host's nVidia drivers in the guest",
},
&cli.StringFlag{
Name: "platform",
Usage: "specify which platform to use, eg: linux/arm64",
},
&cli.BoolFlag{
Name: "unshare-devsys",
Usage: "do not share host devices and sysfs dirs from host",
},
&cli.BoolFlag{
Name: "unshare-groups",
Usage: "do not forward user's additional groups into the container",
},
&cli.BoolFlag{
Name: "unshare-ipc",
Usage: "do not share ipc namespace with host",
},
&cli.BoolFlag{
Name: "unshare-netns",
Usage: "do not share the net namespace with host",
},
&cli.BoolFlag{
Name: "unshare-process",
Usage: "do not share process namespace with host",
},
&cli.BoolFlag{
Name: "unshare-all",
Usage: "activate all the unshare flags below",
},
&cli.BoolFlag{
Name: "no-entry",
Usage: "do not generate a container entry in the application list",
},
&cli.BoolFlag{
Name: "dry-run",
Aliases: []string{"d"},
Usage: "only print the container manager command generated",
},
&cli.BoolFlag{
Name: "absolutely-disable-root-password-i-am-really-positively-sure",
Usage: `⚠️ ⚠️ when setting up a rootful distrobox, this will skip user password setup, leaving it blank. ⚠️ ⚠️`,
},
&cli.BoolFlag{
Name: "compatibility",
Aliases: []string{"C"},
Usage: "show compatibility information and exit",
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
return createAction(ctx, cmd, cfg)
},
}
}
func createAction(ctx context.Context, cmd *cli.Command, cfg *config.Values) error {
if cmd.Bool("compatibility") {
err := showCompatibility(ctx)
if err != nil {
return fmt.Errorf("compatibility check failed: %w", err)
}
return nil
}
containerManager, ok := ctx.Value(containerManagerKey).(containermanager.ContainerManager)
if !ok {
return errors.New("container manager not found in context")
}
generateEntry := cfg.GenerateEntry && !cmd.Bool("no-entry")
opts := commands.CreateOptions{
ContainerImage: cmd.String("image"),
ContainerName: cmd.String("name"),
ContainerHostname: cmd.String("hostname"),
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"),
UnshareGroups: cmd.Bool("unshare-groups") || cmd.Bool("unshare-all") || cmd.Bool("init"),
UnshareIpc: cmd.Bool("unshare-ipc") || cmd.Bool("unshare-all"),
UnshareProcess: cmd.Bool("unshare-process") || cmd.Bool("unshare-all") || cmd.Bool("init"),
AdditionalFlags: cmd.StringSlice("additional-flags"),
AdditionalVolumes: cmd.StringSlice("volume"),
AdditionalPackages: cmd.StringSlice("additional-packages"),
Nopasswd: cmd.Bool("absolutely-disable-root-password-i-am-really-positively-sure"),
ContainerUserCustomHome: cmd.String("home"),
Init: cmd.Bool("init"),
Nvidia: cmd.Bool("nvidia"),
ContainerInitHook: cmd.String("init-hooks"),
ContainerPreInitHook: cmd.String("pre-init-hooks"),
ContainerPlatform: cmd.String("platform"),
DryRun: cmd.Bool("dry-run"),
GenerateEntry: generateEntry,
Rootful: cmd.Bool("root"),
ContainerAlwaysPull: cmd.Bool("pull"),
NonInteractive: cmd.Bool("yes"),
}
// Setting Value: to the resolved default (so --help is honest) means
// cmd.String("name"/"hostname") returns that default even when the user
// passed nothing on the CLI. Two shell branches rely on the option being
// empty to fire — basename(image) in makeContainerName
// (distrobox-create:495-497) and the `<name>.<host>` prefix under
// --unshare-netns in makeContainerHostname (lines 503-505). When neither
// the flag nor the env var was supplied, restore "" so they still fire.
if !cmd.IsSet("name") && cfg.ContainerName == "" {
opts.ContainerName = ""
}
if !cmd.IsSet("hostname") && cfg.ContainerHostname == "" {
opts.ContainerHostname = ""
}
// Positional container_name overrides --name, matching the shell
// (distrobox-create:453-461) so `distrobox create --image alpine my-box`
// names the box `my-box`.
if positional := cmd.Args().First(); positional != "" {
opts.ContainerName = positional
}
progress := ui.NewProgress(os.Stderr)
prompter := ui.NewPrompter(*bufio.NewReader(os.Stdin), os.Stdout)
createCmd := commands.NewCreateCommand(cfg, containerManager, progress, prompter)
result, err := createCmd.Execute(ctx, opts)
var containerAlreadyExistsErr *commands.ContainerAlreadyExistsError
if errors.As(err, &containerAlreadyExistsErr) {
printContainerAlreadyExists(progress, containerAlreadyExistsErr.ContainerName, opts.Rootful)
}
if errors.Is(err, commands.ErrImagePullAbortedByUser) {
progress.Finalize("next time, pull the image first")
return nil
}
if err != nil {
return fmt.Errorf("create command failed: %w", err)
}
if !opts.DryRun {
printCreateCompleted(progress, result.ContainerName, opts.Rootful)
}
return nil
}
func printCreateCompleted(progress *ui.Progress, containerName string, rootful bool) {
rootFlag := ""
if rootful {
rootFlag = "--root "
}
msg := "Distrobox '%s' successfully created.\nTo enter, run:\n\ndistrobox enter %s%s\n\n"
progress.Finalize(msg, containerName, rootFlag, containerName)
}
// defaultHostname mirrors the shell's `default: $(uname -n)` hint shown in
// distrobox-create's --hostname help line. Evaluated once at command-build
// time; an unreachable hostname call falls back to a literal hint.
func defaultHostname() string {
if h, err := os.Hostname(); err == nil && h != "" {
return h
}
return "$(uname -n)"
}
func printContainerAlreadyExists(progress *ui.Progress, containerName string, rootful bool) {
rootFlag := ""
if rootful {
rootFlag = "--root "
}
msg := `Distrobox named '%s' already exists.
To enter, run:
distrobox enter %s%s
`
progress.Finalize(msg, containerName, rootFlag, containerName)
}