Skip to content

Commit

Permalink
Add convert command using bib
Browse files Browse the repository at this point in the history
Add convert command that uses bootc image builder (bib)
to create disk images

Signed-off-by: German Maglione <[email protected]>
  • Loading branch information
germag committed Jul 9, 2024
1 parent 664e527 commit 33716b4
Show file tree
Hide file tree
Showing 2 changed files with 297 additions and 0 deletions.
67 changes: 67 additions & 0 deletions cmd/convert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package cmd

import (
"fmt"
"os"
"strings"

"github.com/containers/podman-bootc/pkg/bib"
"github.com/containers/podman-bootc/pkg/user"
"github.com/containers/podman-bootc/pkg/utils"

"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

var (
convertCmd = &cobra.Command{
Use: "convert <image>",
Short: "Creates a disk image using bootc-image-builder",
Long: "Creates a disk image using bootc-image-builder",
Args: cobra.ExactArgs(1),
RunE: doConvert,
}
options bib.BuildOption
quiet bool
)

func init() {
RootCmd.AddCommand(convertCmd)
convertCmd.Flags().BoolVar(&quiet, "quiet", false, "Suppress output from disk image creation")
convertCmd.Flags().StringVar(&options.Config, "config", "", "Image builder config file")
convertCmd.Flags().StringVar(&options.Output, "output", ".", "output directory (default \".\")")
// Corresponds to bib '--rootfs', we don't use 'rootfs' so to not be confused with podman's 'rootfs' options
// Note: we cannot provide a default value for the filesystem, since this options will overwrite the one defined in
// the image
convertCmd.Flags().StringVar(&options.Filesystem, "filesystem", "", "Overrides the root filesystem (e.g. xfs, btrfs, ext4)")
// Corresponds to bib '--type', using '--format' to be consistent with podman
convertCmd.Flags().StringVar(&options.Format, "format", "qcow2", "Disk image type (ami, anaconda-iso, iso, qcow2, raw, vmdk) [default: qcow2]")
// Corresponds to bib '--target-arch', using '--arch' to be consistent with podman
convertCmd.Flags().StringVar(&options.Arch, "arch", "", "Build for the given target architecture (experimental)")

options.BibContainerImage = os.Getenv("PODMAN_BOOTC_BIB_IMAGE")
options.BibExtraArgs = strings.Fields(os.Getenv("PODMAN_BOOTC_BIB_EXTRA"))
}

func doConvert(_ *cobra.Command, args []string) (err error) {
//get user info who is running the podman bootc command
user, err := user.NewUser()
if err != nil {
return fmt.Errorf("unable to get user: %w", err)
}

machine, err := utils.GetMachineContext()
if err != nil {
println(utils.PodmanMachineErrorMessage)
logrus.Errorf("failed to connect to podman machine. Is podman machine running?\n%s", err)
return err
}

idOrName := args[0]
err = bib.Build(machine.Ctx, user, idOrName, quiet, options)
if err != nil {
return err
}

return nil
}
230 changes: 230 additions & 0 deletions pkg/bib/build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package bib

import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/containers/podman-bootc/pkg/user"
"github.com/containers/podman-bootc/pkg/utils"

"github.com/containers/podman/v5/pkg/bindings/containers"
"github.com/containers/podman/v5/pkg/bindings/images"
"github.com/containers/podman/v5/pkg/domain/entities/types"
"github.com/containers/podman/v5/pkg/specgen"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
)

const defaultBibImage = "quay.io/centos-bootc/bootc-image-builder"

type BuildOption struct {
BibContainerImage string
Config string
Output string
Filesystem string
Format string
Arch string
BibExtraArgs []string
}

