diff --git a/src/cmd/create.go b/src/cmd/create.go index bdd86be37..925b8384d 100644 --- a/src/cmd/create.go +++ b/src/cmd/create.go @@ -21,34 +21,40 @@ import ( "errors" "fmt" "os" + "os/exec" "path/filepath" "strings" "time" "github.com/briandowns/spinner" + "github.com/containers/toolbox/pkg/architecture" "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 ) +type pullImageDecision int + +const ( + pullNo pullImageDecision = iota // 0 - User declined or default + pullYes // 1 - User confirmed + pullUnknown // 2 - Need second prompt +) + var ( createFlags struct { + arch string authFile string container string distro string @@ -75,6 +81,12 @@ var createCmd = &cobra.Command{ func init() { flags := createCmd.Flags() + flags.StringVarP(&createFlags.arch, + "arch", + "a", + "", + "Create a Toolbx container for a different architecture than the host") + flags.StringVar(&createFlags.authFile, "authfile", "", @@ -170,24 +182,43 @@ func create(cmd *cobra.Command, args []string) error { containerArg = "--container" } + var archConfig architecture.Config + + archID, err := resolveArchitectureID(createFlags.arch, createFlags.image) + if err != nil { + return err + } + archConfig.ID = archID + + if !architecture.HasContainerNativeArch(archConfig.ID) { + archName := architecture.GetArchNameOCI(archConfig.ID) + qemuEmulatorPath, err := architecture.IsArchSupportedOnCreation(archID) + if err != nil { + errNotSupported := fmt.Errorf("Cannot create container for architecture %s\n%s", archName, err) + return errNotSupported + } + archConfig.QemuEmulatorPath = qemuEmulatorPath + } + container, image, release, err := resolveContainerAndImageNames(container, containerArg, createFlags.distro, createFlags.image, - createFlags.release) + createFlags.release, + archConfig.ID) if err != nil { return err } - if err := createContainer(container, image, release, createFlags.authFile, true); err != nil { + if err := createContainer(container, image, release, createFlags.authFile, archConfig, true); err != nil { return err } return nil } -func createContainer(container, image, release, authFile string, showCommandToEnter bool) error { +func createContainer(container, image, release, authFile string, archConfig architecture.Config, showCommandToEnter bool) error { if container == "" { panic("container not specified") } @@ -214,14 +245,19 @@ func createContainer(container, image, release, authFile string, showCommandToEn return errors.New(errMsg) } - pulled, err := pullImage(image, release, authFile) + pulled, couldBeNonnativeArch, err := pullImage(image, release, authFile, archConfig.ID) if err != nil { return err } + if !pulled { return nil } + if couldBeNonnativeArch { + image = resolveImageNameWithArchitectureSuffix(image, archConfig.ID) + } + imageFull, err := podman.GetFullyQualifiedImageFromRepoTags(image) if err != nil { var errImage *podman.ImageError @@ -413,6 +449,8 @@ func createContainer(container, image, release, authFile string, showCommandToEn entryPoint := []string{ "toolbox", "--log-level", "debug", "init-container", + "--arch", fmt.Sprintf("%d", archConfig.ID), + "--arch-emulator-path", archConfig.QemuEmulatorPath, "--gid", currentUser.Gid, "--home", currentUserHomeDir, "--shell", userShell, @@ -446,6 +484,10 @@ func createContainer(container, image, release, authFile string, showCommandToEn "--label", "com.github.containers.toolbox=true", }...) + createArgs = append(createArgs, []string{ + "--label", "toolbox-arch=" + architecture.GetArchNameOCI(archConfig.ID), + }...) + createArgs = append(createArgs, devPtsMount...) createArgs = append(createArgs, []string{ @@ -486,19 +528,15 @@ func createContainer(container, image, release, authFile string, showCommandToEn 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() - } + s := startSpinner(fmt.Sprintf("Creating container %s: ", container)) + defer stopSpinner(s) 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() + stopSpinner(s) if showCommandToEnter { fmt.Printf("Created container: %s\n", container) @@ -508,6 +546,13 @@ func createContainer(container, image, release, authFile string, showCommandToEn return nil } +func boolToPullDecision(shouldPull bool) pullImageDecision { + if shouldPull { + return pullYes + } + return pullNo +} + func createHelp(cmd *cobra.Command, args []string) { if utils.IsInsideContainer() { if !utils.IsInsideToolboxContainer() { @@ -567,42 +612,19 @@ func getEnterCommand(container string) string { 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) +func getImageFromRegistryAsync(ctx context.Context, imageFull string, archID int, authFile string) (<-chan *skopeo.Image, <-chan error) { + retValCh := make(chan *skopeo.Image) errCh := make(chan error) go func() { - imageSize, err := getImageSizeFromRegistry(ctx, imageFull) + image, err := skopeo.Inspect(ctx, imageFull, archID, authFile) + if err != nil { errCh <- err return } - retValCh <- imageSize + retValCh <- image }() return retValCh, errCh @@ -665,11 +687,13 @@ func getServiceSocket(serviceName string, unitName string) (string, error) { return "", fmt.Errorf("failed to find a SOCK_STREAM socket for %s", unitName) } -func pullImage(image, release, authFile string) (bool, error) { +func pullImage(image, release, authFile string, archID int) (bool, bool, error) { + isNonNativeArch := !architecture.HasContainerNativeArch(archID) + if ok := utils.ImageReferenceCanBeID(image); ok { logrus.Debugf("Looking up image %s", image) if _, err := podman.ImageExists(image); err == nil { - return true, nil + return true, false, nil } } @@ -680,7 +704,7 @@ func pullImage(image, release, authFile string) (bool, error) { logrus.Debugf("Looking up image %s", imageLocal) if _, err := podman.ImageExists(imageLocal); err == nil { - return true, nil + return true, false, nil } } @@ -692,13 +716,15 @@ func pullImage(image, release, authFile string) (bool, error) { 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) + return false, 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 + imageFullWithArch := resolveImageNameWithArchitectureSuffix(imageFull, archID) + + logrus.Debugf("Looking up image %s", imageFullWithArch) + if _, err := podman.ImageExists(imageFullWithArch); err == nil { + return true, isNonNativeArch, nil } domain := utils.ImageReferenceGetDomain(imageFull) @@ -709,10 +735,18 @@ func pullImage(image, release, authFile string) (bool, error) { promptForDownload := true var shouldPullImage bool + var imageInfo *skopeo.Image + var imageInspectErr error if rootFlags.assumeYes || domain == "localhost" { promptForDownload = false shouldPullImage = true + + if isNonNativeArch { + s := startSpinner("Fetching non-native architecture image info: ") + imageInfo, imageInspectErr = skopeo.Inspect(context.Background(), imageFull, archID, authFile) + stopSpinner(s) + } } if promptForDownload { @@ -723,36 +757,56 @@ func pullImage(image, release, authFile string) (bool, error) { fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase) errMsg := builder.String() - return false, errors.New(errMsg) + return false, false, errors.New(errMsg) } - shouldPullImage = showPromptForDownload(imageFull) + shouldPullImage, imageInfo, imageInspectErr = showPromptForDownload(imageFull, archID, authFile) } if !shouldPullImage { - return false, nil + return false, false, nil } - logrus.Debugf("Pulling image %s", imageFull) + if imageInspectErr != nil && isNonNativeArch { + if errors.Is(imageInspectErr, exec.ErrNotFound) { + return false, false, createErrorSkopeoNotFound(imageFull, archID) + } - 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() + // For now, log and continue (imageInfo will be nil) + logrus.Debugf("Failed to inspect image: %s", imageInspectErr) } - 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) + logrus.Debugf("Pulling image %s", imageFull) - errMsg := builder.String() - return false, errors.New(errMsg) + s := startSpinner(fmt.Sprintf("Pulling %s: ", imageFull)) + defer stopSpinner(s) + + if !isNonNativeArch { + logrus.Debugf("'podman pull' is used for pulling image %s", imageFull) + + if err := podman.Pull(imageFull, authFile); err != nil { + return false, false, createErrorImagePull(imageFull, domain) + } + } else { + logrus.Debugf("'skopeo copy' is used for pulling non-native architecture image %s", imageFull) + + if imageInfo == nil { + // Multi-arch image mismatch + expectedArchName := architecture.GetArchNameOCI(archID) + return false, false, fmt.Errorf("failed to verify: image %s does not support architecture %s or the image does not exists at all", + imageFull, expectedArchName) + } + + if err := imageInfo.VerifyArchitectureMatch(archID); err != nil { + return false, false, err + } + + if err := skopeo.CopyOverrideArch(imageFull, imageFullWithArch, archID, authFile); err != nil { + return false, false, createErrorImagePull(imageFull, domain) + } } - return true, nil + return true, isNonNativeArch, nil } func createPromptForDownload(imageFull, imageSize string) string { @@ -766,8 +820,9 @@ func createPromptForDownload(imageFull, imageSize string) string { return prompt } -func showPromptForDownloadFirst(imageFull string) (bool, error) { +func showPromptForDownloadFirst(imageFull string, archID int, authFile string) (pullImageDecision, *skopeo.Image, error) { prompt := createPromptForDownload(imageFull, " ... MB") + isNonnativeArch := !architecture.HasContainerNativeArch(archID) parentCtx := context.Background() askCtx, askCancel := context.WithCancelCause(parentCtx) @@ -775,48 +830,69 @@ func showPromptForDownloadFirst(imageFull string) (bool, error) { askCh, askErrCh := askForConfirmationAsync(askCtx, prompt, nil) - imageSizeCtx, imageSizeCancel := context.WithCancelCause(parentCtx) - defer imageSizeCancel(errors.New("clean-up")) + imageCtx, imageCancel := context.WithCancelCause(parentCtx) + defer imageCancel(errors.New("clean-up")) - imageSizeCh, imageSizeErrCh := getImageSizeFromRegistryAsync(imageSizeCtx, imageFull) + imageCh, imageErrCh := getImageFromRegistryAsync(imageCtx, imageFull, archID, authFile) - var imageSize string - var shouldPullImage bool + var image *skopeo.Image = nil + var shouldPullImage pullImageDecision = pullNo + var imageInspectErr error = nil select { case val := <-askCh: - shouldPullImage = val - cause := fmt.Errorf("%w: received confirmation without image size", context.Canceled) - imageSizeCancel(cause) + shouldPullImage = boolToPullDecision(val) + + if isNonnativeArch { + if shouldPullImage == pullNo { + return pullNo, nil, nil + } + + s := startSpinner("Fetching non-native architecture image info: ") + + select { + case img := <-imageCh: + stopSpinner(s) + image = img + return shouldPullImage, image, nil + case err := <-imageErrCh: + stopSpinner(s) + return shouldPullImage, nil, err + } + } else { + cause := fmt.Errorf("%w: received confirmation without image info", context.Canceled) + imageCancel(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) + shouldPullImage = pullNo + cause := fmt.Errorf("failed to ask for confirmation without image info: %w", err) + imageCancel(cause) + case val := <-imageCh: + image = val + cause := fmt.Errorf("%w: received image info", context.Canceled) askCancel(cause) - case err := <-imageSizeErrCh: - cause := fmt.Errorf("failed to get image size: %w", err) + case err := <-imageErrCh: + imageInspectErr = err + cause := fmt.Errorf("failed to get image info: %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 + if imageCtx.Err() != nil && askCtx.Err() == nil { + cause := context.Cause(imageCtx) + logrus.Debugf("Show prompt for download: image info canceled: %s", cause) + return shouldPullImage, nil, nil } var done bool - if imageSizeCtx.Err() == nil && askCtx.Err() != nil { + if imageCtx.Err() == nil && askCtx.Err() != nil { select { case val := <-askCh: - logrus.Debugf("Show prompt for download: received pending confirmation without image size") - shouldPullImage = val + logrus.Debugf("Show prompt for download: received pending confirmation without image info") + shouldPullImage = boolToPullDecision(val) done = true case err := <-askErrCh: - logrus.Debugf("Show prompt for download: failed to ask for confirmation without image size: %s", + logrus.Debugf("Show prompt for download: failed to ask for confirmation without image info: %s", err) } } else { @@ -827,13 +903,13 @@ func showPromptForDownloadFirst(imageFull string) (bool, error) { logrus.Debugf("Show prompt for download: ask canceled: %s", cause) if done { - return shouldPullImage, nil + return shouldPullImage, image, imageInspectErr + } else { + return pullUnknown, image, imageInspectErr } - - return false, &promptForDownloadError{imageSize} } -func showPromptForDownloadSecond(imageFull string, errFirst *promptForDownloadError) bool { +func showPromptForDownloadSecond(imageFull, imageSize string) bool { oldState, err := term.GetState(os.Stdin) if err != nil { logrus.Debugf("Show prompt for download: failed to get terminal state: %s", err) @@ -859,12 +935,7 @@ func showPromptForDownloadSecond(imageFull string, errFirst *promptForDownloadEr discardCh, discardErrCh := discardInputAsync(discardCtx) - var prompt string - if errors.Is(errFirst, context.Canceled) { - prompt = createPromptForDownload(imageFull, errFirst.ImageSize) - } else { - prompt = createPromptForDownload(imageFull, "") - } + prompt := createPromptForDownload(imageFull, imageSize) fmt.Printf("\r") @@ -945,22 +1016,53 @@ func showPromptForDownloadSecond(imageFull string, errFirst *promptForDownloadEr return shouldPullImage } -func showPromptForDownload(imageFull string) bool { +func showPromptForDownload(imageFull string, archID int, authFile string) (bool, *skopeo.Image, error) { fmt.Println("Image required to create Toolbx container.") - shouldPullImage, err := showPromptForDownloadFirst(imageFull) - if err == nil { - return shouldPullImage + var shouldPullImageFirst pullImageDecision + var image *skopeo.Image + var imageInspectErr error + + shouldPullImageFirst, image, imageInspectErr = showPromptForDownloadFirst(imageFull, archID, authFile) + + switch shouldPullImageFirst { + case pullYes: + return true, image, imageInspectErr + case pullNo: + return false, image, imageInspectErr } - var errPromptForDownload *promptForDownloadError - if !errors.As(err, &errPromptForDownload) { - panicMsg := fmt.Sprintf("unexpected %T: %s", err, err) - panic(panicMsg) + var imageSize string + var shouldPullImageSecond bool + var getSizeErr error + + if image == nil { + imageSize = "n/a" + } else { + imageSize, getSizeErr = image.GetSizeHuman() + if getSizeErr != nil { + imageSize = "n/a" + } } - shouldPullImage = showPromptForDownloadSecond(imageFull, errPromptForDownload) - return shouldPullImage + shouldPullImageSecond = showPromptForDownloadSecond(imageFull, imageSize) + return shouldPullImageSecond, image, imageInspectErr +} + +func startSpinner(message string) *spinner.Spinner { + if logLevel := logrus.GetLevel(); logLevel < logrus.DebugLevel { + s := spinner.New(spinner.CharSets[9], 500*time.Millisecond, spinner.WithWriterFile(os.Stdout)) + s.Prefix = message + s.Start() + return s + } + return nil +} + +func stopSpinner(s *spinner.Spinner) { + if s != nil { + s.Stop() + } } // systemdNeedsEscape checks whether a byte in a potential dbus ObjectPath needs to be escaped @@ -990,17 +1092,3 @@ func systemdPathBusEscape(path string) string { } 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 -} diff --git a/src/cmd/enter.go b/src/cmd/enter.go index 9ba84d6ad..851868769 100644 --- a/src/cmd/enter.go +++ b/src/cmd/enter.go @@ -27,6 +27,7 @@ import ( var ( enterFlags struct { + arch string container string distro string release string @@ -43,6 +44,12 @@ var enterCmd = &cobra.Command{ func init() { flags := enterCmd.Flags() + flags.StringVarP(&enterFlags.arch, + "arch", + "a", + "", + "Enter a Toolbx container for a different architecture than the host") + flags.StringVarP(&enterFlags.container, "container", "c", @@ -104,11 +111,17 @@ func enter(cmd *cobra.Command, args []string) error { defaultContainer = false } + archID, err := resolveArchitectureID(enterFlags.arch, "") + if err != nil { + return err + } + container, image, release, err := resolveContainerAndImageNames(container, containerArg, enterFlags.distro, "", - enterFlags.release) + enterFlags.release, + archID) if err != nil { return err diff --git a/src/cmd/initContainer.go b/src/cmd/initContainer.go index b3a7bd983..74f3b7fbf 100644 --- a/src/cmd/initContainer.go +++ b/src/cmd/initContainer.go @@ -28,6 +28,7 @@ import ( "syscall" "time" + "github.com/containers/toolbox/pkg/architecture" "github.com/containers/toolbox/pkg/shell" "github.com/containers/toolbox/pkg/utils" "github.com/fsnotify/fsnotify" @@ -41,6 +42,8 @@ import ( var ( initContainerFlags struct { + archID int + archInterp string gid int home string homeLink bool @@ -85,6 +88,16 @@ var initContainerCmd = &cobra.Command{ func init() { flags := initContainerCmd.Flags() + flags.IntVar(&initContainerFlags.archID, + "arch", + architecture.HostArchID, + "Specify the Toolbx container's architecture ID.") + + flags.StringVar(&initContainerFlags.archInterp, + "arch-emulator-path", + "", + "Register an emulator using binfmt_misc with PATH as the interpreter for a non-native architecture container.") + flags.IntVar(&initContainerFlags.gid, "gid", 0, @@ -257,6 +270,31 @@ func initContainer(cmd *cobra.Command, args []string) error { } } + if !architecture.HasContainerNativeArch(initContainerFlags.archID) { + archName := architecture.GetArchNameOCI(initContainerFlags.archID) + interpreterPath := "/run/host" + initContainerFlags.archInterp + + if err := validateCrossArchEmulation(initContainerFlags.archID); err != nil { + return err + } + + logrus.Debugf("Mounting binfmt_misc file system in container for architecture %s", archName) + if err := architecture.MountBinfmtMisc(); err != nil { + return err + } + + resolvedInterpreterPath, err := architecture.IsArchSupportedOnInitialization(initContainerFlags.archID, interpreterPath) + if err != nil { + errNotSupported := fmt.Errorf("Cannot run container for architecture %s:\n%s", archName, err) + return errNotSupported + } + + logrus.Debugf("Registering QEMU emulator for architecture %s in binfmt_misc", archName) + if err := architecture.RegisterBinfmtMisc(initContainerFlags.archID, resolvedInterpreterPath); err != nil { + return err + } + } + for _, mount := range initContainerMounts { if err := mountBind(mount.containerPath, mount.source, mount.flags); err != nil { return err @@ -1223,6 +1261,28 @@ func updateTimeZoneFromLocalTime() error { return nil } +func validateCrossArchEmulation(archID int) error { + archName := architecture.GetArchNameOCI(archID) + logrus.Debugf("Testing QEMU emulation for architecture %s", archName) + + _, err := shell.RunWithExitCode2("true", nil, nil, nil) + + if err != nil { + if errors.Is(err, syscall.ENOEXEC) { + return fmt.Errorf( + "QEMU emulation for architecture %s is not working\n"+ + "Please verify that:\n"+ + " 1. QEMU user-mode emulation is installed on the host system: qemu-user-static package\n"+ + " 2. binfmt_misc is properly configured on the host system", + archName) + } + return fmt.Errorf("failed to test QEMU emulation for architecture %s: %w", archName, err) + } + + logrus.Debugf("Test of QEMU emulation for architecture %s has succeeded", archName) + return nil +} + func writeTimeZone(timeZone string) error { const etcTimeZone = "/etc/timezone" diff --git a/src/cmd/rootMigrationPath.go b/src/cmd/rootMigrationPath.go index 33f970145..0464e4aed 100644 --- a/src/cmd/rootMigrationPath.go +++ b/src/cmd/rootMigrationPath.go @@ -24,6 +24,7 @@ import ( "os" "strings" + "github.com/containers/toolbox/pkg/architecture" "github.com/containers/toolbox/pkg/utils" "github.com/spf13/cobra" ) @@ -56,7 +57,7 @@ func rootRunImpl(cmd *cobra.Command, args []string) error { return &exitError{exitCode, err} } - container, image, release, err := resolveContainerAndImageNames("", "", "", "", "") + container, image, release, err := resolveContainerAndImageNames("", "", "", "", "", architecture.HostArchID) if err != nil { return err } diff --git a/src/cmd/run.go b/src/cmd/run.go index ed421aa68..1e7f7f853 100644 --- a/src/cmd/run.go +++ b/src/cmd/run.go @@ -30,6 +30,7 @@ import ( "syscall" "time" + "github.com/containers/toolbox/pkg/architecture" "github.com/containers/toolbox/pkg/nvidia" "github.com/containers/toolbox/pkg/podman" "github.com/containers/toolbox/pkg/shell" @@ -52,6 +53,7 @@ type entryPointError struct { var ( runFlags struct { + arch string container string distro string preserveFDs uint @@ -73,6 +75,12 @@ func init() { flags := runCmd.Flags() flags.SetInterspersed(false) + flags.StringVarP(&runFlags.arch, + "arch", + "a", + "", + "Run command inside a Toolbx container for a different architecture than the host") + flags.StringVarP(&runFlags.container, "container", "c", @@ -141,11 +149,17 @@ func run(cmd *cobra.Command, args []string) error { command := args + archID, err := resolveArchitectureID(runFlags.arch, "") + if err != nil { + return err + } + container, image, release, err := resolveContainerAndImageNames(runFlags.container, "--container", runFlags.distro, "", - runFlags.release) + runFlags.release, + archID) if err != nil { return err @@ -225,7 +239,7 @@ func runCommand(container string, return nil } - if err := createContainer(container, image, release, "", false); err != nil { + if err := createContainer(container, image, release, "", architecture.GetArchConfigDefault(), false); err != nil { return err } } else if containersCount == 1 && defaultContainer { @@ -962,8 +976,15 @@ func showEntryPointLog(line string) error { } if !logLevelFound { - errMsg, _ := strings.CutPrefix(line, "Error: ") - return &entryPointError{errMsg} + // Messages sent to stderr with a 'Warning:' prefix in the entry point + // are propagated to stderr on the host + if strings.HasPrefix(line, "Warning:") { + fmt.Fprintf(os.Stderr, "%s\n", line) + return nil + } else { + errMsg, _ := strings.CutPrefix(line, "Error: ") + return &entryPointError{errMsg} + } } logger := logrus.StandardLogger() diff --git a/src/cmd/utils.go b/src/cmd/utils.go index 191da4ef6..c3762b48a 100644 --- a/src/cmd/utils.go +++ b/src/cmd/utils.go @@ -31,6 +31,7 @@ import ( "strings" "syscall" + "github.com/containers/toolbox/pkg/architecture" "github.com/containers/toolbox/pkg/shell" "github.com/containers/toolbox/pkg/utils" "github.com/sirupsen/logrus" @@ -261,6 +262,18 @@ func discardInputAsync(ctx context.Context) (<-chan int, <-chan error) { return retValCh, errCh } +func createErrorConflictingArchSpecs(archCLI, archTag int) error { + var builder strings.Builder + fmt.Fprintf(&builder, "conflicting architecture specifications\n") + fmt.Fprintf(&builder, "--arch=%s but image tag specifies %s\n", + architecture.GetArchNameOCI(archCLI), + architecture.GetArchNameOCI(archTag)) + fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase) + + errMsg := builder.String() + return errors.New(errMsg) +} + func createErrorContainerNotFound(container string) error { var builder strings.Builder fmt.Fprintf(&builder, "container %s not found\n", container) @@ -281,6 +294,15 @@ func createErrorDistroWithoutRelease(distro string) error { return errors.New(errMsg) } +func createErrorImagePull(image, domain string) error { + var builder strings.Builder + fmt.Fprintf(&builder, "failed to pull image %s\n", image) + 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) + + return errors.New(builder.String()) +} + func createErrorInvalidContainer(containerArg string) error { var builder strings.Builder fmt.Fprintf(&builder, "invalid argument for '%s'\n", containerArg) @@ -345,6 +367,14 @@ func createErrorProfileDNotFound() error { return errors.New(errMsg) } +func createErrorSkopeoNotFound(imageFull string, archID int) error { + archName := architecture.GetArchNameOCI(archID) + return fmt.Errorf( + "Cannot inspect image %s for architecture %s: skopeo is not installed.\n"+ + "Skopeo is required for creating non-native architecture containers.", + imageFull, archName) +} + func createErrorSudoersDNotFound() error { const sudoersD = "/etc/sudoers.d" @@ -483,9 +513,40 @@ func poll(pollFn pollFunc, eventFD int32, fds ...int32) error { } } -func resolveContainerAndImageNames(container, containerArg, distroCLI, imageCLI, releaseCLI string) ( +func resolveArchitectureID(arch string, image string) (int, error) { + archID := architecture.NotSpecified + if arch != "" { + archIDParsed, err := architecture.ParseArgArchValue(arch) + if err != nil { + return architecture.NotSpecified, err + } + archID = archIDParsed + } + + if image != "" && utils.IsSupportedDistroImage(image) { + archIDFromTag := architecture.ImageReferenceGetArchFromTag(image) + + if archID == architecture.NotSpecified && archIDFromTag != architecture.NotSpecified { + logrus.Debug("non-native architecture was detected in the image tag -> cross-architecture approach is going to be used") + + archID = archIDFromTag + } else if archID != archIDFromTag && archIDFromTag != architecture.NotSpecified { + return architecture.NotSpecified, createErrorConflictingArchSpecs(archID, archIDFromTag) + } + } + + if archID == architecture.NotSpecified { + archID = architecture.HostArchID + } + + return archID, nil +} + +func resolveContainerAndImageNames(container, containerArg, distroCLI, imageCLI, releaseCLI string, archID int) ( string, string, string, error, ) { + containerWasEmpty := container == "" + container, image, release, err := utils.ResolveContainerAndImageNames(container, distroCLI, imageCLI, @@ -540,9 +601,35 @@ func resolveContainerAndImageNames(container, containerArg, distroCLI, imageCLI, } } + if containerWasEmpty && !architecture.HasContainerNativeArch(archID) { + archIDFromTag := architecture.ImageReferenceGetArchFromTag(image) + + if archIDFromTag == architecture.NotSpecified { + archName := architecture.GetArchNameOCI(archID) + if archName != "" { + container = container + "-" + archName + } + } + } + return container, image, release, nil } +func resolveImageNameWithArchitectureSuffix(image string, archID int) string { + if architecture.HasContainerNativeArch(archID) { + return image + } + + archIDFromTag := architecture.ImageReferenceGetArchFromTag(image) + isSupportedDistroImage := utils.IsSupportedDistroImage(image) + + if isSupportedDistroImage && archIDFromTag == architecture.NotSpecified { + return image + "-" + architecture.GetArchNameOCI(archID) + } + + return image +} + // showManual tries to open the specified manual page using man on stdout func showManual(manual string) error { manBinary, err := exec.LookPath("man") diff --git a/src/pkg/architecture/architecture.go b/src/pkg/architecture/architecture.go new file mode 100644 index 000000000..652ed224d --- /dev/null +++ b/src/pkg/architecture/architecture.go @@ -0,0 +1,293 @@ +/* + * Copyright © 2019 – 2026 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 architecture + +import ( + "debug/elf" + "errors" + "fmt" + "os" + "os/exec" + "runtime" + "strings" + + "github.com/containers/toolbox/pkg/utils" + "github.com/sirupsen/logrus" +) + +type Architecture struct { + ID int + NameBinfmt string + NameOCI string + Aliases []string + ELFMagic []byte + ELFMask []byte + + BinfmtFlags string + BinfmtName string + BinfmtMagicType string + BinfmtOffset string +} + +type Config struct { + ID int + QemuEmulatorPath string +} + +const ( + NotSpecified = iota + Aarch64 + Ppc64le + X86_64 +) + +var supportedArchitectures = map[int]Architecture{ + Aarch64: { + ID: Aarch64, + NameBinfmt: "aarch64", + NameOCI: "arm64", + Aliases: []string{"aarch64", "arm64"}, + ELFMagic: []byte{0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0xb7, 0x00}, + ELFMask: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xff}, + }, + Ppc64le: { + ID: Ppc64le, + NameBinfmt: "ppc64le", + NameOCI: "ppc64le", + Aliases: []string{"ppc64le"}, + ELFMagic: []byte{0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x15, 0x00}, + ELFMask: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0x00}, + }, + X86_64: { + ID: X86_64, + NameBinfmt: "x86_64", + NameOCI: "amd64", + Aliases: []string{"x86_64", "amd64"}, + ELFMagic: []byte{0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x3e, 0x00}, + ELFMask: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xfe, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xff}, + }, +} + +var ( + HostArchID int + supportedArgArchValues map[string]int +) + +func init() { + supportedArgArchValues = make(map[string]int) + for archID, arch := range supportedArchitectures { + for _, alias := range arch.Aliases { + supportedArgArchValues[alias] = archID + } + } + + HostArchID, _ = ParseArgArchValue(runtime.GOARCH) +} + +func GetArchConfigDefault() Config { + return Config{ + ID: HostArchID, + QemuEmulatorPath: "", + } +} + +func getArchitecture(archID int) (Architecture, bool) { + arch, exists := supportedArchitectures[archID] + return arch, exists +} + +func getArchNameBinfmt(arch int) string { + if arch == NotSpecified { + logrus.Warnf("Getting arch name for not specified architecture") + return "arch_not_specified" + } + if archObj, exists := supportedArchitectures[arch]; exists { + return archObj.NameBinfmt + } + return "" +} + +func GetArchNameOCI(arch int) string { + if arch == NotSpecified { + logrus.Warnf("Getting arch name for not specified architecture") + return "arch_not_specified" + } + if archObj, exists := supportedArchitectures[arch]; exists { + return archObj.NameOCI + } + return "" +} + +func HasContainerNativeArch(archID int) bool { + return archID == HostArchID +} + +func ImageReferenceGetArchFromTag(image string) int { + tag := utils.ImageReferenceGetTag(image) + + if tag == "" { + return NotSpecified + } + + i := strings.LastIndexByte(tag, '-') + if i == -1 { + return NotSpecified + } + + archInTag := tag[i+1:] + + for archID, arch := range supportedArchitectures { + if arch.NameBinfmt == archInTag || arch.NameOCI == archInTag { + return archID + } + } + + return NotSpecified +} + +func IsArchSupportedOnCreation(archID int) (string, error) { + archName := getArchNameBinfmt(archID) + archNameDebug := GetArchNameOCI(archID) + logrus.Debugf("Checking QEMU emulation support for architecture %s", archNameDebug) + + qemuBinaryPossibleNames := []string{ + fmt.Sprintf("qemu-%s-static", archName), + fmt.Sprintf("qemu-%s", archName), + } + + foundQemuBinaryPath := "" + for _, qemuName := range qemuBinaryPossibleNames { + qemuBinaryPath, err := exec.LookPath(qemuName) + + if err != nil { + if errors.Is(err, exec.ErrNotFound) { + continue + } + + return "", fmt.Errorf("failed to look up binary '%s': %w", qemuName, err) + } + + if isStaticallyLinkedELF(qemuBinaryPath) { + foundQemuBinaryPath = qemuBinaryPath + break + } + } + + if foundQemuBinaryPath == "" { + err := fmt.Errorf("The host system does not have the required support: No %s statically linked QEMU emulator binary found", archNameDebug) + return "", err + } + + if !validateBinfmtRegistration(archID, false) { + err := fmt.Errorf("The host system does not have the required support: No %s binfmt_misc registration found", archNameDebug) + return "", err + } + + return foundQemuBinaryPath, nil +} + +func IsArchSupportedOnInitialization(archID int, interpreterPath string) (string, error) { + archName := getArchNameBinfmt(archID) + archNameDebug := GetArchNameOCI(archID) + logrus.Debugf("Checking QEMU emulation support for architecture %s", archNameDebug) + + if isStaticallyLinkedELF(interpreterPath) { + if !validateBinfmtRegistration(archID, true) { + return "", fmt.Errorf("The host system does not have the required support: No %s binfmt_misc registration found", archNameDebug) + } + return interpreterPath, nil + } + + // Fallback: check standard locations on the host + logrus.Debugf("Interpreter at %s not found or not statically linked, checking fallback locations in '/run/host/usr/bin/'", interpreterPath) + fmt.Fprintf(os.Stderr, "Warning: QEMU emulator not found at expected path '%s', using fallback at '/run/host/usr/bin/'\n", interpreterPath) + + qemuBinaryPossiblePaths := []string{ + fmt.Sprintf("/run/host/usr/bin/qemu-%s-static", archName), + fmt.Sprintf("/run/host/usr/bin/qemu-%s", archName), + } + + for _, qemuPath := range qemuBinaryPossiblePaths { + if isStaticallyLinkedELF(qemuPath) { + logrus.Debugf("Found valid QEMU binary at %s", qemuPath) + + if !validateBinfmtRegistration(archID, true) { + return "", fmt.Errorf("The host system does not have the required support: No %s binfmt_misc registration found", archNameDebug) + } + return qemuPath, nil + } + } + + return "", fmt.Errorf("The host system does not have the required support: No %s statically linked QEMU emulator binary found", archNameDebug) +} + +func isStaticallyLinkedELF(filePath string) bool { + if !utils.PathExists(filePath) { + logrus.Debugf("File '%s' does not exist\n", filePath) + return false + } + + f, err := elf.Open(filePath) + if err != nil { + logrus.Debugf("File '%s' is not an ELF file\n", filePath) + return false + } + defer f.Close() + + // Check for PT_INTERP program header + for _, prog := range f.Progs { + if prog.Type == elf.PT_INTERP { + // Dynamically linked + logrus.Debugf("File '%s' is dynamically linked\n", filePath) + return false + } + } + + // Statically linked + return true +} + +func ParseArgArchValue(value string) (int, error) { + archID, exists := supportedArgArchValues[value] + if !exists { + return NotSpecified, fmt.Errorf("architecture '%s' is not supported by Toolbx", value) + } + + return archID, nil +} + +func validateBinfmtRegistration(archID int, withinContainer bool) bool { + archName := getArchNameBinfmt(archID) + inContainerPathPrefix := "" + + if withinContainer { + inContainerPathPrefix = "/run/host" + } + + qemuBinfmtPossiblePaths := []string{ + fmt.Sprintf("%s/proc/sys/fs/binfmt_misc/qemu-%s", inContainerPathPrefix, archName), + fmt.Sprintf("%s/proc/sys/fs/binfmt_misc/qemu-%s-static", inContainerPathPrefix, archName), + } + + for _, binfmtPath := range qemuBinfmtPossiblePaths { + if utils.PathExists(binfmtPath) { + logrus.Debugf("Architecture %s is supported", archName) + return true + } + } + return false +} diff --git a/src/pkg/architecture/binfmt_misc.go b/src/pkg/architecture/binfmt_misc.go new file mode 100644 index 000000000..3dc8eddbb --- /dev/null +++ b/src/pkg/architecture/binfmt_misc.go @@ -0,0 +1,151 @@ +/* + * Copyright © 2019 – 2026 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 architecture + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/containers/toolbox/pkg/shell" + "github.com/sirupsen/logrus" +) + +type Registration struct { + Name string + MagicType string + Offset string + Magic []byte + Mask []byte + Interpreter string + Flags string +} + +const ( + defaultMagicType = "M" + defaultFlags = "FC" + defaultOffset = "0" + binfmtMiscPath = "/proc/sys/fs/binfmt_misc" +) + +func (r *Registration) buildRegistrationString() string { + return fmt.Sprintf(":%s:%s:%s:%s:%s:%s:%s", + r.Name, r.MagicType, r.Offset, + bytesToEscapedString(r.Magic), + bytesToEscapedString(r.Mask), + r.Interpreter, r.Flags) +} + +func (r *Registration) register() error { + logrus.Debugf("Registering binfmt_misc for %s", r.Name) + + regString := r.buildRegistrationString() + logrus.Debugf("Registration string: %s", regString) + + if err := os.WriteFile(filepath.Join(binfmtMiscPath, "register"), []byte(regString), 0200); err != nil { + return fmt.Errorf("failed to register binfmt_misc handler: %w", err) + } + return nil +} + +func bytesToEscapedString(bytes []byte) string { + var result strings.Builder + for _, b := range bytes { + result.WriteString(fmt.Sprintf("\\x%02x", b)) + } + return result.String() +} + +func getDefaultRegistration(archID int, interpreterPath string) *Registration { + arch, exists := getArchitecture(archID) + if !exists { + return nil + } + + var name string + flags := defaultFlags + magicType := defaultMagicType + offset := defaultOffset + + if arch.BinfmtName != "" { + name = arch.BinfmtName + } else { + name = "qemu-" + arch.NameBinfmt + } + + if arch.BinfmtFlags != "" { + flags = arch.BinfmtFlags + } + + if arch.BinfmtMagicType != "" { + magicType = arch.BinfmtMagicType + } + + if arch.BinfmtOffset != "" { + offset = arch.BinfmtOffset + } + + interpreter := interpreterPath + if !strings.HasPrefix(interpreterPath, "/run/host/") { + interpreter = filepath.Join("/run/host", interpreter) + } + + return &Registration{ + Name: name, + MagicType: magicType, + Offset: offset, + Magic: arch.ELFMagic, + Mask: arch.ELFMask, + Interpreter: interpreter, + Flags: flags, + } +} + +func MountBinfmtMisc() error { + args := []string{ + "binfmt_misc", + "-t", + "binfmt_misc", + binfmtMiscPath, + } + + var stdout bytes.Buffer + + if err := shell.Run("mount", nil, &stdout, nil, args...); err != nil { + return fmt.Errorf("failed to mount binfmt_misc: %w", err) + } + + logrus.Debugf("Result of mount command: %s", stdout.String()) + + return nil +} + +func RegisterBinfmtMisc(archID int, interpreterPath string) error { + reg := getDefaultRegistration(archID, interpreterPath) + if reg == nil { + logrus.Debugf("Unable to register binfmt_misc for architecture '%s'", GetArchNameOCI(archID)) + return fmt.Errorf("Toolbx does not support architecture '%s'", GetArchNameOCI(archID)) + } + + if err := reg.register(); err != nil { + return err + } + + return nil +} diff --git a/src/pkg/shell/shell.go b/src/pkg/shell/shell.go index 00fa36997..6c9cfcacd 100644 --- a/src/pkg/shell/shell.go +++ b/src/pkg/shell/shell.go @@ -81,8 +81,49 @@ func RunContextWithExitCode(ctx context.Context, return 0, nil } +func RunContextWithExitCode2(ctx context.Context, + name string, + stdin io.Reader, + stdout, stderr io.Writer, + arg ...string) (int, error) { + + logLevel := logrus.GetLevel() + if stderr == nil && logLevel >= logrus.DebugLevel { + stderr = os.Stderr + } + + cmd := exec.CommandContext(ctx, name, arg...) + cmd.Stdin = stdin + cmd.Stdout = stdout + cmd.Stderr = stderr + + if err := cmd.Run(); err != nil { + exitCode := 1 + + if ctxErr := ctx.Err(); ctxErr != nil { + return 1, ctxErr + } + + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + exitCode = exitErr.ExitCode() + return exitCode, err + } + + return exitCode, err + } + + return 0, nil +} + func RunWithExitCode(name string, stdin io.Reader, stdout, stderr io.Writer, arg ...string) (int, error) { ctx := context.Background() exitCode, err := RunContextWithExitCode(ctx, name, stdin, stdout, stderr, arg...) return exitCode, err } + +func RunWithExitCode2(name string, stdin io.Reader, stdout, stderr io.Writer, arg ...string) (int, error) { + ctx := context.Background() + exitCode, err := RunContextWithExitCode2(ctx, name, stdin, stdout, stderr, arg...) + return exitCode, err +} diff --git a/src/pkg/skopeo/skopeo.go b/src/pkg/skopeo/skopeo.go index 8b15c7370..7fd52be90 100644 --- a/src/pkg/skopeo/skopeo.go +++ b/src/pkg/skopeo/skopeo.go @@ -20,24 +20,116 @@ import ( "bytes" "context" "encoding/json" + "errors" + "fmt" + "os" + "github.com/containers/toolbox/pkg/architecture" "github.com/containers/toolbox/pkg/shell" + "github.com/docker/go-units" + "github.com/sirupsen/logrus" ) type Layer struct { Size json.Number } + type Image struct { - LayersData []Layer + Architecture string `json:"Architecture"` + LayersData []Layer + NameFull string +} + +func (image *Image) GetSize() (float64, error) { + var imageSizeFloat float64 + + if image.LayersData == nil { + return -1, errors.New("'skopeo inspect' did not have LayersData") + } + + for _, layer := range image.LayersData { + if layerSize, err := layer.Size.Float64(); err != nil { + return -1, err + } else { + imageSizeFloat += layerSize + } + } + + return imageSizeFloat, nil +} + +func (image *Image) GetSizeHuman() (string, error) { + imageSizeFloat, err := image.GetSize() + if err != nil { + return "", err + } + + imageSizeHuman := units.HumanSize(imageSizeFloat) + return imageSizeHuman, nil +} + +func (image *Image) VerifyArchitectureMatch(expectedArchID int) error { + expectedArchName := architecture.GetArchNameOCI(expectedArchID) + logrus.Debugf("Verifying image %s supports architecture %s", image.NameFull, expectedArchName) + + actualArchID, err := architecture.ParseArgArchValue(image.Architecture) + if err != nil { + return err + } + + if actualArchID != expectedArchID { + // Single-arch image mismatch + return fmt.Errorf("image %s is a single-architecture image for %s, but %s was requested", + image.NameFull, image.Architecture, expectedArchName) + } + + logrus.Debugf("Architecture verification passed: %s", expectedArchName) + return nil } -func Inspect(ctx context.Context, target string) (*Image, error) { +func CopyOverrideArch(source, destination string, archID int, authfile string) error { + + destinationWithTransport := "containers-storage:" + destination + sourceWithTransport := "docker://" + source + args := []string{"copy", "--override-arch", architecture.GetArchNameOCI(archID)} + + if authfile != "" { + args = append(args, []string{"--src-authfile", authfile}...) + } + + args = append(args, sourceWithTransport, destinationWithTransport) + + if logrus.GetLevel() < logrus.DebugLevel { + if err := shell.Run("skopeo", nil, nil, nil, args...); err != nil { + return err + } + } else { + if err := shell.Run("skopeo", nil, os.Stderr, nil, args...); err != nil { + return err + } + } + + return nil +} + +func Inspect(ctx context.Context, target string, archID int, authfile string) (*Image, error) { var stdout bytes.Buffer targetWithTransport := "docker://" + target - args := []string{"inspect", "--format", "json", targetWithTransport} + args := []string{"inspect", "--format", "json"} + + if !architecture.HasContainerNativeArch(archID) { + archName := architecture.GetArchNameOCI(archID) + args = append(args, []string{"--override-arch", archName}...) + } - if err := shell.RunContext(ctx, "skopeo", nil, &stdout, nil, args...); err != nil { + if authfile != "" { + args = append(args, []string{"--authfile", authfile}...) + } + + args = append(args, targetWithTransport) + + if _, err := shell.RunContextWithExitCode2(ctx, "skopeo", nil, &stdout, nil, args...); err != nil { return nil, err } @@ -47,5 +139,7 @@ func Inspect(ctx context.Context, target string) (*Image, error) { return nil, err } + image.NameFull = target + return &image, nil } diff --git a/src/pkg/utils/utils.go b/src/pkg/utils/utils.go index f3de23b1b..c88f8ef67 100644 --- a/src/pkg/utils/utils.go +++ b/src/pkg/utils/utils.go @@ -664,6 +664,20 @@ func IsP11KitClientPresent() (bool, error) { return false, err } +func IsSupportedDistroImage(image string) bool { + basename := ImageReferenceGetBasename(image) + if basename == "" { + return false + } + + for _, distroObj := range supportedDistros { + if distroObj.ImageBasename == basename { + return true + } + } + return false +} + func SetUpConfiguration() error { logrus.Debug("Setting up configuration")