mirror of
https://github.com/89luca89/distrobox.git
synced 2026-08-17 16:34:42 -05:00
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>
328 lines
11 KiB
Go
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)
|
|
}
|