mirror of
https://github.com/89luca89/distrobox.git
synced 2026-11-02 20:30:16 -06:00
324 lines
10 KiB
Go
324 lines
10 KiB
Go
package commands
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/89luca89/distrobox/pkg/config"
|
|
"github.com/89luca89/distrobox/pkg/containermanager"
|
|
"github.com/89luca89/distrobox/pkg/ui"
|
|
)
|
|
|
|
const (
|
|
maxHostnameLength = 64
|
|
)
|
|
|
|
var ErrHostnameTooLong = fmt.Errorf("hostname too long, must be less than %d characters", maxHostnameLength)
|
|
var ErrImagePullAbortedByUser = errors.New("image pull operation aborted by user")
|
|
|
|
type ContainerAlreadyExistsError struct {
|
|
ContainerName string
|
|
}
|
|
|
|
func (e *ContainerAlreadyExistsError) Error() string {
|
|
return fmt.Sprintf("container named '%s' already exists", e.ContainerName)
|
|
}
|
|
|
|
type CreateCommand struct {
|
|
cfg *config.Values
|
|
containerManager containermanager.ContainerManager
|
|
generateEntryCmd *GenerateEntryCommand
|
|
progress *ui.Progress
|
|
prompter *ui.Prompter
|
|
}
|
|
|
|
type CreateOptions struct {
|
|
// ContainerClone name of the distrobox container to use as base for a new container
|
|
ContainerClone string
|
|
// ContainerImage image to use for the container
|
|
ContainerImage string
|
|
// ContainerName name of the distrobox
|
|
ContainerName string
|
|
// ContainerHostname hostname to set inside the container
|
|
ContainerHostname string
|
|
// ContainerPlatform platform to use for the container (e.g., linux/amd64, linux/arm64)
|
|
ContainerPlatform string
|
|
Nopasswd bool
|
|
|
|
// UnshareNetNs if true, do not share host network namespace
|
|
UnshareNetNs bool
|
|
// UnshareDevsys if true, do not share host devices and sysfs dirs from host
|
|
UnshareDevsys bool
|
|
// UnshareGroups if true, do not forward user's additional groups into the container
|
|
UnshareGroups bool
|
|
// UnshareIpc if true, do not share host IPC namespace
|
|
UnshareIpc bool
|
|
// UnshareProcess if true, do not share host process namespace
|
|
UnshareProcess bool
|
|
|
|
AdditionalFlags []string
|
|
AdditionalVolumes []string
|
|
AdditionalPackages []string
|
|
ContainerPreInitHook string
|
|
ContainerInitHook string
|
|
|
|
ContainerUserCustomHome string
|
|
ContainerHomePrefix string
|
|
Init bool
|
|
|
|
Nvidia bool
|
|
DryRun bool
|
|
|
|
GenerateEntry bool
|
|
Rootful bool
|
|
|
|
ContainerAlwaysPull bool
|
|
NonInteractive bool
|
|
}
|
|
|
|
type CreateResult struct {
|
|
ContainerName string
|
|
ContainerImage string
|
|
ContainerHostname string
|
|
}
|
|
|
|
func NewCreateCommand(cfg *config.Values, cm containermanager.ContainerManager, progress *ui.Progress, prompter *ui.Prompter) *CreateCommand {
|
|
return &CreateCommand{
|
|
cfg: cfg,
|
|
containerManager: cm,
|
|
generateEntryCmd: NewGenerateEntryCommand(cfg, NewListCommand(cfg, cm)),
|
|
progress: progress,
|
|
prompter: prompter,
|
|
}
|
|
}
|
|
|
|
func (c *CreateCommand) Execute(ctx context.Context, opts CreateOptions) (*CreateResult, error) {
|
|
containerImage := c.makeContainerImage(&opts)
|
|
containerName := c.makeContainerName(&opts, containerImage)
|
|
containerHostname, err := c.makeContainerHostname(&opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
containerUserCustomHome := c.makeContainerUserCustomHome(&opts, containerName)
|
|
|
|
if !opts.DryRun && c.containerManager.Exists(ctx, containerName) {
|
|
return nil, &ContainerAlreadyExistsError{ContainerName: containerName}
|
|
}
|
|
|
|
if opts.ContainerClone != "" && !opts.DryRun {
|
|
cloneImage, err := c.clone(ctx, opts.ContainerClone)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to clone container %s: %w", opts.ContainerClone, err)
|
|
}
|
|
containerImage = cloneImage
|
|
}
|
|
|
|
if err := c.askPullImage(ctx, containerImage, opts); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c.progress.Next("Creating '%s' using image %s", containerName, containerImage)
|
|
|
|
err = c.containerManager.Create(
|
|
ctx,
|
|
containermanager.CreateOptions{
|
|
ContainerName: containerName,
|
|
ContainerImage: containerImage,
|
|
ContainerClone: opts.ContainerClone,
|
|
ContainerUserCustomHome: containerUserCustomHome,
|
|
ContainerHostname: containerHostname,
|
|
ContainerPlatform: opts.ContainerPlatform,
|
|
Nopasswd: opts.Nopasswd,
|
|
UnshareDevsys: opts.UnshareDevsys,
|
|
UnshareGroups: opts.UnshareGroups,
|
|
UnshareIPC: opts.UnshareIpc,
|
|
UnshareNetNS: opts.UnshareNetNs,
|
|
UnshareProcess: opts.UnshareProcess,
|
|
AdditionalFlags: splitFields(opts.AdditionalFlags),
|
|
AdditionalVolumes: opts.AdditionalVolumes,
|
|
AdditionalPackages: opts.AdditionalPackages,
|
|
ContainerPreInitHook: opts.ContainerPreInitHook,
|
|
ContainerInitHook: opts.ContainerInitHook,
|
|
Init: opts.Init,
|
|
Nvidia: opts.Nvidia,
|
|
DryRun: opts.DryRun,
|
|
},
|
|
)
|
|
|
|
if err != nil {
|
|
c.progress.Fail()
|
|
return nil, fmt.Errorf("failed to create container: %w", err)
|
|
}
|
|
|
|
c.progress.Done()
|
|
|
|
if opts.GenerateEntry && !opts.DryRun && !opts.Rootful {
|
|
err := c.generateEntryCmd.Execute(
|
|
ctx,
|
|
&GenerateEntryOptions{
|
|
ContainerName: containerName,
|
|
// "auto" triggers distro icon detection; without it the entry
|
|
// would get an empty Icon=.
|
|
Icon: "auto",
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate entry for container %s: %w", containerName, err)
|
|
}
|
|
}
|
|
|
|
return &CreateResult{
|
|
ContainerName: containerName,
|
|
ContainerImage: containerImage,
|
|
ContainerHostname: containerHostname,
|
|
}, nil
|
|
}
|
|
|
|
// Determine right containerImage to use
|
|
//
|
|
// If no clone option and no container image, let's choose a default image to use.
|
|
//
|
|
// If no name is specified and we're using the default container_image, then let's
|
|
// set a default name for the container, that is distinguishable from the default
|
|
// toolbx one. This will avoid problems when using both toolbx and distrobox on
|
|
// the same system.
|
|
func (c *CreateCommand) makeContainerImage(opts *CreateOptions) string {
|
|
containerImage := opts.ContainerImage
|
|
if opts.ContainerClone == "" && containerImage == "" {
|
|
containerImage = c.cfg.DefaultContainerImage
|
|
}
|
|
if opts.DryRun && opts.ContainerClone != "" {
|
|
containerImage = opts.ContainerClone
|
|
}
|
|
|
|
return containerImage
|
|
}
|
|
|
|
// Determine right containerName to use
|
|
//
|
|
// If no name is specified and no image is specified, then let's
|
|
// set a default name for the container, that is distinguishable from the default
|
|
// toolbx one. This will avoid problems when using both toolbx and distrobox on
|
|
// the same system.
|
|
//
|
|
// If no container_name is declared, we build our container name starting from the
|
|
// container image specified.
|
|
//
|
|
// Examples:
|
|
//
|
|
// alpine -> alpine
|
|
// ubuntu:20.04 -> ubuntu-20.04
|
|
// registry.fedoraproject.org/fedora-toolbox:39 -> fedora-toolbox-39
|
|
// ghcr.io/void-linux/void-linux:latest-full-x86_64 -> void-linux-latest-full-x86_64
|
|
func (c *CreateCommand) makeContainerName(opts *CreateOptions, containerImage string) string {
|
|
containerName := opts.ContainerName
|
|
// Mirror the shell: when no name is given and the effective image is the
|
|
// default image, use the default name (distinguishable from toolbx).
|
|
if containerName == "" && opts.ContainerClone == "" && containerImage == c.cfg.DefaultContainerImage {
|
|
containerName = c.cfg.DefaultContainerName
|
|
}
|
|
if containerName == "" {
|
|
base := path.Base(containerImage)
|
|
base = strings.ReplaceAll(base, ":", "-")
|
|
base = strings.ReplaceAll(base, ".", "-")
|
|
containerName = base
|
|
}
|
|
|
|
return containerName
|
|
}
|
|
|
|
func (c *CreateCommand) makeContainerHostname(opts *CreateOptions) (string, error) {
|
|
containerHostname := opts.ContainerHostname
|
|
if containerHostname == "" {
|
|
hostname, err := os.Hostname()
|
|
if err != nil {
|
|
return "", fmt.Errorf("unable to get hostname: %w", err)
|
|
}
|
|
containerHostname = hostname
|
|
if opts.UnshareNetNs {
|
|
containerHostname = fmt.Sprintf("%s.%s", opts.ContainerName, hostname)
|
|
}
|
|
}
|
|
|
|
if len(containerHostname) > maxHostnameLength {
|
|
return "", ErrHostnameTooLong
|
|
}
|
|
|
|
return containerHostname, nil
|
|
}
|
|
|
|
// Determine right containerUserCustomHome to use
|
|
//
|
|
// We check if the user has a custom home prefix to use for the container home.
|
|
// If we have a home prefix to use, and no custom home set, then we set
|
|
// the custom home to be PREFIX/CONTAINER_NAME
|
|
func (c *CreateCommand) makeContainerUserCustomHome(
|
|
opts *CreateOptions,
|
|
containerName string,
|
|
) string {
|
|
containerUserCustomHome := strings.TrimRight(opts.ContainerUserCustomHome, "/")
|
|
if opts.ContainerHomePrefix != "" && containerUserCustomHome == "" {
|
|
containerUserCustomHome = filepath.Join(opts.ContainerHomePrefix, containerName)
|
|
}
|
|
return containerUserCustomHome
|
|
}
|
|
|
|
func (c *CreateCommand) clone(ctx context.Context, containerName string) (string, error) {
|
|
i, err := c.containerManager.InspectContainer(ctx, containerName)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to inspect container status: %w", err)
|
|
}
|
|
|
|
if i.ContainerStatus == "running" {
|
|
return "", errors.New("cannot clone running container, name: " + containerName)
|
|
}
|
|
|
|
commitTag := fmt.Sprintf("%s:%s", strings.ToLower(containerName), time.Now().Format("2006-01-02"))
|
|
|
|
err = c.containerManager.Commit(ctx, i.ContainerID, commitTag)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to commit container '%s:%s': %w", i.ContainerID, commitTag, err)
|
|
}
|
|
|
|
return commitTag, nil
|
|
}
|
|
|
|
// splitFields word-splits each entry of in on whitespace. A single CLI
|
|
// invocation like --additional-flags "--label=a=1 --label=b=2" arrives here as
|
|
// one slice element with embedded spaces; podman would parse that as a single
|
|
// flag value. Splitting matches the manifest parser's behavior.
|
|
func splitFields(in []string) []string {
|
|
out := make([]string, 0, len(in))
|
|
for _, v := range in {
|
|
out = append(out, strings.Fields(v)...)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (c *CreateCommand) askPullImage(ctx context.Context, containerImage string, opts CreateOptions) error {
|
|
if opts.ContainerAlwaysPull || !c.containerManager.ImageExists(ctx, containerImage) {
|
|
skipConfirm := opts.NonInteractive || opts.ContainerAlwaysPull || opts.DryRun
|
|
if !skipConfirm {
|
|
msg := fmt.Sprintf("Image '%s' not found.\n. Do you want to pull the image now?", containerImage)
|
|
answer := c.prompter.Prompt(msg, true)
|
|
if !answer {
|
|
return ErrImagePullAbortedByUser
|
|
}
|
|
}
|
|
|
|
err := c.containerManager.PullImage(ctx, containerImage, opts.ContainerPlatform, opts.DryRun)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to pull image '%s': %w", containerImage, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|