Skip to content

Commit 3413e52

Browse files
committed
Add convert command using bib
Add convert command that uses bootc image builder (bib) to create disk images Signed-off-by: German Maglione <[email protected]>
1 parent 79c9266 commit 3413e52

File tree

2 files changed

+272
-0
lines changed

2 files changed

+272
-0
lines changed

cmd/convert.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
8+
"github.com/containers/podman-bootc/pkg/bib"
9+
"github.com/containers/podman-bootc/pkg/user"
10+
"github.com/containers/podman-bootc/pkg/utils"
11+
12+
"github.com/sirupsen/logrus"
13+
"github.com/spf13/cobra"
14+
)
15+
16+
var (
17+
convertCmd = &cobra.Command{
18+
Use: "convert <image>",
19+
Short: "Creates a disk image using bootc-image-builder",
20+
Long: "Creates a disk image using bootc-image-builder",
21+
Args: cobra.ExactArgs(1),
22+
RunE: doConvert,
23+
}
24+
options bib.BuildOption
25+
quiet bool
26+
)
27+
28+
func init() {
29+
RootCmd.AddCommand(convertCmd)
30+
convertCmd.Flags().BoolVar(&quiet, "quiet", false, "Suppress output from disk image creation")
31+
convertCmd.Flags().StringVar(&options.Config, "config", "", "Image builder config file")
32+
convertCmd.Flags().StringVar(&options.Output, "output", ".", "output directory (default \".\")")
33+
// Corresponds to bib '--rootfs', we don't use 'rootfs' so to not be confused with podman's 'rootfs' options
34+
// Note: we cannot provide a default value for the filesystem, since this options will overwrite the one defined in
35+
// the image
36+
convertCmd.Flags().StringVar(&options.Filesystem, "filesystem", "", "Overrides the root filesystem (e.g. xfs, btrfs, ext4)")
37+
// Corresponds to bib '--type', using '--format' to be consistent with podman
38+
convertCmd.Flags().StringVar(&options.Format, "format", "qcow2", "Disk image type (ami, anaconda-iso, iso, qcow2, raw, vmdk) [default: qcow2]")
39+
// Corresponds to bib '--target-arch', using '--arch' to be consistent with podman
40+
convertCmd.Flags().StringVar(&options.Arch, "arch", "", "Build for the given target architecture (experimental)")
41+
42+
options.BibContainerImage = os.Getenv("PODMAN_BOOTC_BIB_IMAGE")
43+
options.BibExtraArgs = strings.Fields(os.Getenv("PODMAN_BOOTC_BIB_EXTRA"))
44+
}
45+
46+
func doConvert(_ *cobra.Command, args []string) (err error) {
47+
//get user info who is running the podman bootc command
48+
usr, err := user.NewUser()
49+
if err != nil {
50+
return fmt.Errorf("unable to get user: %w", err)
51+
}
52+
53+
machine, err := utils.GetMachineContext()
54+
if err != nil {
55+
println(utils.PodmanMachineErrorMessage)
56+
logrus.Errorf("failed to connect to podman machine. Is podman machine running?\n%s", err)
57+
return err
58+
}
59+
60+
idOrName := args[0]
61+
err = bib.Build(machine.Ctx, usr, idOrName, quiet, options)
62+
if err != nil {
63+
return err
64+
}
65+
66+
return nil
67+
}

