toolbox/src/cmd/create.go

1014 lines
27 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright © 2019 2025 Red Hat Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cmd
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/briandowns/spinner"
"github.com/containers/toolbox/pkg/podman"
"github.com/containers/toolbox/pkg/shell"
"github.com/containers/toolbox/pkg/skopeo"
"github.com/containers/toolbox/pkg/term"
"github.com/containers/toolbox/pkg/utils"
"github.com/docker/go-units"
"github.com/godbus/dbus/v5"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
type promptForDownloadError struct {
ImageSize string
}
const (
alpha = `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`
num = `0123456789`
alphanum = alpha + num
)
var (
createFlags struct {
authFile string
container string
distro string
image string
release string
}
createToolboxShMounts = []struct {
containerPath string
source string
}{
{"/etc/profile.d/toolbox.sh", "/etc/profile.d/toolbox.sh"},
{"/etc/profile.d/toolbox.sh", "/usr/share/profile.d/toolbox.sh"},
}
)
var createCmd = &cobra.Command{
Use: "create",
Short: "Create a new Toolbx container",
RunE: create,
ValidArgsFunction: completionEmpty,
}
func init() {
flags := createCmd.Flags()
flags.StringVar(&createFlags.authFile,
"authfile",
"",
"Path to a file with credentials for authenticating to the registry for private images")
flags.StringVarP(&createFlags.container,
"container",
"c",
"",
"Assign a different name to the Toolbx container")
flags.StringVarP(&createFlags.distro,
"distro",
"d",
"",
"Create a Toolbx container for a different operating system distribution than the host")
flags.StringVarP(&createFlags.image,
"image",
"i",
"",
"Change the name of the base image used to create the Toolbx container")
flags.StringVarP(&createFlags.release,
"release",
"r",
"",
"Create a Toolbx container for a different operating system release than the host")
createCmd.SetHelpFunc(createHelp)
if err := createCmd.RegisterFlagCompletionFunc("distro", completionDistroNames); err != nil {
panicMsg := fmt.Sprintf("failed to register flag completion function: %v", err)
panic(panicMsg)
}
if err := createCmd.RegisterFlagCompletionFunc("image", completionImageNames); err != nil {
panicMsg := fmt.Sprintf("failed to register flag completion function: %v", err)
panic(panicMsg)
}
rootCmd.AddCommand(createCmd)
}
func create(cmd *cobra.Command, args []string) error {
if utils.IsInsideContainer() {
if !utils.IsInsideToolboxContainer() {
return errors.New("this is not a Toolbx container")
}
exitCode, err := utils.ForwardToHost()
return &exitError{exitCode, err}
}
if cmd.Flag("distro").Changed && cmd.Flag("image").Changed {
var builder strings.Builder
fmt.Fprintf(&builder, "options --distro and --image cannot be used together\n")
fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase)
errMsg := builder.String()
return errors.New(errMsg)
}
if cmd.Flag("image").Changed && cmd.Flag("release").Changed {
var builder strings.Builder
fmt.Fprintf(&builder, "options --image and --release cannot be used together\n")
fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase)
errMsg := builder.String()
return errors.New(errMsg)
}
if cmd.Flag("authfile").Changed {
if !utils.PathExists(createFlags.authFile) {
var builder strings.Builder
fmt.Fprintf(&builder, "file %s not found\n", createFlags.authFile)
fmt.Fprintf(&builder, "'podman login' can be used to create the file.\n")
fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase)
errMsg := builder.String()
return errors.New(errMsg)
}
}
var container string
var containerArg string
if len(args) != 0 {
container = args[0]
containerArg = "CONTAINER"
} else if createFlags.container != "" {
container = createFlags.container
containerArg = "--container"
}
container, image, release, err := resolveContainerAndImageNames(container,
containerArg,
createFlags.distro,
createFlags.image,
createFlags.release)
if err != nil {
return err
}
if err := createContainer(container, image, release, createFlags.authFile, true); err != nil {
return err
}
return nil
}
func createContainer(container, image, release, authFile string, showCommandToEnter bool) error {
if container == "" {
panic("container not specified")
}
if image == "" {
panic("image not specified")
}
if release == "" {
panic("release not specified")
}
enterCommand := getEnterCommand(container)
logrus.Debugf("Checking if container %s already exists", container)
if exists, _ := podman.ContainerExists(container); exists {
var builder strings.Builder
fmt.Fprintf(&builder, "container %s already exists\n", container)
fmt.Fprintf(&builder, "Enter with: %s\n", enterCommand)
fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase)
errMsg := builder.String()
return errors.New(errMsg)
}
pulled, err := pullImage(image, release, authFile)
if err != nil {
return err
}
if !pulled {
return nil
}
imageFull, err := podman.GetFullyQualifiedImageFromRepoTags(image)
if err != nil {
var errImage *podman.ImageError
if errors.As(err, &errImage) {
if errors.Is(err, podman.ErrImageRepoTagsEmpty) {
logrus.Debugf("Image %s has empty RepoTags, likely because it is without a name", image)
imageFull = image
} else if errors.Is(err, podman.ErrImageRepoTagsMissing) {
return fmt.Errorf("missing RepoTags for image %s", image)
} else {
panicMsg := fmt.Sprintf("unexpected %T: %s", err, err)
panic(panicMsg)
}
} else {
return err
}
}
var toolbxDelayEntryPointEnv []string
if toolbxDelayEntryPoint, ok := os.LookupEnv("TOOLBX_DELAY_ENTRY_POINT"); ok {
toolbxDelayEntryPointEnvArg := "TOOLBX_DELAY_ENTRY_POINT=" + toolbxDelayEntryPoint
toolbxDelayEntryPointEnv = []string{"--env", toolbxDelayEntryPointEnvArg}
}
var toolbxFailEntryPointEnv []string
if toolbxFailEntryPoint, ok := os.LookupEnv("TOOLBX_FAIL_ENTRY_POINT"); ok {
toolbxFailEntryPointEnvArg := "TOOLBX_FAIL_ENTRY_POINT=" + toolbxFailEntryPoint
toolbxFailEntryPointEnv = []string{"--env", toolbxFailEntryPointEnvArg}
}
toolboxPath := os.Getenv("TOOLBOX_PATH")
toolboxPathEnvArg := "TOOLBOX_PATH=" + toolboxPath
toolboxPathMountArg := toolboxPath + ":/usr/bin/toolbox:ro"
var runtimeDirectory string
var xdgRuntimeDirEnv []string
if currentUser.Uid == "0" {
runtimeDirectory, err = utils.GetRuntimeDirectory(currentUser)
if err != nil {
return err
}
} else {
xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR")
xdgRuntimeDirEnvArg := "XDG_RUNTIME_DIR=" + xdgRuntimeDir
xdgRuntimeDirEnv = []string{"--env", xdgRuntimeDirEnvArg}
runtimeDirectory = xdgRuntimeDir
}
runtimeDirectoryMountArg := runtimeDirectory + ":" + runtimeDirectory
logrus.Debug("Checking if 'podman create' supports '--mount type=devpts'")
var devPtsMount []string
if podman.CheckVersion("2.1.0") {
logrus.Debug("'podman create' supports '--mount type=devpts'")
devPtsMount = []string{"--mount", "type=devpts,destination=/dev/pts"}
}
var usernsArg string
if currentUser.Uid == "0" {
usernsArg = "host"
} else {
usernsArg = "keep-id"
}
dbusSystemSocket, err := getDBusSystemSocket()
if err != nil {
return err
}
dbusSystemSocketMountArg := dbusSystemSocket + ":" + dbusSystemSocket
currentUserHomeDir := getCurrentUserHomeDir()
homeDirEvaled, err := filepath.EvalSymlinks(currentUserHomeDir)
if err != nil {
return fmt.Errorf("failed to canonicalize %s", currentUserHomeDir)
}
logrus.Debugf("%s canonicalized to %s", currentUserHomeDir, homeDirEvaled)
homeDirMountArg := homeDirEvaled + ":" + homeDirEvaled + ":rslave"
var avahiSocketMount []string
avahiSocket, err := getServiceSocket("Avahi", "avahi-daemon.socket")
if err != nil {
logrus.Debug(err)
}
if avahiSocket != "" {
avahiSocketMountArg := avahiSocket + ":" + avahiSocket
avahiSocketMount = []string{"--volume", avahiSocketMountArg}
}
var kcmSocketMount []string
kcmSocket, err := getServiceSocket("KCM", "sssd-kcm.socket")
if err != nil {
logrus.Debug(err)
}
if kcmSocket != "" {
kcmSocketMountArg := kcmSocket + ":" + kcmSocket
kcmSocketMount = []string{"--volume", kcmSocketMountArg}
}
var pcscSocketMount []string
pcscSocket, err := getServiceSocket("pcsc", "pcscd.socket")
if err != nil {
logrus.Debug(err)
}
if pcscSocket != "" {
pcscSocketMountArg := pcscSocket + ":" + pcscSocket
pcscSocketMount = []string{"--volume", pcscSocketMountArg}
}
var mediaLink []string
var mediaMount []string
if utils.PathExists("/media") {
logrus.Debug("Checking if /media is a symbolic link to /run/media")
mediaPath, _ := filepath.EvalSymlinks("/media")
if mediaPath == "/run/media" {
logrus.Debug("/media is a symbolic link to /run/media")
mediaLink = []string{"--media-link"}
} else {
mediaMount = []string{"--volume", "/media:/media:rslave"}
}
}
var mntLink []string
var mntMount []string
if utils.PathExists("/mnt") {
logrus.Debug("Checking if /mnt is a symbolic link to /var/mnt")
mntPath, _ := filepath.EvalSymlinks("/mnt")
if mntPath == "/var/mnt" {
logrus.Debug("/mnt is a symbolic link to /var/mnt")
mntLink = []string{"--mnt-link"}
} else {
mntMount = []string{"--volume", "/mnt:/mnt:rslave"}
}
}
var runMediaMount []string
if utils.PathExists("/run/media") {
runMediaMount = []string{"--volume", "/run/media:/run/media:rslave"}
}
logrus.Debug("Looking up toolbox.sh")
var toolboxShMount []string
for _, mount := range createToolboxShMounts {
if utils.PathExists(mount.source) {
logrus.Debugf("Found %s", mount.source)
toolboxShMountArg := mount.source + ":" + mount.containerPath + ":ro"
toolboxShMount = []string{"--volume", toolboxShMountArg}
break
}
}
logrus.Debug("Checking if /home is a symbolic link to /var/home")
var slashHomeLink []string
slashHomeEvaled, _ := filepath.EvalSymlinks("/home")
if slashHomeEvaled == "/var/home" {
logrus.Debug("/home is a symbolic link to /var/home")
slashHomeLink = []string{"--home-link"}
}
logLevelString := podman.LogLevel.String()
userShell := os.Getenv("SHELL")
if userShell == "" {
return errors.New("failed to get the current user's default shell")
}
entryPoint := []string{
"toolbox", "--log-level", "debug",
"init-container",
"--gid", currentUser.Gid,
"--home", currentUserHomeDir,
"--shell", userShell,
"--uid", currentUser.Uid,
"--user", currentUser.Username,
}
entryPoint = append(entryPoint, slashHomeLink...)
entryPoint = append(entryPoint, mediaLink...)
entryPoint = append(entryPoint, mntLink...)
createArgs := []string{
"--log-level", logLevelString,
"create",
"--cgroupns", "host",
"--dns", "none",
}
createArgs = append(createArgs, toolbxDelayEntryPointEnv...)
createArgs = append(createArgs, toolbxFailEntryPointEnv...)
createArgs = append(createArgs, []string{
"--env", toolboxPathEnvArg,
}...)
createArgs = append(createArgs, xdgRuntimeDirEnv...)
createArgs = append(createArgs, []string{
"--hostname", "toolbx",
"--ipc", "host",
"--label", "com.github.containers.toolbox=true",
}...)
createArgs = append(createArgs, devPtsMount...)
createArgs = append(createArgs, []string{
"--name", container,
"--network", "host",
"--no-hosts",
"--pid", "host",
"--privileged",
"--security-opt", "label=disable",
"--ulimit", "host",
"--userns", usernsArg,
"--user", "root:root",
"--volume", "/:/run/host:rslave",
"--volume", "/dev:/dev:rslave",
"--volume", dbusSystemSocketMountArg,
"--volume", homeDirMountArg,
"--volume", toolboxPathMountArg,
"--volume", runtimeDirectoryMountArg,
}...)
if podman.CheckVersion("3.2.0") {
if runtime, _ := podman.GetRuntimeName(); runtime == "crun" {
createArgs = append(createArgs, []string{
"--group-add", "keep-groups",
}...)
}
}
createArgs = append(createArgs, avahiSocketMount...)
createArgs = append(createArgs, kcmSocketMount...)
createArgs = append(createArgs, mediaMount...)
createArgs = append(createArgs, mntMount...)
createArgs = append(createArgs, pcscSocketMount...)
createArgs = append(createArgs, runMediaMount...)
createArgs = append(createArgs, toolboxShMount...)
createArgs = append(createArgs, []string{
imageFull,
}...)
createArgs = append(createArgs, entryPoint...)
logrus.Debugf("Creating container %s:", container)
logrus.Debug("podman")
for _, arg := range createArgs {
logrus.Debugf("%s", arg)
}
s := spinner.New(spinner.CharSets[9], 500*time.Millisecond, spinner.WithWriterFile(os.Stdout))
if logLevel := logrus.GetLevel(); logLevel < logrus.DebugLevel {
s.Prefix = fmt.Sprintf("Creating container %s: ", container)
s.Start()
defer s.Stop()
}
if err := shell.Run("podman", nil, nil, nil, createArgs...); err != nil {
return fmt.Errorf("failed to create container %s", container)
}
// The spinner must be stopped before showing the 'enter' hint below.
s.Stop()
if showCommandToEnter {
fmt.Printf("Created container: %s\n", container)
fmt.Printf("Enter with: %s\n", enterCommand)
}
return nil
}
func createHelp(cmd *cobra.Command, args []string) {
if utils.IsInsideContainer() {
if !utils.IsInsideToolboxContainer() {
fmt.Fprintf(os.Stderr, "Error: this is not a Toolbx container\n")
return
}
if _, err := utils.ForwardToHost(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
return
}
return
}
if err := showManual("toolbox-create"); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
return
}
}
func getDBusSystemSocket() (string, error) {
logrus.Debug("Resolving path to the D-Bus system socket")
address := os.Getenv("DBUS_SYSTEM_BUS_ADDRESS")
if address == "" {
address = "unix:path=/var/run/dbus/system_bus_socket"
}
addressSplit := strings.Split(address, "=")
if len(addressSplit) != 2 {
return "", errors.New("failed to get the path to the D-Bus system socket")
}
path := addressSplit[1]
pathEvaled, err := filepath.EvalSymlinks(path)
if err != nil {
logrus.Debugf("Resolving path to the D-Bus system socket: failed to evaluate symbolic links in %s: %s",
path,
err)
return "", errors.New("failed to resolve the path to the D-Bus system socket")
}
return pathEvaled, nil
}
func getEnterCommand(container string) string {
var enterCommand string
switch container {
case utils.ContainerNameDefault:
enterCommand = fmt.Sprintf("%s enter", executableBase)
default:
enterCommand = fmt.Sprintf("%s enter %s", executableBase, container)
}
return enterCommand
}
func getImageSizeFromRegistry(ctx context.Context, imageFull string) (string, error) {
image, err := skopeo.Inspect(ctx, imageFull)
if err != nil {
return "", err
}
if image.LayersData == nil {
return "", errors.New("'skopeo inspect' did not have LayersData")
}
var imageSizeFloat float64
for _, layer := range image.LayersData {
if layerSize, err := layer.Size.Float64(); err != nil {
return "", err
} else {
imageSizeFloat += layerSize
}
}
imageSizeHuman := units.HumanSize(imageSizeFloat)
return imageSizeHuman, nil
}
func getImageSizeFromRegistryAsync(ctx context.Context, imageFull string) (<-chan string, <-chan error) {
retValCh := make(chan string)
errCh := make(chan error)
go func() {
imageSize, err := getImageSizeFromRegistry(ctx, imageFull)
if err != nil {
errCh <- err
return
}
retValCh <- imageSize
}()
return retValCh, errCh
}
func getServiceSocket(serviceName string, unitName string) (string, error) {
logrus.Debugf("Resolving path to the %s socket", serviceName)
connection, err := dbus.SystemBus()
if err != nil {
logrus.Debugf("Resolving path to the %s socket: failed to connect to the D-Bus system instance: %s",
serviceName,
err)
return "", errors.New("failed to connect to the D-Bus system instance")
}
unitNameEscaped := systemdPathBusEscape(unitName)
unitPath := dbus.ObjectPath("/org/freedesktop/systemd1/unit/" + unitNameEscaped)
unit := connection.Object("org.freedesktop.systemd1", unitPath)
call := unit.Call("org.freedesktop.DBus.Properties.GetAll", 0, "")
var result map[string]dbus.Variant
err = call.Store(&result)
if err != nil {
logrus.Debugf("Resolving path to the %s socket: failed to get the properties of %s: %s",
serviceName,
unitName,
err)
return "", fmt.Errorf("failed to get the properties of %s", unitName)
}
listenVariant, listenFound := result["Listen"]
if !listenFound {
return "", fmt.Errorf("failed to find the Listen property of %s", unitName)
}
listenVariantSignature := listenVariant.Signature().String()
if listenVariantSignature != "aav" {
return "", errors.New("unknown reply from org.freedesktop.DBus.Properties.GetAll")
}
listenValue := listenVariant.Value()
sockets := listenValue.([][]interface{})
for _, socket := range sockets {
if socket[0] == "Stream" {
path := socket[1].(string)
if !strings.HasPrefix(path, "/") {
continue
}
pathEvaled, err := filepath.EvalSymlinks(path)
if err != nil {
continue
}
return pathEvaled, nil
}
}
return "", fmt.Errorf("failed to find a SOCK_STREAM socket for %s", unitName)
}
func pullImage(image, release, authFile string) (bool, error) {
if ok := utils.ImageReferenceCanBeID(image); ok {
logrus.Debugf("Looking up image %s", image)
if _, err := podman.ImageExists(image); err == nil {
return true, nil
}
}
hasDomain := utils.ImageReferenceHasDomain(image)
if !hasDomain {
imageLocal := "localhost/" + image
logrus.Debugf("Looking up image %s", imageLocal)
if _, err := podman.ImageExists(imageLocal); err == nil {
return true, nil
}
}
var imageFull string
if hasDomain {
imageFull = image
} else {
var err error
imageFull, err = utils.GetFullyQualifiedImageFromDistros(image, release)
if err != nil {
return false, fmt.Errorf("image %s not found in local storage and known registries", image)
}
}
logrus.Debugf("Looking up image %s", imageFull)
if _, err := podman.ImageExists(imageFull); err == nil {
return true, nil
}
domain := utils.ImageReferenceGetDomain(imageFull)
if domain == "" {
panicMsg := fmt.Sprintf("failed to get domain from %s", imageFull)
panic(panicMsg)
}
promptForDownload := true
var shouldPullImage bool
if rootFlags.assumeYes || domain == "localhost" {
promptForDownload = false
shouldPullImage = true
}
if promptForDownload {
if !term.IsTerminal(os.Stdin) || !term.IsTerminal(os.Stdout) {
var builder strings.Builder
fmt.Fprintf(&builder, "image required to create Toolbx container.\n")
fmt.Fprintf(&builder, "Use option '--assumeyes' to download the image.\n")
fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase)
errMsg := builder.String()
return false, errors.New(errMsg)
}
shouldPullImage = showPromptForDownload(imageFull)
}
if !shouldPullImage {
return false, nil
}
logrus.Debugf("Pulling image %s", imageFull)
if logLevel := logrus.GetLevel(); logLevel < logrus.DebugLevel {
s := spinner.New(spinner.CharSets[9], 500*time.Millisecond, spinner.WithWriterFile(os.Stdout))
s.Prefix = fmt.Sprintf("Pulling %s: ", imageFull)
s.Start()
defer s.Stop()
}
if err := podman.Pull(imageFull, authFile); err != nil {
var builder strings.Builder
fmt.Fprintf(&builder, "failed to pull image %s\n", imageFull)
fmt.Fprintf(&builder, "If it was a private image, log in with: podman login %s\n", domain)
fmt.Fprintf(&builder, "Use '%s --verbose ...' for further details.", executableBase)
errMsg := builder.String()
return false, errors.New(errMsg)
}
return true, nil
}
func createPromptForDownload(imageFull, imageSize string) string {
var prompt string
if imageSize == "" {
prompt = fmt.Sprintf("Download %s? [y/N]:", imageFull)
} else {
prompt = fmt.Sprintf("Download %s (%s)? [y/N]:", imageFull, imageSize)
}
return prompt
}
func showPromptForDownloadFirst(imageFull string) (bool, error) {
prompt := createPromptForDownload(imageFull, " ... MB")
parentCtx := context.Background()
askCtx, askCancel := context.WithCancelCause(parentCtx)
defer askCancel(errors.New("clean-up"))
askCh, askErrCh := askForConfirmationAsync(askCtx, prompt, nil)
imageSizeCtx, imageSizeCancel := context.WithCancelCause(parentCtx)
defer imageSizeCancel(errors.New("clean-up"))
imageSizeCh, imageSizeErrCh := getImageSizeFromRegistryAsync(imageSizeCtx, imageFull)
var imageSize string
var shouldPullImage bool
select {
case val := <-askCh:
shouldPullImage = val
cause := fmt.Errorf("%w: received confirmation without image size", context.Canceled)
imageSizeCancel(cause)
case err := <-askErrCh:
shouldPullImage = false
cause := fmt.Errorf("failed to ask for confirmation without image size: %w", err)
imageSizeCancel(cause)
case val := <-imageSizeCh:
imageSize = val
cause := fmt.Errorf("%w: received image size", context.Canceled)
askCancel(cause)
case err := <-imageSizeErrCh:
cause := fmt.Errorf("failed to get image size: %w", err)
askCancel(cause)
}
if imageSizeCtx.Err() != nil && askCtx.Err() == nil {
cause := context.Cause(imageSizeCtx)
logrus.Debugf("Show prompt for download: image size canceled: %s", cause)
return shouldPullImage, nil
}
var done bool
if imageSizeCtx.Err() == nil && askCtx.Err() != nil {
select {
case val := <-askCh:
logrus.Debugf("Show prompt for download: received pending confirmation without image size")
shouldPullImage = val
done = true
case err := <-askErrCh:
logrus.Debugf("Show prompt for download: failed to ask for confirmation without image size: %s",
err)
}
} else {
panic("code should not be reached")
}
cause := context.Cause(askCtx)
logrus.Debugf("Show prompt for download: ask canceled: %s", cause)
if done {
return shouldPullImage, nil
}
return false, &promptForDownloadError{imageSize}
}
func showPromptForDownloadSecond(imageFull string, errFirst *promptForDownloadError) bool {
oldState, err := term.GetState(os.Stdin)
if err != nil {
logrus.Debugf("Show prompt for download: failed to get terminal state: %s", err)
return false
}
defer term.SetState(os.Stdin, oldState)
lockedState := term.NewStateFrom(oldState,
term.WithVMIN(1),
term.WithVTIME(0),
term.WithoutECHO(),
term.WithoutICANON())
if err := term.SetState(os.Stdin, lockedState); err != nil {
logrus.Debugf("Show prompt for download: failed to set terminal state: %s", err)
return false
}
parentCtx := context.Background()
discardCtx, discardCancel := context.WithCancelCause(parentCtx)
defer discardCancel(errors.New("clean-up"))
discardCh, discardErrCh := discardInputAsync(discardCtx)
var prompt string
if errors.Is(errFirst, context.Canceled) {
prompt = createPromptForDownload(imageFull, errFirst.ImageSize)
} else {
prompt = createPromptForDownload(imageFull, "")
}
fmt.Printf("\r")
askCtx, askCancel := context.WithCancelCause(parentCtx)
defer askCancel(errors.New("clean-up"))
var askForConfirmationPreFnDone bool
askForConfirmationPreFn := func() error {
defer discardCancel(errors.New("clean-up"))
if askForConfirmationPreFnDone {
return nil
}
// Erase to end of line
fmt.Printf("\033[K")
// Save the cursor position.
fmt.Printf("\033[s")
if err := term.SetState(os.Stdin, oldState); err != nil {
return fmt.Errorf("failed to restore terminal state: %w", err)
}
cause := errors.New("terminal restored")
discardCancel(cause)
err := <-discardErrCh
if !errors.Is(err, context.Canceled) {
return fmt.Errorf("failed to discard input: %w", err)
}
logrus.Debugf("Show prompt for download: stopped discarding input: %s", err)
discardTotal := <-discardCh
logrus.Debugf("Show prompt for download: discarded input: %d bytes", discardTotal)
if discardTotal == 0 {
askForConfirmationPreFnDone = true
return nil
}
if err := term.SetState(os.Stdin, lockedState); err != nil {
return fmt.Errorf("failed to set terminal state: %w", err)
}
discardCtx, discardCancel = context.WithCancelCause(parentCtx)
// A deferred call can't be used for this CancelCauseFunc,
// because the 'discard' operation needs to continue running
// until the next invocation of this function. It relies on
// the guarantee that AskForConfirmationAsync will always call
// its askForConfirmationPreFunc as long as the function
// returns errContinue.
discardCh, discardErrCh = discardInputAsync(discardCtx)
// Restore the cursor position
fmt.Printf("\033[u")
// Erase to end of line
fmt.Printf("\033[K")
fmt.Printf("...\n")
return errContinue
}
askCh, askErrCh := askForConfirmationAsync(askCtx, prompt, askForConfirmationPreFn)
var shouldPullImage bool
select {
case val := <-askCh:
logrus.Debug("Show prompt for download: received confirmation with image size")
shouldPullImage = val
case err := <-askErrCh:
logrus.Debugf("Show prompt for download: failed to ask for confirmation with image size: %s", err)
shouldPullImage = false
}
return shouldPullImage
}
func showPromptForDownload(imageFull string) bool {
fmt.Println("Image required to create Toolbx container.")
shouldPullImage, err := showPromptForDownloadFirst(imageFull)
if err == nil {
return shouldPullImage
}
var errPromptForDownload *promptForDownloadError
if !errors.As(err, &errPromptForDownload) {
panicMsg := fmt.Sprintf("unexpected %T: %s", err, err)
panic(panicMsg)
}
shouldPullImage = showPromptForDownloadSecond(imageFull, errPromptForDownload)
return shouldPullImage
}
// systemdNeedsEscape checks whether a byte in a potential dbus ObjectPath needs to be escaped
func systemdNeedsEscape(i int, b byte) bool {
// Escape everything that is not a-z-A-Z-0-9
// Also escape 0-9 if it's the first character
return strings.IndexByte(alphanum, b) == -1 ||
(i == 0 && strings.IndexByte(num, b) != -1)
}
// systemdPathBusEscape sanitizes a constituent string of a dbus ObjectPath using the
// rules that systemd uses for serializing special characters.
func systemdPathBusEscape(path string) string {
// Special case the empty string
if len(path) == 0 {
return "_"
}
n := []byte{}
for i := 0; i < len(path); i++ {
c := path[i]
if systemdNeedsEscape(i, c) {
e := fmt.Sprintf("_%x", c)
n = append(n, []byte(e)...)
} else {
n = append(n, c)
}
}
return string(n)
}
func (err *promptForDownloadError) Error() string {
innerErr := err.Unwrap()
errMsg := innerErr.Error()
return errMsg
}
func (err *promptForDownloadError) Unwrap() error {
if err.ImageSize == "" {
return errors.New("failed to get image size")
}
return context.Canceled
}