diff --git a/bib/cmd/bootc-image-builder/image.go b/bib/cmd/bootc-image-builder/image.go index a041ce3c..09d43a47 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 @@ -221,15 +223,35 @@ func genPartitionTable(c *ManifestConfig, customizations *blueprint.Customizatio } } + 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 / @@ -335,17 +357,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..7b80f40d 100644 --- a/bib/go.mod +++ b/bib/go.mod @@ -6,9 +6,9 @@ require ( github.com/BurntSushi/toml v1.5.1-0.20250403130103-3d3abc24416a github.com/cheggaaa/pb/v3 v3.1.7 github.com/hashicorp/go-version v1.7.0 - github.com/osbuild/blueprint v1.6.0 + github.com/osbuild/blueprint v1.7.0 github.com/osbuild/image-builder-cli v0.0.0-20250331194259-63bb56e12db3 - github.com/osbuild/images v0.145.0 + github.com/osbuild/images v0.147.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.6 diff --git a/bib/go.sum b/bib/go.sum index 819deff2..8a5dfe67 100644 --- a/bib/go.sum +++ b/bib/go.sum @@ -235,12 +235,12 @@ github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/selinux v1.11.1 h1:nHFvthhM0qY8/m+vfhJylliSshm8G1jJ2jDMcgULaH8= github.com/opencontainers/selinux v1.11.1/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= -github.com/osbuild/blueprint v1.6.0 h1:HUV1w/dMxpgqOgVtHhfTZE3zRmWQkuW/qTfx9smKImI= -github.com/osbuild/blueprint v1.6.0/go.mod h1:0d3dlY8aSJ6jM6NHwBmJFF1VIySsp/GsDpcJQ0yrOqM= +github.com/osbuild/blueprint v1.7.0 h1:SpuoFtTc0pofX89EcMrxPCVSPcN4rFGAe/H/brEEBjs= +github.com/osbuild/blueprint v1.7.0/go.mod h1:LfxBgOupiH6h6dfFHAkHK9Kpj9Yd7cSHnQd6zIiuKlc= 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/osbuild/images v0.147.0 h1:vLl8xbbY4sUHHToFkC4MimWTrYWAgaHu5ea9JGRsQmU= +github.com/osbuild/images v0.147.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 059bfccc..cbc345fb 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" @@ -27,6 +29,7 @@ type OSRelease struct { type Info struct { OSRelease OSRelease UEFIVendor string + SELinuxPolicy string ImageCustomization *blueprint.Customizations } @@ -63,6 +66,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 readImageCustomization(root string) (*blueprint.Customizations, error) { prefix := path.Join(root, bibPathPrefix) config, err := buildconfig.LoadConfig(path.Join(prefix, "config.json")) @@ -102,6 +138,11 @@ func LoadInfo(root string) (*Info, error) { return nil, 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"], " ") @@ -118,6 +159,7 @@ func LoadInfo(root string) (*Info, error) { }, UEFIVendor: vendor, + SELinuxPolicy: selinuxPolicy, ImageCustomization: customization, }, nil }