pkg/bib/build.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
package bib
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
11+
"github.com/containers/podman-bootc/pkg/user"
12+
"github.com/containers/podman-bootc/pkg/utils"
13+
14+
"github.com/containers/podman/v5/pkg/bindings/containers"
15+
"github.com/containers/podman/v5/pkg/domain/entities/types"
16+
"github.com/containers/podman/v5/pkg/specgen"
17+
"github.com/opencontainers/runtime-spec/specs-go"
18+
"github.com/sirupsen/logrus"
19+
)
20+
21+
const defaultBibImage = "quay.io/centos-bootc/bootc-image-builder"
22+
23+
type BuildOption struct {
24+
BibContainerImage string
25+
Config string
26+
Output string
27+
Filesystem string
28+
Format string
29+
Arch string
30+
BibExtraArgs []string
31+
}
32+
33+
func Build(ctx context.Context, user user.User, imageNameOrId string, quiet bool, buildOption BuildOption) error {
34+
outputInfo, err := os.Stat(buildOption.Output)
35+
if err != nil {
36+
return fmt.Errorf("output directory %s: %w", buildOption.Output, err)
37+
}
38+
39+
if !outputInfo.IsDir() {
40+
return fmt.Errorf("%s is not a directory ", buildOption.Output)
41+
}
42+
43+
_, err = os.Stat(buildOption.Config)
44+
if err != nil {
45+
return fmt.Errorf("config file %s: %w", buildOption.Config, err)
46+
}
47+
48+
// Let's convert both the config file and the output directory to their absolute paths.
49+
buildOption.Output, err = filepath.Abs(buildOption.Output)
50+
if err != nil {
51+
return fmt.Errorf("getting output directory absolute path: %w", err)
52+
}
53+
54+
buildOption.Config, err = filepath.Abs(buildOption.Config)
55+
if err != nil {
56+
return fmt.Errorf("getting config file absolute path: %w", err)
57+
}
58+
59+
// We assume the user's home directory is accessible from the podman machine VM, this
60+
// will fail if any of the output or the config file are outside the user's home directory.
61+
if !strings.HasPrefix(buildOption.Output, user.HomeDir()) {
62+
return errors.New("the output directory must be inside the user's home directory")
63+
}
64+
65+
if !strings.HasPrefix(buildOption.Config, user.HomeDir()) {
66+
return errors.New("the output directory must be inside the user's home directory")
67+
}
68+
69+
// Let's pull the bootc image container if necessary
70+
imageInspect, err := utils.PullAndInspect(ctx, imageNameOrId)
71+
if err != nil {
72+
return fmt.Errorf("pulling image: %w", err)
73+
}
74+
imageFullName := imageInspect.RepoTags[0]
75+
76+
if buildOption.BibContainerImage == "" {
77+
label, found := imageInspect.Labels["bootc.diskimage-builder"]
78+
if found && label != "" {
79+
buildOption.BibContainerImage = label
80+
} else {
81+
buildOption.BibContainerImage = defaultBibImage
82+
}
83+
}
84+
85+
// Let's pull the Bootc Image Builder if necessary
86+
_, err = utils.PullAndInspect(ctx, buildOption.BibContainerImage)
87+
if err != nil {
88+
return fmt.Errorf("pulling bootc image builder image: %w", err)
89+
}
90+
91+
// BIB doesn't work with just the image ID or short name, it requires the image full name
92+
bibContainer, err := createBibContainer(ctx, buildOption.BibContainerImage, imageFullName, buildOption)
93+
if err != nil {
94+
return fmt.Errorf("failed to create image builder container: %w", err)
95+
}
96+
97+
err = containers.Start(ctx, bibContainer.ID, &containers.StartOptions{})
98+
if err != nil {
99+
return fmt.Errorf("failed to start image builder container: %w", err)
100+
}
101+
102+
// Ensure we've cancelled the container attachment when exiting this function, as
103+
// it takes over stdout/stderr handling
104+
attachCancelCtx, cancelAttach := context.WithCancel(ctx)
105+
defer cancelAttach()
106+
107+
if !quiet {
108+
attachOpts := new(containers.AttachOptions).WithStream(true)
109+
if err := containers.Attach(attachCancelCtx, bibContainer.ID, os.Stdin, os.Stdout, os.Stderr, nil, attachOpts); err != nil {
110+
return fmt.Errorf("attaching image builder container: %w", err)
111+
}
112+
}
113+
114+
exitCode, err := containers.Wait(ctx, bibContainer.ID, nil)
115+
if err != nil {
116+
return fmt.Errorf("failed to wait for image builder container: %w", err)
117+
}
118+
119+
if exitCode != 0 {
120+
return fmt.Errorf("failed to run image builder")
121+
}
122+
123+
return nil
124+
}
125+
126+
func createBibContainer(ctx context.Context, bibContainerImage, imageFullName string, buildOption BuildOption) (types.ContainerCreateResponse, error) {
127+
privileged := true
128+
autoRemove := true
129+
labelNested := true
130+
terminal := true // Allocate pty so we can show progress bars, spinners etc.
131+
132+
bibArgs := bibArguments(imageFullName, buildOption)
133+
134+
s := &specgen.SpecGenerator{
135+
ContainerBasicConfig: specgen.ContainerBasicConfig{
136+
Remove: &autoRemove,
137+
Annotations: map[string]string{"io.podman.annotations.label": "type:unconfined_t"},
138+
Terminal: &terminal,
139+
Command: bibArgs,
140+
SdNotifyMode: "container", // required otherwise crun will fail to open the sd-bus
141+
},
142+
ContainerStorageConfig: specgen.ContainerStorageConfig{
143+
Image: bibContainerImage,
144+
Mounts: []specs.Mount{
145+
{
146+
Source: buildOption.Config,
147+
Destination: "/config.toml",
148+
Type: "bind",
149+
},
150+
{
151+
Source: buildOption.Output,
152+
Destination: "/output",
153+
Type: "bind",
154+
Options: []string{"nosuid", "nodev"},
155+
},
156+
{
157+
Source: "/var/lib/containers/storage",
158+
Destination: "/var/lib/containers/storage",
159+
Type: "bind",
160+
},
161+
},
162+
},
163+
ContainerSecurityConfig: specgen.ContainerSecurityConfig{
164+
Privileged: &privileged,
165+
LabelNested: &labelNested,
166+
SelinuxOpts: []string{"type:unconfined_t"},
167+
},
168+
ContainerNetworkConfig: specgen.ContainerNetworkConfig{
169+
NetNS: specgen.Namespace{
170+
NSMode: specgen.Bridge,
171+
},
172+
},
173+
}
174+
175+
logrus.Debugf("Installing %s using %s", imageFullName, bibContainerImage)
176+
createResponse, err := containers.CreateWithSpec(ctx, s, &containers.CreateOptions{})
177+
if err != nil {
178+
return createResponse, fmt.Errorf("failed to create image builder container: %w", err)
179+
}
180+
return createResponse, nil
181+
}
182+
183+
func bibArguments(imageNameOrId string, buildOption BuildOption) []string {
184+
args := []string{
185+
"--local", // we pull the image if necessary, so don't pull it from a registry
186+
}
187+
188+
if buildOption.Filesystem != "" {
189+
args = append(args, "--rootfs", buildOption.Filesystem)
190+
}
191+
192+
if buildOption.Arch != "" {
193+
args = append(args, "--target-arch", buildOption.Arch)
194+
}
195+
196+
if buildOption.Format != "" {
197+
args = append(args, "--type", buildOption.Format)
198+
}
199+
200+
args = append(args, buildOption.BibExtraArgs...)
201+
args = append(args, imageNameOrId)
202+
203+
logrus.Debugf("BIB arguments: %v", args)
204+
return args
205+
}

0 commit comments

Comments
 (0)