This commit completes the CLI integration for the debos backend: ✅ IMPLEMENTED: - New debos build path accessible via --use-debos flag - Full CLI compatibility with existing bootc-image-builder interface - Automatic fallback to osbuild when --use-debos not specified - Comprehensive debos-specific command line options 🔧 NEW FLAGS: - --use-debos: Enable debos backend instead of osbuild - --debos-suite: Override Debian suite detection - --debos-packages: Additional packages to install - --debos-ostree: Enable/disable OSTree integration - --debos-repository: OSTree repository path - --debos-branch: OSTree branch name - --debos-dry-run: Perform dry run without building 🧪 TESTING: - All tests passing with comprehensive test script - Dry-run functionality working correctly - Suite and architecture detection functional - Help output properly displays all debos options 🎯 USAGE EXAMPLES: - Basic: ./bootc-image-builder --use-debos debian:trixie - Custom suite: --use-debos --debos-suite bookworm debian:bookworm - Dry run: --use-debos --debos-dry-run debian:trixie The debos backend is now fully integrated and ready for end-to-end testing!
783 lines
25 KiB
Go
783 lines
25 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime/debug"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/pflag"
|
|
"golang.org/x/exp/slices"
|
|
|
|
"github.com/osbuild/images/pkg/arch"
|
|
"github.com/osbuild/images/pkg/bib/blueprintload"
|
|
"github.com/osbuild/images/pkg/cloud"
|
|
"github.com/osbuild/images/pkg/cloud/awscloud"
|
|
"github.com/osbuild/images/pkg/container"
|
|
"github.com/osbuild/images/pkg/dnfjson"
|
|
"github.com/osbuild/images/pkg/experimentalflags"
|
|
"github.com/osbuild/images/pkg/manifest"
|
|
"github.com/osbuild/images/pkg/rpmmd"
|
|
|
|
"github.com/particle-os/debian-bootc-image-builder/bib/internal/imagetypes"
|
|
"github.com/particle-os/debian-bootc-image-builder/bib/internal/solver"
|
|
"github.com/particle-os/debian-bootc-image-builder/bib/internal/debian-patch"
|
|
podman_container "github.com/osbuild/images/pkg/bib/container"
|
|
"github.com/osbuild/images/pkg/bib/osinfo"
|
|
|
|
"github.com/osbuild/image-builder-cli/pkg/progress"
|
|
"github.com/osbuild/image-builder-cli/pkg/setup"
|
|
"github.com/osbuild/image-builder-cli/pkg/util"
|
|
)
|
|
|
|
const (
|
|
// As a baseline heuristic we double the size of
|
|
// the input container to support in-place updates.
|
|
// This is planned to be more configurable in the
|
|
// future.
|
|
containerSizeToDiskSizeMultiplier = 2
|
|
)
|
|
|
|
// all possible locations for the bib's distro definitions
|
|
// ./data/defs and ./bib/data/defs are for development
|
|
// /usr/share/bootc-image-builder/defs is for the production, containerized version
|
|
var distroDefPaths = []string{
|
|
"./data/defs",
|
|
"./bib/data/defs",
|
|
"/usr/share/bootc-image-builder/defs",
|
|
}
|
|
|
|
var (
|
|
osGetuid = os.Getuid
|
|
osGetgid = os.Getgid
|
|
|
|
osStdout = os.Stdout
|
|
osStderr = os.Stderr
|
|
)
|
|
|
|
// canChownInPath checks if the ownership of files can be set in a given path.
|
|
func canChownInPath(path string) (bool, error) {
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !info.IsDir() {
|
|
return false, fmt.Errorf("%s is not a directory", path)
|
|
}
|
|
|
|
checkFile, err := os.CreateTemp(path, ".writecheck")
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer func() {
|
|
if err := os.Remove(checkFile.Name()); err != nil {
|
|
// print the error message for info but don't error out
|
|
fmt.Fprintf(os.Stderr, "error deleting %s: %s\n", checkFile.Name(), err.Error())
|
|
}
|
|
}()
|
|
return checkFile.Chown(osGetuid(), osGetgid()) == nil, nil
|
|
}
|
|
|
|
func inContainerOrUnknown() bool {
|
|
// no systemd-detect-virt, err on the side of container
|
|
if _, err := exec.LookPath("systemd-detect-virt"); err != nil {
|
|
return true
|
|
}
|
|
// exit code "0" means the container is detected
|
|
err := exec.Command("systemd-detect-virt", "-c", "-q").Run()
|
|
return err == nil
|
|
}
|
|
|
|
// getContainerSize returns the size of an already pulled container image in bytes
|
|
func getContainerSize(imgref string) (uint64, error) {
|
|
output, err := exec.Command("podman", "image", "inspect", imgref, "--format", "{{.Size}}").Output()
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed inspect image: %w", util.OutputErr(err))
|
|
}
|
|
size, err := strconv.ParseUint(strings.TrimSpace(string(output)), 10, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("cannot parse image size: %w", err)
|
|
}
|
|
|
|
logrus.Debugf("container size: %v", size)
|
|
return size, nil
|
|
}
|
|
|
|
func makeManifest(c *ManifestConfig, solver solver.Solver, cacheRoot string) (manifest.OSBuildManifest, map[string][]rpmmd.RepoConfig, error) {
|
|
mani, err := Manifest(c)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("cannot get manifest: %w", err)
|
|
}
|
|
|
|
// depsolve packages using our Debian solver
|
|
depsolvedSets := make(map[string]dnfjson.DepsolveResult)
|
|
depsolvedRepos := make(map[string][]rpmmd.RepoConfig)
|
|
for name, pkgSets := range mani.GetPackageSetChains() {
|
|
// Extract packages from all PackageSets
|
|
var allPackages []string
|
|
for _, pkgSet := range pkgSets {
|
|
allPackages = append(allPackages, pkgSet.Include...)
|
|
}
|
|
res, err := solver.Depsolve(allPackages, 0)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("cannot depsolve: %w", err)
|
|
}
|
|
// Convert the generic result to DNF types for compatibility
|
|
if depsolveResult, ok := res.(*debianpatch.DebianDepsolveResult); ok {
|
|
depsolvedSets[name] = debianpatch.ConvertDebianDepsolveResultToDNF(*depsolveResult)
|
|
depsolvedRepos[name] = debianpatch.ConvertDebianDepsolveResultToDNF(*depsolveResult).Repos
|
|
} else {
|
|
return nil, nil, fmt.Errorf("unexpected depsolve result type: %T", res)
|
|
}
|
|
}
|
|
|
|
// Resolve container - the normal case is that host and target
|
|
// architecture are the same. However it is possible to build
|
|
// cross-arch images by using qemu-user. This will run everything
|
|
// (including the build-root) with the target arch then, it
|
|
// is fast enough (given that it's mostly I/O and all I/O is
|
|
// run naively via syscall translation)
|
|
|
|
// XXX: should NewResolver() take "arch.Arch"?
|
|
resolver := container.NewResolver(c.Architecture.String())
|
|
|
|
containerSpecs := make(map[string][]container.Spec)
|
|
for plName, sourceSpecs := range mani.GetContainerSourceSpecs() {
|
|
for _, c := range sourceSpecs {
|
|
resolver.Add(c)
|
|
}
|
|
specs, err := resolver.Finish()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("cannot resolve containers: %w", err)
|
|
}
|
|
for _, spec := range specs {
|
|
if spec.Arch != c.Architecture {
|
|
return nil, nil, fmt.Errorf("image found is for unexpected architecture %q (expected %q), if that is intentional, please make sure --target-arch matches", spec.Arch, c.Architecture)
|
|
}
|
|
}
|
|
containerSpecs[plName] = specs
|
|
}
|
|
|
|
var opts manifest.SerializeOptions
|
|
// Note: RpmDownloader is Red Hat specific, not used in Debian
|
|
// We'll use the default downloader for Debian packages
|
|
mf, err := mani.Serialize(depsolvedSets, containerSpecs, nil, &opts)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("[ERROR] manifest serialization failed: %s", err.Error())
|
|
}
|
|
return mf, depsolvedRepos, nil
|
|
}
|
|
|
|
func saveManifest(ms manifest.OSBuildManifest, fpath string) (err error) {
|
|
b, err := json.MarshalIndent(ms, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal data for %q: %s", fpath, err.Error())
|
|
}
|
|
b = append(b, '\n') // add new line at end of file
|
|
fp, err := os.Create(fpath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create output file %q: %s", fpath, err.Error())
|
|
}
|
|
defer func() { err = errors.Join(err, fp.Close()) }()
|
|
if _, err := fp.Write(b); err != nil {
|
|
return fmt.Errorf("failed to write output file %q: %s", fpath, err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// manifestFromCobra generate an osbuild manifest from a cobra commandline.
|
|
//
|
|
// It takes an unstarted progres bar and will start it at the right
|
|
// point (it cannot be started yet to avoid the "podman pull" progress
|
|
// and our progress fighting). The caller is responsible for stopping
|
|
// the progress bar (this function cannot know what else needs to happen
|
|
// after manifest generation).
|
|
//
|
|
// TODO: provide a podman progress reader to integrate the podman progress
|
|
// into our progress.
|
|
func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.ProgressBar) ([]byte, *mTLSConfig, error) {
|
|
cntArch := arch.Current()
|
|
|
|
imgref := args[0]
|
|
userConfigFile, _ := cmd.Flags().GetString("config")
|
|
imgTypes, _ := cmd.Flags().GetStringArray("type")
|
|
rpmCacheRoot, _ := cmd.Flags().GetString("rpmmd")
|
|
targetArch, _ := cmd.Flags().GetString("target-arch")
|
|
rootFs, _ := cmd.Flags().GetString("rootfs")
|
|
buildImgref, _ := cmd.Flags().GetString("build-container")
|
|
useLibrepo, _ := cmd.Flags().GetBool("use-librepo")
|
|
|
|
// If --local was given, warn in the case of --local or --local=true (true is the default), error in the case of --local=false
|
|
if cmd.Flags().Changed("local") {
|
|
localStorage, _ := cmd.Flags().GetBool("local")
|
|
if localStorage {
|
|
fmt.Fprintf(os.Stderr, "WARNING: --local is now the default behavior, you can remove it from the command line\n")
|
|
} else {
|
|
return nil, nil, fmt.Errorf(`--local=false is no longer supported, remove it and make sure to pull the container before running bib:
|
|
sudo podman pull %s`, imgref)
|
|
}
|
|
}
|
|
|
|
if targetArch != "" {
|
|
target, err := arch.FromString(targetArch)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if target != arch.Current() {
|
|
// TODO: detect if binfmt_misc for target arch is
|
|
// available, e.g. by mounting the binfmt_misc fs into
|
|
// the container and inspects the files or by
|
|
// including tiny statically linked target-arch
|
|
// binaries inside our bib container
|
|
fmt.Fprintf(os.Stderr, "WARNING: target-arch is experimental and needs an installed 'qemu-user' package\n")
|
|
if slices.Contains(imgTypes, "iso") {
|
|
return nil, nil, fmt.Errorf("cannot build iso for different target arches yet")
|
|
}
|
|
cntArch = target
|
|
}
|
|
}
|
|
// TODO: add "target-variant", see https://github.com/osbuild/bootc-image-builder/pull/139/files#r1467591868
|
|
|
|
if err := setup.ValidateHasContainerStorageMounted(); err != nil {
|
|
return nil, nil, fmt.Errorf("could not access container storage, did you forget -v /var/lib/containers/storage:/var/lib/containers/storage? (%w)", err)
|
|
}
|
|
|
|
imageTypes, err := imagetypes.New(imgTypes...)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("cannot detect build types %v: %w", imgTypes, err)
|
|
}
|
|
|
|
config, err := blueprintload.LoadWithFallback(userConfigFile)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("cannot read config: %w", err)
|
|
}
|
|
|
|
pbar.SetPulseMsgf("Manifest generation step")
|
|
pbar.Start()
|
|
|
|
if err := setup.ValidateHasContainerTags(imgref); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
cntSize, err := getContainerSize(imgref)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("cannot get container size: %w", err)
|
|
}
|
|
container, err := podman_container.New(imgref)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer func() {
|
|
if err := container.Stop(); err != nil {
|
|
logrus.Warnf("error stopping container: %v", err)
|
|
}
|
|
}()
|
|
|
|
var rootfsType string
|
|
if rootFs != "" {
|
|
rootfsType = rootFs
|
|
} else {
|
|
rootfsType, err = container.DefaultRootfsType()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("cannot get rootfs type for container: %w", err)
|
|
}
|
|
if rootfsType == "" {
|
|
return nil, nil, fmt.Errorf(`no default root filesystem type specified in container, please use "--rootfs" to set manually`)
|
|
}
|
|
}
|
|
|
|
// Gather some data from the containers distro
|
|
sourceinfo, err := osinfo.Load(container.Root())
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
buildContainer := container
|
|
buildSourceinfo := sourceinfo
|
|
startedBuildContainer := false
|
|
defer func() {
|
|
if startedBuildContainer {
|
|
if err := buildContainer.Stop(); err != nil {
|
|
logrus.Warnf("error stopping container: %v", err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
if buildImgref != "" {
|
|
buildContainer, err = podman_container.New(buildImgref)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
startedBuildContainer = true
|
|
|
|
// Gather some data from the containers distro
|
|
buildSourceinfo, err = osinfo.Load(buildContainer.Root())
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
} else {
|
|
buildImgref = imgref
|
|
}
|
|
|
|
// Create a Debian-focused solver
|
|
debianSolver, err := solver.NewSolver(sourceinfo, rpmCacheRoot, cntArch)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to create Debian solver: %w", err)
|
|
}
|
|
|
|
manifestConfig := &ManifestConfig{
|
|
Architecture: cntArch,
|
|
Config: config,
|
|
ImageTypes: imageTypes,
|
|
Imgref: imgref,
|
|
BuildImgref: buildImgref,
|
|
RootfsMinsize: cntSize * containerSizeToDiskSizeMultiplier,
|
|
DistroDefPaths: distroDefPaths,
|
|
SourceInfo: sourceinfo,
|
|
BuildSourceInfo: buildSourceinfo,
|
|
RootFSType: rootfsType,
|
|
UseLibrepo: useLibrepo,
|
|
}
|
|
|
|
manifest, repos, err := makeManifest(manifestConfig, debianSolver, rpmCacheRoot)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
mTLS, err := extractTLSKeys(SimpleFileReader{}, repos)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return manifest, mTLS, nil
|
|
}
|
|
|
|
func cmdManifest(cmd *cobra.Command, args []string) error {
|
|
pbar, err := progress.New("")
|
|
if err != nil {
|
|
// this should never happen
|
|
return fmt.Errorf("cannot create progress bar: %w", err)
|
|
}
|
|
defer pbar.Stop()
|
|
|
|
mf, _, err := manifestFromCobra(cmd, args, pbar)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot generate manifest: %w", err)
|
|
}
|
|
fmt.Println(string(mf))
|
|
return nil
|
|
}
|
|
|
|
func handleAWSFlags(cmd *cobra.Command) (cloud.Uploader, error) {
|
|
imgTypes, _ := cmd.Flags().GetStringArray("type")
|
|
region, _ := cmd.Flags().GetString("aws-region")
|
|
if region == "" {
|
|
return nil, nil
|
|
}
|
|
bucketName, _ := cmd.Flags().GetString("aws-bucket")
|
|
imageName, _ := cmd.Flags().GetString("aws-ami-name")
|
|
targetArch, _ := cmd.Flags().GetString("target-arch")
|
|
|
|
if !slices.Contains(imgTypes, "ami") {
|
|
return nil, fmt.Errorf("aws flags set for non-ami image type (type is set to %s)", strings.Join(imgTypes, ","))
|
|
}
|
|
|
|
// check as many permission prerequisites as possible before starting
|
|
uploaderOpts := &awscloud.UploaderOptions{
|
|
TargetArch: targetArch,
|
|
}
|
|
uploader, err := awscloud.NewUploader(region, bucketName, imageName, uploaderOpts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
status := io.Discard
|
|
if logrus.GetLevel() >= logrus.InfoLevel {
|
|
status = os.Stderr
|
|
}
|
|
if err := uploader.Check(status); err != nil {
|
|
return nil, err
|
|
}
|
|
return uploader, nil
|
|
}
|
|
|
|
func cmdBuild(cmd *cobra.Command, args []string) error {
|
|
chown, _ := cmd.Flags().GetString("chown")
|
|
imgTypes, _ := cmd.Flags().GetStringArray("type")
|
|
osbuildStore, _ := cmd.Flags().GetString("store")
|
|
outputDir, _ := cmd.Flags().GetString("output")
|
|
targetArch, _ := cmd.Flags().GetString("target-arch")
|
|
progressType, _ := cmd.Flags().GetString("progress")
|
|
useDebos, _ := cmd.Flags().GetBool("use-debos")
|
|
|
|
// If --use-debos is specified, use the debos backend
|
|
if useDebos {
|
|
return cmdBuildDebos(cmd, args)
|
|
}
|
|
|
|
logrus.Debug("Validating environment")
|
|
if err := setup.Validate(targetArch); err != nil {
|
|
return fmt.Errorf("cannot validate the setup: %w", err)
|
|
}
|
|
logrus.Debug("Ensuring environment setup")
|
|
switch inContainerOrUnknown() {
|
|
case false:
|
|
fmt.Fprintf(os.Stderr, "WARNING: running outside a container, this is an unsupported configuration\n")
|
|
case true:
|
|
if err := setup.EnsureEnvironment(osbuildStore); err != nil {
|
|
return fmt.Errorf("cannot ensure the environment: %w", err)
|
|
}
|
|
}
|
|
|
|
if err := os.MkdirAll(outputDir, 0o777); err != nil {
|
|
return fmt.Errorf("cannot setup build dir: %w", err)
|
|
}
|
|
|
|
uploader, err := handleAWSFlags(cmd)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot handle AWS setup: %w", err)
|
|
}
|
|
|
|
canChown, err := canChownInPath(outputDir)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot ensure ownership: %w", err)
|
|
}
|
|
if !canChown && chown != "" {
|
|
return fmt.Errorf("chowning is not allowed in output directory")
|
|
}
|
|
|
|
pbar, err := progress.New(progressType)
|
|
if err != nil {
|
|
return fmt.Errorf("cannto create progress bar: %w", err)
|
|
}
|
|
defer pbar.Stop()
|
|
|
|
manifest_fname := fmt.Sprintf("manifest-%s.json", strings.Join(imgTypes, "-"))
|
|
pbar.SetMessagef("Generating manifest %s", manifest_fname)
|
|
mf, mTLS, err := manifestFromCobra(cmd, args, pbar)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot build manifest: %w", err)
|
|
}
|
|
pbar.SetMessagef("Done generating manifest")
|
|
|
|
// collect pipeline exports for each image type
|
|
imageTypes, err := imagetypes.New(imgTypes...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
exports := imageTypes.Exports()
|
|
manifestPath := filepath.Join(outputDir, manifest_fname)
|
|
if err := saveManifest(mf, manifestPath); err != nil {
|
|
return fmt.Errorf("cannot save manifest: %w", err)
|
|
}
|
|
|
|
pbar.SetPulseMsgf("Disk image building step")
|
|
pbar.SetMessagef("Building %s", manifest_fname)
|
|
|
|
var osbuildEnv []string
|
|
if !canChown {
|
|
// set export options for osbuild
|
|
osbuildEnv = []string{"OSBUILD_EXPORT_FORCE_NO_PRESERVE_OWNER=1"}
|
|
}
|
|
|
|
if mTLS != nil {
|
|
envVars, cleanup, err := prepareOsbuildMTLSConfig(mTLS)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to prepare osbuild TLS keys: %w", err)
|
|
}
|
|
|
|
defer cleanup()
|
|
|
|
osbuildEnv = append(osbuildEnv, envVars...)
|
|
}
|
|
|
|
if experimentalflags.Bool("debug-qemu-user") {
|
|
osbuildEnv = append(osbuildEnv, "OBSBUILD_EXPERIMENAL=debug-qemu-user")
|
|
}
|
|
osbuildOpts := progress.OSBuildOptions{
|
|
StoreDir: osbuildStore,
|
|
OutputDir: outputDir,
|
|
ExtraEnv: osbuildEnv,
|
|
}
|
|
if err = progress.RunOSBuild(pbar, mf, exports, &osbuildOpts); err != nil {
|
|
return fmt.Errorf("cannot run osbuild: %w", err)
|
|
}
|
|
|
|
pbar.SetMessagef("Build complete!")
|
|
if uploader != nil {
|
|
// XXX: pass our own progress.ProgressBar here
|
|
// *for now* just stop our own progress and let the uploadAMI
|
|
// progress take over - but we really need to fix this in a
|
|
// followup
|
|
pbar.Stop()
|
|
for idx, imgType := range imgTypes {
|
|
switch imgType {
|
|
case "ami":
|
|
diskpath := filepath.Join(outputDir, exports[idx], "disk.raw")
|
|
if err := upload(uploader, diskpath, cmd.Flags()); err != nil {
|
|
return fmt.Errorf("cannot upload AMI: %w", err)
|
|
}
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
} else {
|
|
pbar.SetMessagef("Results saved in %s", outputDir)
|
|
}
|
|
|
|
if err := chownR(outputDir, chown); err != nil {
|
|
return fmt.Errorf("cannot setup owner for %q: %w", outputDir, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func chownR(path string, chown string) error {
|
|
if chown == "" {
|
|
return nil
|
|
}
|
|
errFmt := "cannot parse chown: %v"
|
|
|
|
var gid int
|
|
uidS, gidS, _ := strings.Cut(chown, ":")
|
|
uid, err := strconv.Atoi(uidS)
|
|
if err != nil {
|
|
return fmt.Errorf(errFmt, err)
|
|
}
|
|
if gidS != "" {
|
|
gid, err = strconv.Atoi(gidS)
|
|
if err != nil {
|
|
return fmt.Errorf(errFmt, err)
|
|
}
|
|
} else {
|
|
gid = osGetgid()
|
|
}
|
|
|
|
return filepath.Walk(path, func(name string, info os.FileInfo, err error) error {
|
|
if err == nil {
|
|
err = os.Chown(name, uid, gid)
|
|
}
|
|
return err
|
|
})
|
|
}
|
|
|
|
var rootLogLevel string
|
|
|
|
func rootPreRunE(cmd *cobra.Command, _ []string) error {
|
|
verbose, _ := cmd.Flags().GetBool("verbose")
|
|
progress, _ := cmd.Flags().GetString("progress")
|
|
switch {
|
|
case rootLogLevel != "":
|
|
level, err := logrus.ParseLevel(rootLogLevel)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
logrus.SetLevel(level)
|
|
case verbose:
|
|
logrus.SetLevel(logrus.InfoLevel)
|
|
default:
|
|
logrus.SetLevel(logrus.ErrorLevel)
|
|
}
|
|
if verbose && progress == "auto" {
|
|
if err := cmd.Flags().Set("progress", "verbose"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func versionFromBuildInfo() (string, error) {
|
|
info, ok := debug.ReadBuildInfo()
|
|
if !ok {
|
|
return "", fmt.Errorf("cannot read build info")
|
|
}
|
|
var buildTainted bool
|
|
gitRev := "unknown"
|
|
buildTime := "unknown"
|
|
for _, bs := range info.Settings {
|
|
switch bs.Key {
|
|
case "vcs.revision":
|
|
gitRev = bs.Value[:7]
|
|
case "vcs.time":
|
|
buildTime = bs.Value
|
|
case "vcs.modified":
|
|
bT, err := strconv.ParseBool(bs.Value)
|
|
if err != nil {
|
|
logrus.Errorf("Error parsing 'vcs.modified': %v", err)
|
|
bT = true
|
|
}
|
|
buildTainted = bT
|
|
}
|
|
}
|
|
|
|
return fmt.Sprintf(`build_revision: %s
|
|
build_time: %s
|
|
build_tainted: %v
|
|
`, gitRev, buildTime, buildTainted), nil
|
|
}
|
|
|
|
func buildCobraCmdline() (*cobra.Command, error) {
|
|
version, err := versionFromBuildInfo()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rootCmd := &cobra.Command{
|
|
Use: "bootc-image-builder",
|
|
Long: "Create a bootable image from an ostree native container\n\n" +
|
|
"Supports both osbuild (default) and debos backends.\n" +
|
|
"Use --use-debos to enable the debos backend for Debian-based images.",
|
|
PersistentPreRunE: rootPreRunE,
|
|
SilenceErrors: true,
|
|
Version: version,
|
|
}
|
|
rootCmd.SetVersionTemplate(version)
|
|
|
|
rootCmd.PersistentFlags().StringVar(&rootLogLevel, "log-level", "", "logging level (debug, info, error); default error")
|
|
rootCmd.PersistentFlags().BoolP("verbose", "v", false, `Switch to verbose mode`)
|
|
|
|
buildCmd := &cobra.Command{
|
|
Use: "build IMAGE_NAME",
|
|
Short: "Create a bootable image from a container (default command)",
|
|
Long: "Create a bootable image from a container image.\n\n" +
|
|
"Supports both osbuild (default) and debos backends.\n" +
|
|
"Use --use-debos to enable the debos backend for Debian-based images.\n\n" +
|
|
"(default action if no command is given)\n" +
|
|
"IMAGE_NAME: container image to build into a bootable image",
|
|
Args: cobra.ExactArgs(1),
|
|
DisableFlagsInUseLine: true,
|
|
RunE: cmdBuild,
|
|
SilenceUsage: true,
|
|
Example: rootCmd.Use + " build quay.io/debian-bootc/debian-bootc:bookworm\n" +
|
|
rootCmd.Use + " quay.io/debian-bootc/debian-bootc:bookworm\n" +
|
|
rootCmd.Use + " build --use-debos debian:trixie\n" +
|
|
rootCmd.Use + " --use-debos --debos-suite bookworm debian:bookworm\n",
|
|
Version: rootCmd.Version,
|
|
}
|
|
buildCmd.SetVersionTemplate(version)
|
|
|
|
rootCmd.AddCommand(buildCmd)
|
|
manifestCmd := &cobra.Command{
|
|
Use: "manifest",
|
|
Short: "Only create the manifest but don't build the image.",
|
|
Args: cobra.ExactArgs(1),
|
|
DisableFlagsInUseLine: true,
|
|
RunE: cmdManifest,
|
|
SilenceUsage: true,
|
|
Version: rootCmd.Version,
|
|
}
|
|
manifestCmd.SetVersionTemplate(version)
|
|
|
|
versionCmd := &cobra.Command{
|
|
Use: "version",
|
|
Short: "Show the version and quit",
|
|
SilenceUsage: true,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
root := cmd.Root()
|
|
root.SetArgs([]string{"--version"})
|
|
return root.Execute()
|
|
},
|
|
}
|
|
|
|
rootCmd.AddCommand(versionCmd)
|
|
|
|
rootCmd.AddCommand(manifestCmd)
|
|
manifestCmd.Flags().Bool("tls-verify", false, "DEPRECATED: require HTTPS and verify certificates when contacting registries")
|
|
if err := manifestCmd.Flags().MarkHidden("tls-verify"); err != nil {
|
|
return nil, fmt.Errorf("cannot hide 'tls-verify' :%w", err)
|
|
}
|
|
manifestCmd.Flags().String("rpmmd", "/rpmmd", "rpm metadata cache directory")
|
|
manifestCmd.Flags().String("target-arch", "", "build for the given target architecture (experimental)")
|
|
manifestCmd.Flags().String("build-container", "", "Use a custom container for the image build")
|
|
manifestCmd.Flags().StringArray("type", []string{"qcow2"}, fmt.Sprintf("image types to build [%s]", imagetypes.Available()))
|
|
manifestCmd.Flags().Bool("local", true, "DEPRECATED: --local is now the default behavior, make sure to pull the container image before running bootc-image-builder")
|
|
if err := manifestCmd.Flags().MarkHidden("local"); err != nil {
|
|
return nil, fmt.Errorf("cannot hide 'local' :%w", err)
|
|
}
|
|
manifestCmd.Flags().String("rootfs", "", "Root filesystem type. If not given, the default configured in the source container image is used.")
|
|
manifestCmd.Flags().Bool("use-librepo", true, "switch to librepo for pkg download, needs new enough osbuild")
|
|
// --config is only useful for developers who run bib outside
|
|
// of a container to generate a manifest. so hide it by
|
|
// default from users.
|
|
manifestCmd.Flags().String("config", "", "build config file; /config.json will be used if present")
|
|
if err := manifestCmd.Flags().MarkHidden("config"); err != nil {
|
|
return nil, fmt.Errorf("cannot hide 'config' :%w", err)
|
|
}
|
|
|
|
buildCmd.Flags().AddFlagSet(manifestCmd.Flags())
|
|
buildCmd.Flags().String("aws-ami-name", "", "name for the AMI in AWS (only for type=ami)")
|
|
buildCmd.Flags().String("aws-bucket", "", "target S3 bucket name for intermediate storage when creating AMI (only for type=ami)")
|
|
buildCmd.Flags().String("aws-region", "", "target region for AWS uploads (only for type=ami)")
|
|
buildCmd.Flags().String("chown", "", "chown the ouput directory to match the specified UID:GID")
|
|
buildCmd.Flags().String("output", ".", "artifact output directory")
|
|
buildCmd.Flags().String("store", "/store", "osbuild store for intermediate pipeline trees")
|
|
//TODO: add json progress for higher level tools like "podman bootc"
|
|
buildCmd.Flags().String("progress", "auto", "type of progress bar to use (e.g. verbose,term)")
|
|
|
|
// Add debos-specific flags
|
|
buildCmd.Flags().Bool("use-debos", false, "Use debos backend instead of osbuild")
|
|
buildCmd.Flags().String("debos-suite", "", "Override Debian suite detection (e.g., bookworm, trixie)")
|
|
buildCmd.Flags().StringArray("debos-packages", []string{}, "Additional packages to install during debos build")
|
|
buildCmd.Flags().Bool("debos-ostree", true, "Enable OSTree integration for bootc compatibility")
|
|
buildCmd.Flags().String("debos-repository", "/ostree/repo", "OSTree repository path")
|
|
buildCmd.Flags().String("debos-branch", "", "OSTree branch name (auto-generated if not specified)")
|
|
buildCmd.Flags().Bool("debos-dry-run", false, "Perform a dry run without building (debos --dry-run)")
|
|
|
|
// flag rules
|
|
for _, dname := range []string{"output", "store", "rpmmd"} {
|
|
if err := buildCmd.MarkFlagDirname(dname); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := buildCmd.MarkFlagFilename("config"); err != nil {
|
|
return nil, err
|
|
}
|
|
buildCmd.MarkFlagsRequiredTogether("aws-region", "aws-bucket", "aws-ami-name")
|
|
|
|
// If no subcommand is given, assume the user wants to use the build subcommand
|
|
// See https://github.com/spf13/cobra/issues/823#issuecomment-870027246
|
|
// which cannot be used verbatim because the arguments for "build" like
|
|
// "quay.io" will create an "err != nil". Ideally we could check err
|
|
// for something like cobra.UnknownCommandError but cobra just gives
|
|
// us an error string
|
|
cmd, _, err := rootCmd.Find(os.Args[1:])
|
|
injectBuildArg := func() {
|
|
args := append([]string{buildCmd.Name()}, os.Args[1:]...)
|
|
rootCmd.SetArgs(args)
|
|
}
|
|
// command not known, i.e. happens for "bib quay.io/debian/..."
|
|
if err != nil && !slices.Contains([]string{"help", "completion"}, os.Args[1]) {
|
|
injectBuildArg()
|
|
}
|
|
// command appears valid, e.g. "bib --local quay.io/debian" but this
|
|
// is the parser just assuming "quay.io" is an argument for "--local" :(
|
|
if err == nil && cmd.Use == rootCmd.Use && cmd.Flags().Parse(os.Args[1:]) != pflag.ErrHelp {
|
|
injectBuildArg()
|
|
}
|
|
|
|
return rootCmd, nil
|
|
}
|
|
|
|
func run() error {
|
|
rootCmd, err := buildCobraCmdline()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return rootCmd.Execute()
|
|
}
|
|
|
|
func main() {
|
|
if err := run(); err != nil {
|
|
log.Fatalf("error: %s", err)
|
|
}
|
|
}
|