Skip to content

Commit 33716b4

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 664e527 commit 33716b4

File tree

2 files changed

+297
-0
lines changed

2 files changed

+297
-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+
user, 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, user, idOrName, quiet, options)
62+
if err != nil {
63+
return err
64+
}
65+
66+
return nil
67+
}

pkg/bib/build.go

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

0 commit comments

Comments
 (0)