func Build(ctx context.Context, user user.User, imageNameOrId string, quiet bool, buildOption BuildOption) error {
outputInfo, err := os.Stat(buildOption.Output)
if err != nil {
return fmt.Errorf("output directory %s: %w", buildOption.Output, err)
}

if !outputInfo.IsDir() {
return fmt.Errorf("%s is not a directory ", buildOption.Output)
}

_, err = os.Stat(buildOption.Config)
if err != nil {
return fmt.Errorf("config file %s: %w", buildOption.Config, err)
}

// Let's convert both the config file and the output directory to their absolute paths.
buildOption.Output, err = filepath.Abs(buildOption.Output)
if err != nil {
return fmt.Errorf("getting output directory absolute path: %w", err)
}

buildOption.Config, err = filepath.Abs(buildOption.Config)
if err != nil {
return fmt.Errorf("getting config file absolute path: %w", err)
}

// We assume the user's home directory is accessible from the podman machine VM, this
// will fail if any of the output or the config file are outside the user's home directory.
if !strings.HasPrefix(buildOption.Output, user.HomeDir()) {
return errors.New("the output directory must be inside the user's home directory")
}

if !strings.HasPrefix(buildOption.Config, user.HomeDir()) {
return errors.New("the output directory must be inside the user's home directory")
}

// Let's pull the bootc image container if necessary
imageInspect, err := utils.PullAndInspect(ctx, imageNameOrId)
if err != nil {
return fmt.Errorf("pulling image: %w", err)
}
imageFullName := imageInspect.RepoTags[0]

if buildOption.BibContainerImage == "" {
label, found := imageInspect.Labels["bootc.diskimage-builder"]
if found && label != "" {
buildOption.BibContainerImage = label
} else {
buildOption.BibContainerImage = defaultBibImage
}
}

// Let's pull the Bootc Image Builder if necessary
_, err = utils.PullAndInspect(ctx, buildOption.BibContainerImage)
if err != nil {
return fmt.Errorf("pulling bootc image builder image: %w", err)
}

// BIB doesn't work with just the image ID or short name, it requires the image full name
bibContainer, err := createBibContainer(ctx, buildOption.BibContainerImage, imageFullName, buildOption)
if err != nil {
return fmt.Errorf("failed to create image builder container: %w", err)
}

err = containers.Start(ctx, bibContainer.ID, &containers.StartOptions{})
if err != nil {
return fmt.Errorf("failed to start image builder container: %w", err)
}

// Ensure we've cancelled the container attachment when exiting this function, as
// it takes over stdout/stderr handling
attachCancelCtx, cancelAttach := context.WithCancel(ctx)
defer cancelAttach()

if !quiet {
attachOpts := new(containers.AttachOptions).WithStream(true)
if err := containers.Attach(attachCancelCtx, bibContainer.ID, os.Stdin, os.Stdout, os.Stderr, nil, attachOpts); err != nil {
return fmt.Errorf("attaching image builder container: %w", err)
}
}

exitCode, err := containers.Wait(ctx, bibContainer.ID, nil)
if err != nil {
return fmt.Errorf("failed to wait for image builder container: %w", err)
}

if exitCode != 0 {
return fmt.Errorf("failed to run image builder")
}

return nil
}

// pullImage fetches the container image if not present
func pullImage(ctx context.Context, imageNameOrId string) (imageFullName string, err error) {
pullPolicy := "missing"
ids, err := images.Pull(ctx, imageNameOrId, &images.PullOptions{Policy: &pullPolicy})
if err != nil {
return "", fmt.Errorf("failed to pull image: %w", err)
}

if len(ids) == 0 {
return "", fmt.Errorf("no ids returned from image pull")
}

if len(ids) > 1 {
return "", fmt.Errorf("multiple ids returned from image pull")
}

imageInfo, err := images.GetImage(ctx, imageNameOrId, &images.GetOptions{})
if err != nil {
return "", fmt.Errorf("failed to get image: %w", err)
}

return imageInfo.RepoTags[0], nil
}

func createBibContainer(ctx context.Context, bibContainerImage, imageFullName string, buildOption BuildOption) (types.ContainerCreateResponse, error) {
privileged := true
autoRemove := true
labelNested := true
terminal := true // Allocate pty so we can show progress bars, spinners etc.

bibArgs := bibArguments(imageFullName, buildOption)

s := &specgen.SpecGenerator{
ContainerBasicConfig: specgen.ContainerBasicConfig{
Remove: &autoRemove,
Annotations: map[string]string{"io.podman.annotations.label": "type:unconfined_t"},
Terminal: &terminal,
Command: bibArgs,
SdNotifyMode: "container", // required otherwise crun will fail to open the sd-bus
},
ContainerStorageConfig: specgen.ContainerStorageConfig{
Image: bibContainerImage,
Mounts: []specs.Mount{
{
Source: buildOption.Config,
Destination: "/config.toml",
Type: "bind",
},
{
Source: buildOption.Output,
Destination: "/output",
Type: "bind",
Options: []string{"nosuid", "nodev"},
},
{
Source: "/var/lib/containers/storage",
Destination: "/var/lib/containers/storage",
Type: "bind",
},
},
},
ContainerSecurityConfig: specgen.ContainerSecurityConfig{
Privileged: &privileged,
LabelNested: &labelNested,
SelinuxOpts: []string{"type:unconfined_t"},
},
ContainerNetworkConfig: specgen.ContainerNetworkConfig{
NetNS: specgen.Namespace{
NSMode: specgen.Bridge,
},
},
}

logrus.Debugf("Installing %s using %s", imageFullName, bibContainerImage)
createResponse, err := containers.CreateWithSpec(ctx, s, &containers.CreateOptions{})
if err != nil {
return createResponse, fmt.Errorf("failed to create image builder container: %w", err)
}
return createResponse, nil
}

func bibArguments(imageNameOrId string, buildOption BuildOption) []string {
args := []string{
"--local", // we pull the image if necessary, so don't pull it from a registry
}

if buildOption.Filesystem != "" {
args = append(args, "--rootfs", buildOption.Filesystem)
}

if buildOption.Arch != "" {
args = append(args, "--target-arch", buildOption.Arch)
}

if buildOption.Format != "" {
args = append(args, "--type", buildOption.Format)
}

args = append(args, buildOption.BibExtraArgs...)
args = append(args, imageNameOrId)

logrus.Debugf("BIB arguments: %v", args)
return args
}

0 comments on commit 33716b4

Please sign in to comment.