-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add convert command using bib #58
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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]") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should iso be dropped as it's an alias for anaconda-iso? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it was also removed from the README, see: https://github.com/osbuild/bootc-image-builder?tab=readme-ov-file#-image-types |
||
// 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 | ||
usr, 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, usr, idOrName, quiet, options) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,205 @@ | ||||||
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/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 { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should error out early here when --output or --config haven't been set. For the config, we could consider creating an empty temp file and pass that on? |
||||||
outputInfo, err := os.Stat(buildOption.Output) | ||||||
if err != nil { | ||||||
return fmt.Errorf("output directory %s: %w", buildOption.Output, err) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
|
||||||
if !outputInfo.IsDir() { | ||||||
return fmt.Errorf("%s is not a directory ", buildOption.Output) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
|
||||||
_, err = os.Stat(buildOption.Config) | ||||||
if err != nil { | ||||||
return fmt.Errorf("config file %s: %w", buildOption.Config, err) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
|
||||||
// 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 | ||||||
} | ||||||
|
||||||
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", | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm assuming that this command is always running in sudo / root and we shouldn't use This would cause an issue if a user did Unsure to be honest how this should be worked around / I guess we assume that podman-bootc is always running privileged? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
No; we assume the container is running privileged inside podman machine. |
||||||
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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. --local is always true now and is being deprecated, so no need for this. |
||||||
} | ||||||
|
||||||
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 | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To avoid it being displayed twice.