diff --git a/bib/cmd/bootc-image-builder/image.go b/bib/cmd/bootc-image-builder/image.go index 27814778..500a5e40 100644 --- a/bib/cmd/bootc-image-builder/image.go +++ b/bib/cmd/bootc-image-builder/image.go @@ -39,7 +39,8 @@ const DEFAULT_SIZE = uint64(10 * GibiByte) type ManifestConfig struct { // OCI image path (without the transport, that is always docker://) - Imgref string + Imgref string + BuildImgref string ImageTypes imagetypes.ImageTypes @@ -57,7 +58,8 @@ type ManifestConfig struct { DistroDefPaths []string // Extracted information about the source container image - SourceInfo *source.Info + SourceInfo *source.Info + BuildSourceInfo *source.Info // RootFSType specifies the filesystem type for the root partition RootFSType string @@ -209,15 +211,35 @@ func genPartitionTable(c *ManifestConfig, customizations *blueprint.Customizatio if err != nil { return nil, fmt.Errorf("error reading disk customizations: %w", err) } + var partitionTable *disk.PartitionTable switch { // XXX: move into images library case fsCust != nil && diskCust != nil: return nil, fmt.Errorf("cannot combine disk and filesystem customizations") case diskCust != nil: - return genPartitionTableDiskCust(c, diskCust, rng) + partitionTable, err = genPartitionTableDiskCust(c, diskCust, rng) + if err != nil { + return nil, err + } default: - return genPartitionTableFsCust(c, fsCust, rng) + partitionTable, err = genPartitionTableFsCust(c, fsCust, rng) + if err != nil { + return nil, err + } + } + + // Ensure ext4 rootfs has fs-verity enabled + rootfs := partitionTable.FindMountable("/") + if rootfs != nil { + switch elem := rootfs.(type) { + case *disk.Filesystem: + if elem.Type == "ext4" { + elem.MkfsOptions = append(elem.MkfsOptions, []disk.MkfsOption{disk.MkfsVerity}...) + } + } } + + return partitionTable, nil } // calcRequiredDirectorySizes will calculate the minimum sizes for / @@ -323,17 +345,25 @@ func manifestForDiskImage(c *ManifestConfig, rng *rand.Rand) (*manifest.Manifest Name: c.Imgref, Local: true, } + buildContainerSource := container.SourceSpec{ + Source: c.BuildImgref, + Name: c.BuildImgref, + Local: true, + } var customizations *blueprint.Customizations if c.Config != nil { customizations = c.Config.Customizations } - img := image.NewBootcDiskImage(containerSource) + img := image.NewBootcDiskImage(containerSource, buildContainerSource) img.Users = users.UsersFromBP(customizations.GetUsers()) img.Groups = users.GroupsFromBP(customizations.GetGroups()) - // TODO: get from the bootc container instead of hardcoding it - img.SELinux = "targeted" + img.SELinux = c.SourceInfo.SELinuxPolicy + img.BuildSELinux = img.SELinux + if c.BuildSourceInfo != nil { + img.BuildSELinux = c.BuildSourceInfo.SELinuxPolicy + } img.KernelOptionsAppend = []string{ "rw", diff --git a/bib/cmd/bootc-image-builder/image_test.go b/bib/cmd/bootc-image-builder/image_test.go index c84e8883..487ae7d9 100644 --- a/bib/cmd/bootc-image-builder/image_test.go +++ b/bib/cmd/bootc-image-builder/image_test.go @@ -375,8 +375,9 @@ func findMountableSizeableFor(pt *disk.PartitionTable, needle string) (disk.Moun func TestGenPartitionTableSetsRootfsForAllFilesystemsXFS(t *testing.T) { rng := bib.CreateRand() + a, _ := arch.FromString("amd64") cnf := &bib.ManifestConfig{ - Architecture: arch.FromString("amd64"), + Architecture: a, RootFSType: "xfs", } cus := &blueprint.Customizations{ @@ -406,8 +407,9 @@ func TestGenPartitionTableSetsRootfsForAllFilesystemsXFS(t *testing.T) { func TestGenPartitionTableSetsRootfsForAllFilesystemsBtrfs(t *testing.T) { rng := bib.CreateRand() + a, _ := arch.FromString("amd64") cnf := &bib.ManifestConfig{ - Architecture: arch.FromString("amd64"), + Architecture: a, RootFSType: "btrfs", } cus := &blueprint.Customizations{} @@ -429,8 +431,9 @@ func TestGenPartitionTableSetsRootfsForAllFilesystemsBtrfs(t *testing.T) { func TestGenPartitionTableDiskCustomizationRunsValidateLayoutConstraints(t *testing.T) { rng := bib.CreateRand() + a, _ := arch.FromString("amd64") cnf := &bib.ManifestConfig{ - Architecture: arch.FromString("amd64"), + Architecture: a, RootFSType: "xfs", } cus := &blueprint.Customizations{ @@ -650,8 +653,9 @@ func TestGenPartitionTableDiskCustomizationSizes(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { + a, _ := arch.FromString("amd64") cnf := &bib.ManifestConfig{ - Architecture: arch.FromString("amd64"), + Architecture: a, RootFSType: "xfs", RootfsMinsize: tc.rootfsMinSize, } diff --git a/bib/cmd/bootc-image-builder/main.go b/bib/cmd/bootc-image-builder/main.go index c5d734bc..996d4304 100644 --- a/bib/cmd/bootc-image-builder/main.go +++ b/bib/cmd/bootc-image-builder/main.go @@ -203,6 +203,7 @@ func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.Progress 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 @@ -216,17 +217,23 @@ func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.Progress } } - if targetArch != "" && arch.FromString(targetArch) != 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") + 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 } - cntArch = arch.FromString(targetArch) } // TODO: add "target-variant", see https://github.com/osbuild/bootc-image-builder/pull/139/files#r1467591868 @@ -285,26 +292,55 @@ func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.Progress 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 = source.LoadInfo(buildContainer.Root()) + if err != nil { + return nil, nil, err + } + } else { + buildImgref = imgref + } + // This is needed just for RHEL and RHSM in most cases, but let's run it every time in case // the image has some non-standard dnf plugins. - if err := container.InitDNF(); err != nil { + if err := buildContainer.InitDNF(); err != nil { return nil, nil, err } - solver, err := container.NewContainerSolver(rpmCacheRoot, cntArch, sourceinfo) + solver, err := buildContainer.NewContainerSolver(rpmCacheRoot, cntArch, sourceinfo) if err != nil { return nil, nil, err } manifestConfig := &ManifestConfig{ - Architecture: cntArch, - Config: config, - ImageTypes: imageTypes, - Imgref: imgref, - RootfsMinsize: cntSize * containerSizeToDiskSizeMultiplier, - DistroDefPaths: distroDefPaths, - SourceInfo: sourceinfo, - RootFSType: rootfsType, - UseLibrepo: useLibrepo, + 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, solver, rpmCacheRoot) @@ -644,6 +680,7 @@ func buildCobraCmdline() (*cobra.Command, error) { } 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 { diff --git a/bib/go.mod b/bib/go.mod index a46c42e7..5a1a8e9f 100644 --- a/bib/go.mod +++ b/bib/go.mod @@ -131,3 +131,5 @@ require ( google.golang.org/protobuf v1.36.5 // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) + +replace github.com/osbuild/images => github.com/alexlarsson/osbuild-images v0.0.0-20250516113537-a29ea0d42b03 diff --git a/bib/go.sum b/bib/go.sum index 819deff2..2975a502 100644 --- a/bib/go.sum +++ b/bib/go.sum @@ -16,6 +16,8 @@ github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1o github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= +github.com/alexlarsson/osbuild-images v0.0.0-20250516113537-a29ea0d42b03 h1:+ADbFl/OFhoaOZJH93urWXR+ERryiDc5BmLAb3xKKKc= +github.com/alexlarsson/osbuild-images v0.0.0-20250516113537-a29ea0d42b03/go.mod h1:jY21PhkxIozII4M0xCqZL7poLtFwDJlEGj88pb3lalQ= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= @@ -239,8 +241,6 @@ github.com/osbuild/blueprint v1.6.0 h1:HUV1w/dMxpgqOgVtHhfTZE3zRmWQkuW/qTfx9smKI github.com/osbuild/blueprint v1.6.0/go.mod h1:0d3dlY8aSJ6jM6NHwBmJFF1VIySsp/GsDpcJQ0yrOqM= github.com/osbuild/image-builder-cli v0.0.0-20250331194259-63bb56e12db3 h1:M3yYunKH4quwJLQrnFo7dEwCTKorafNC+AUqAo7m5Yo= github.com/osbuild/image-builder-cli v0.0.0-20250331194259-63bb56e12db3/go.mod h1:0sEmiQiMo1ChSuOoeONN0RmsoZbQEvj2mlO2448gC5w= -github.com/osbuild/images v0.145.0 h1:ZbY13lP02dJ090TTKq8UrPjuDrijPWKUMZQEG0zVRpA= -github.com/osbuild/images v0.145.0/go.mod h1:jY21PhkxIozII4M0xCqZL7poLtFwDJlEGj88pb3lalQ= github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f h1:/UDgs8FGMqwnHagNDPGOlts35QkhAZ8by3DR7nMih7M= github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f/go.mod h1:J6OG6YJVEWopen4avK3VNQSnALmmjvniMmni/YFYAwc= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= diff --git a/bib/internal/source/source.go b/bib/internal/source/source.go index 4288e238..84eb6e0f 100644 --- a/bib/internal/source/source.go +++ b/bib/internal/source/source.go @@ -1,6 +1,8 @@ package source import ( + "bufio" + "errors" "fmt" "os" "path" @@ -21,8 +23,9 @@ type OSRelease struct { } type Info struct { - OSRelease OSRelease - UEFIVendor string + OSRelease OSRelease + UEFIVendor string + SELinuxPolicy string } func validateOSRelease(osrelease map[string]string) error { @@ -58,6 +61,39 @@ func uefiVendor(root string) (string, error) { return "", fmt.Errorf("cannot find UEFI vendor in %s", bootupdEfiDir) } +func readSelinuxPolicy(root string) (string, error) { + configPath := "etc/selinux/config" + f, err := os.Open(path.Join(root, configPath)) + if err != nil { + return "", fmt.Errorf("cannot read selinux config %s: %w", configPath, err) + } + // nolint:errcheck + defer f.Close() + + policy := "" + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if len(line) == 0 { + continue + } + if strings.HasPrefix(line, "#") { + continue + } + + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + return "", errors.New("selinux config: invalid input") + } + key := strings.TrimSpace(parts[0]) + if key == "SELINUXTYPE" { + policy = strings.TrimSpace(parts[1]) + } + } + + return policy, nil +} + func LoadInfo(root string) (*Info, error) { osrelease, err := distro.ReadOSReleaseFromTree(root) if err != nil { @@ -71,6 +107,12 @@ func LoadInfo(root string) (*Info, error) { if err != nil { logrus.Debugf("cannot read UEFI vendor: %v, setting it to none", err) } + + selinuxPolicy, err := readSelinuxPolicy(root) + if err != nil { + logrus.Debugf("cannot read selinux policy: %v, setting it to none", err) + } + var idLike []string if osrelease["ID_LIKE"] != "" { idLike = strings.Split(osrelease["ID_LIKE"], " ") @@ -86,6 +128,7 @@ func LoadInfo(root string) (*Info, error) { IDLike: idLike, }, - UEFIVendor: vendor, + UEFIVendor: vendor, + SELinuxPolicy: selinuxPolicy, }, nil }