Skip to content

Commit f398e86

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 f398e86

File tree

2 files changed

+327
-0
lines changed

2 files changed

+327
-0
lines changed

cmd/convert.go

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

pkg/bib/build.go

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

0 commit comments

Comments
 (0)