-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add convert command that uses bootc image builder (bib) to create disk images Signed-off-by: German Maglione <[email protected]>
- Loading branch information
Showing
2 changed files
with
333 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package cmd | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"os" | ||
|
||
"github.com/containers/podman-bootc/pkg/bib" | ||
"github.com/containers/podman-bootc/pkg/user" | ||
"github.com/containers/podman-bootc/pkg/utils" | ||
|
||
"github.com/containers/podman/v5/pkg/bindings" | ||
"github.com/sirupsen/logrus" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
const bibImage = "quay.io/centos-bootc/bootc-image-builder:latest" | ||
|
||
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 | ||
amiCfg bib.AmiConfig | ||
quiet bool | ||
bibContainerImage string | ||
) | ||
|
||
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)") | ||
|
||
// Extra AMi flags | ||
convertCmd.Flags().StringVar(&amiCfg.Name, "aws-ami-name", "", "Name for the AMI in AWS (only for format=ami)") | ||
convertCmd.Flags().StringVar(&amiCfg.Bucket, "aws-bucket", "", "Target S3 bucket name for intermediate storage when creating AMI (only for format=ami)") | ||
convertCmd.Flags().StringVar(&amiCfg.Region, "aws-region", "", "Target region for AWS uploads (only for format=ami)") | ||
|
||
bibContainerImage = os.Getenv("PODMAN_BOOTC_BIB_IMAGE") | ||
if bibContainerImage == "" { | ||
bibContainerImage = bibImage | ||
} | ||
} | ||
|
||
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) | ||
} | ||
|
||
//podman machine connection | ||
machineInfo, err := utils.GetMachineInfo(user) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if machineInfo == nil { | ||
println(utils.PodmanMachineErrorMessage) | ||
return errors.New("rootful podman machine is required, please run 'podman machine init --rootful'") | ||
} | ||
|
||
if !machineInfo.Rootful { | ||
println(utils.PodmanMachineErrorMessage) | ||
return errors.New("rootful podman machine is required, please run 'podman machine set --rootful'") | ||
} | ||
|
||
if _, err := os.Stat(machineInfo.PodmanSocket); err != nil { | ||
println(utils.PodmanMachineErrorMessage) | ||
logrus.Errorf("podman machine socket is missing. Is podman machine running?\n%s", err) | ||
return err | ||
} | ||
|
||
ctx, err := bindings.NewConnectionWithIdentity( | ||
context.Background(), | ||
fmt.Sprintf("unix://%s", machineInfo.PodmanSocket), | ||
machineInfo.SSHIdentityPath, | ||
true) | ||
if err != nil { | ||
println(utils.PodmanMachineErrorMessage) | ||
logrus.Errorf("failed to connect to the podman socket. Is podman machine running?\n%s", err) | ||
return err | ||
} | ||
|
||
idOrName := args[0] | ||
err = bib.Build(ctx, quiet, bibContainerImage, idOrName, user, options, amiCfg) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,227 @@ | ||
package bib | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
|
||
"github.com/containers/podman-bootc/pkg/user" | ||
|
||
"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" | ||
) | ||
|
||
type BuildOption struct { | ||
Config string | ||
Output string | ||
Filesystem string | ||
Format string | ||
Arch string | ||
} | ||
|
||
type AmiConfig struct { | ||
Name string | ||
Bucket string | ||
Region string | ||
} | ||
|
||
func Build(ctx context.Context, quiet bool, bibContainerImage, imageNameOrId string, user user.User, buildOption BuildOption, amiCnfg AmiConfig) 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 Builder if necessary | ||
_, err = pullImage(ctx, bibContainerImage) | ||
if err != nil { | ||
return fmt.Errorf("pulling bootc image builder image: %w", err) | ||
} | ||
|
||
imageFullName, err := pullImage(ctx, imageNameOrId) | ||
if err != nil { | ||
return fmt.Errorf("pulling image: %w", err) | ||
} | ||
|
||
bibContainer, err := createBibContainer(ctx, bibContainerImage, imageFullName, buildOption, amiCnfg) | ||
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, amiCnfg AmiConfig) (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, amiCnfg) | ||
|
||
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, | ||
}, | ||
}, | ||
} | ||
|
||
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, amiCfg AmiConfig) []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) | ||
} | ||
|
||
// AMI specific options | ||
if amiCfg.Name != "" { | ||
args = append(args, "--aws-ami-name", amiCfg.Name) | ||
} | ||
|
||
if amiCfg.Bucket != "" { | ||
args = append(args, "--aws-bucket", amiCfg.Bucket) | ||
} | ||
|
||
if amiCfg.Region != "" { | ||
args = append(args, "--aws-region", amiCfg.Region) | ||
} | ||
|
||
args = append(args, imageNameOrId) | ||
return args | ||
} |