Skip to content

Commit 9e327cb

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 70116ee commit 9e327cb

File tree

2 files changed

+333
-0
lines changed

2 files changed

+333
-0
lines changed

cmd/convert.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"os"
8+
9+
"github.com/containers/podman-bootc/pkg/bib"
10+
"github.com/containers/podman-bootc/pkg/user"
11+
"github.com/containers/podman-bootc/pkg/utils"
12+
13+
"github.com/containers/podman/v5/pkg/bindings"
14+
"github.com/sirupsen/logrus"
15+
"github.com/spf13/cobra"
16+
)
17+
18+
const bibImage = "quay.io/centos-bootc/bootc-image-builder:latest"
19+
20+
var (
21+
convertCmd = &cobra.Command{
22+
Use: "convert <image>",
23+
Short: "Creates a disk image using bootc-image-builder",
24+
Long: "Creates a disk image using bootc-image-builder",
25+
Args: cobra.ExactArgs(1),
26+
RunE: doConvert,
27+
}
28+
options bib.BuildOption
29+
amiCfg bib.AmiConfig
30+
quiet bool
31+
bibContainerImage string
32+
)
33+
34+
func init() {
35+
RootCmd.AddCommand(convertCmd)
36+
convertCmd.Flags().BoolVar(&quiet, "quiet", false, "Suppress output from disk image creation")
37+
convertCmd.Flags().StringVar(&options.Config, "config", "", "Image builder config file")
38+
convertCmd.Flags().StringVar(&options.Output, "output", ".", "output directory (default \".\")")
39+
// Corresponds to bib '--rootfs', we don't use 'rootfs' so to not be confused with podman's 'rootfs' options
40+
// Note: we cannot provide a default value for the filesystem, since this options will overwrite the one defined in
41+
// the image
42+
convertCmd.Flags().StringVar(&options.Filesystem, "filesystem", "", "Overrides the root filesystem (e.g. xfs, btrfs, ext4)")
43+
// Corresponds to bib '--type', using '--format' to be consistent with podman
44+
convertCmd.Flags().StringVar(&options.Format, "format", "qcow2", "Disk image type (ami, anaconda-iso, iso, qcow2, raw, vmdk) [default: qcow2]")
45+
// Corresponds to bib '--target-arch', using '--arch' to be consistent with podman
46+
convertCmd.Flags().StringVar(&options.Arch, "arch", "", "Build for the given target architecture (experimental)")
47+
48+
// Extra AMi flags
49+
convertCmd.Flags().StringVar(&amiCfg.Name, "aws-ami-name", "", "Name for the AMI in AWS (only for format=ami)")
50+
convertCmd.Flags().StringVar(&amiCfg.Bucket, "aws-bucket", "", "Target S3 bucket name for intermediate storage when creating AMI (only for format=ami)")
51+
convertCmd.Flags().StringVar(&amiCfg.Region, "aws-region", "", "Target region for AWS uploads (only for format=ami)")
52+
53+
bibContainerImage = os.Getenv("PODMAN_BOOTC_BIB_IMAGE")
54+
if bibContainerImage == "" {
55+
bibContainerImage = bibImage
56+
}
57+
}
58+
59+
func doConvert(_ *cobra.Command, args []string) (err error) {
60+
//get user info who is running the podman bootc command
61+
user, err := user.NewUser()
62+
if err != nil {
63+
return fmt.Errorf("unable to get user: %w", err)
64+
}
65+
66+
//podman machine connection
67+
machineInfo, err := utils.GetMachineInfo(user)
68+
if err != nil {
69+
return err
70+
}
71+
72+
if machineInfo == nil {
73+
println(utils.PodmanMachineErrorMessage)
74+
return errors.New("rootful podman machine is required, please run 'podman machine init --rootful'")
75+
}
76+
77+
if !machineInfo.Rootful {
78+
println(utils.PodmanMachineErrorMessage)
79+
return errors.New("rootful podman machine is required, please run 'podman machine set --rootful'")
80+
}
81+
82+
if _, err := os.Stat(machineInfo.PodmanSocket); err != nil {
83+
println(utils.PodmanMachineErrorMessage)
84+
logrus.Errorf("podman machine socket is missing. Is podman machine running?\n%s", err)
85+
return err
86+
}
87+
88+
ctx, err := bindings.NewConnectionWithIdentity(
89+
context.Background(),
90+
fmt.Sprintf("unix://%s", machineInfo.PodmanSocket),
91+
machineInfo.SSHIdentityPath,
92+
true)
93+
if err != nil {
94+
println(utils.PodmanMachineErrorMessage)
95+
logrus.Errorf("failed to connect to the podman socket. Is podman machine running?\n%s", err)
96+
return err
97+
}
98+
99+
idOrName := args[0]
100+
err = bib.Build(ctx, quiet, bibContainerImage, idOrName, user, options, amiCfg)
101+
if err != nil {
102+
return err
103+
}
104+
105+
return nil
106+
}

