|
| 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