pkg/bib/build.go

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
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+
13+
"github.com/containers/podman/v5/pkg/bindings/containers"
14+
"github.com/containers/podman/v5/pkg/bindings/images"
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+
)
19+
20+
type BuildOption struct {
21+
Config string
22+
Output string
23+
Filesystem string
24+
Format string
25+
Arch string
26+
}
27+
28+
type AmiConfig struct {
29+
Name string
30+
Bucket string
31+
Region string
32+
}
33+
34+
func Build(ctx context.Context, quiet bool, bibContainerImage, imageNameOrId string, user user.User, buildOption BuildOption, amiCnfg AmiConfig) 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 Builder if necessary
71+
_, err = pullImage(ctx, bibContainerImage)
72+
if err != nil {
73+
return fmt.Errorf("pulling bootc image builder image: %w", err)
74+
}
75+
76+
imageFullName, err := pullImage(ctx, imageNameOrId)
77+
if err != nil {
78+
return fmt.Errorf("pulling image: %w", err)
79+
}
80+
81+
bibContainer, err := createBibContainer(ctx, bibContainerImage, imageFullName, buildOption, amiCnfg)
82+
if err != nil {
83+
return fmt.Errorf("failed to create image builder container: %w", err)
84+
}
85+
86+
err = containers.Start(ctx, bibContainer.ID, &containers.StartOptions{})
87+
if err != nil {
88+
return fmt.Errorf("failed to start image builder container: %w", err)
89+
}
90+
91+
// Ensure we've cancelled the container attachment when exiting this function, as
92+
// it takes over stdout/stderr handling
93+
attachCancelCtx, cancelAttach := context.WithCancel(ctx)
94+
defer cancelAttach()
95+
96+
if !quiet {
97+
attachOpts := new(containers.AttachOptions).WithStream(true)
98+
if err := containers.Attach(attachCancelCtx, bibContainer.ID, os.Stdin, os.Stdout, os.Stderr, nil, attachOpts); err != nil {
99+
return fmt.Errorf("attaching image builder container: %w", err)
100+
}
101+
}
102+
103+
exitCode, err := containers.Wait(ctx, bibContainer.ID, nil)
104+
if err != nil {
105+
return fmt.Errorf("failed to wait for image builder container: %w", err)
106+
}
107+
108+
if exitCode != 0 {
109+
return fmt.Errorf("failed to run image builder")
110+
}
111+
112+
return nil
113+
}
114+
115+
// pullImage fetches the container image if not present
116+
func pullImage(ctx context.Context, imageNameOrId string) (imageFullName string, err error) {
117+
pullPolicy := "missing"
118+
ids, err := images.Pull(ctx, imageNameOrId, &images.PullOptions{Policy: &pullPolicy})
119+
if err != nil {
120+
return "", fmt.Errorf("failed to pull image: %w", err)
121+
}
122+
123+
if len(ids) == 0 {
124+
return "", fmt.Errorf("no ids returned from image pull")
125+
}
126+
127+
if len(ids) > 1 {
128+
return "", fmt.Errorf("multiple ids returned from image pull")
129+
}
130+
131+
imageInfo, err := images.GetImage(ctx, imageNameOrId, &images.GetOptions{})
132+
if err != nil {
133+
return "", fmt.Errorf("failed to get image: %w", err)
134+
}
135+
136+
return imageInfo.RepoTags[0], nil
137+
}
138+
139+
func createBibContainer(ctx context.Context, bibContainerImage, imageFullName string, buildOption BuildOption, amiCnfg AmiConfig) (types.ContainerCreateResponse, error) {
140+
privileged := true
141+
autoRemove := true
142+
labelNested := true
143+
terminal := true // Allocate pty so we can show progress bars, spinners etc.
144+
145+
bibArgs := bibArguments(imageFullName, buildOption, amiCnfg)
146+
147+
s := &specgen.SpecGenerator{
148+
ContainerBasicConfig: specgen.ContainerBasicConfig{
149+
Remove: &autoRemove,
150+
Annotations: map[string]string{"io.podman.annotations.label": "type:unconfined_t"},
151+
Terminal: &terminal,
152+
Command: bibArgs,
153+
SdNotifyMode: "container", // required otherwise crun will fail to open the sd-bus
154+
},
155+
ContainerStorageConfig: specgen.ContainerStorageConfig{
156+
Image: bibContainerImage,
157+
Mounts: []specs.Mount{
158+
{
159+
Source: buildOption.Config,
160+
Destination: "/config.toml",
161+
Type: "bind",
162+
},
163+
{
164+
Source: buildOption.Output,
165+
Destination: "/output",
166+
Type: "bind",
167+
Options: []string{"nosuid", "nodev"},
168+
},
169+
{
170+
Source: "/var/lib/containers/storage",
171+
Destination: "/var/lib/containers/storage",
172+
Type: "bind",
173+
},
174+
},
175+
},
176+
ContainerSecurityConfig: specgen.ContainerSecurityConfig{
177+
Privileged: &privileged,
178+
LabelNested: &labelNested,
179+
SelinuxOpts: []string{"type:unconfined_t"},
180+
},
181+
ContainerNetworkConfig: specgen.ContainerNetworkConfig{
182+
NetNS: specgen.Namespace{
183+
NSMode: specgen.Bridge,
184+
},
185+
},
186+
}
187+
188+
createResponse, err := containers.CreateWithSpec(ctx, s, &containers.CreateOptions{})
189+
if err != nil {
190+
return createResponse, fmt.Errorf("failed to create image builder container: %w", err)
191+
}
192+
return createResponse, nil
193+
}
194+
195+
func bibArguments(imageNameOrId string, buildOption BuildOption, amiCfg AmiConfig) []string {
196+
args := []string{
197+
"--local", // we pull the image if necessary, so don't pull it from a registry
198+
}
199+
200+
if buildOption.Filesystem != "" {
201+
args = append(args, "--rootfs", buildOption.Filesystem)
202+
}
203+
204+
if buildOption.Arch != "" {
205+
args = append(args, "--target-arch", buildOption.Arch)
206+
}
207+
208+
if buildOption.Format != "" {
209+
args = append(args, "--type", buildOption.Format)
210+
}
211+
212+
// AMI specific options
213+
if amiCfg.Name != "" {
214+
args = append(args, "--aws-ami-name", amiCfg.Name)
215+
}
216+
217+
if amiCfg.Bucket != "" {
218+
args = append(args, "--aws-bucket", amiCfg.Bucket)
219+
}
220+
221+
if amiCfg.Region != "" {
222+
args = append(args, "--aws-region", amiCfg.Region)
223+
}
224+
225+
args = append(args, imageNameOrId)
226+
return args
227+
}

0 commit comments

Comments
 (0)