diff --git a/pkg/distro/distro_test.go b/pkg/distro/distro_test.go index 78b8244498..e6432c5909 100644 --- a/pkg/distro/distro_test.go +++ b/pkg/distro/distro_test.go @@ -36,6 +36,8 @@ func listTestedDistros(t *testing.T) []string { // needed for knowing the names of pipelines from the static object without // having access to a manifest, which we need when parsing metadata from build // results. +// NOTE: The static list of pipelines really only needs to include those that +// have rpm or ostree metadata in them. func TestImageTypePipelineNames(t *testing.T) { // types for parsing the opaque manifest with just the fields we care about type rpmStageOptions struct { @@ -53,16 +55,16 @@ func TestImageTypePipelineNames(t *testing.T) { Pipelines []pipeline `json:"pipelines"` } - assert := assert.New(t) distroFactory := distrofactory.NewDefault() distros := listTestedDistros(t) for _, distroName := range distros { d := distroFactory.GetDistro(distroName) for _, archName := range d.ListArches() { arch, err := d.GetArch(archName) - assert.Nil(err) + assert.Nil(t, err) for _, imageTypeName := range arch.ListImageTypes() { t.Run(fmt.Sprintf("%s/%s/%s", distroName, archName, imageTypeName), func(t *testing.T) { + assert := assert.New(t) imageType, err := arch.GetImageType(imageTypeName) assert.Nil(err) @@ -138,11 +140,10 @@ func TestImageTypePipelineNames(t *testing.T) { err = json.Unmarshal(mf, pm) assert.NoError(err) - assert.Equal(len(allPipelines), len(pm.Pipelines)) + var pmNames []string for idx := range pm.Pipelines { - // manifest pipeline names should be identical to the ones - // defined in the image type and in the same order - assert.Equal(allPipelines[idx], pm.Pipelines[idx].Name) + // Gather the names of the manifest piplines for later + pmNames = append(pmNames, pm.Pipelines[idx].Name) if pm.Pipelines[idx].Name == "os" { rpmStagePresent := false @@ -171,6 +172,15 @@ func TestImageTypePipelineNames(t *testing.T) { // sure they match. assert.Equal(imageType.Exports()[0], pm.Pipelines[len(pm.Pipelines)-1].Name) + // The pipelines named in allPipelines must exist in the manifest, and in the + // order specified (eg. 'build' first) but it does not need to be an exact + // match. Only the pipelines with rpm or ostree metadata are required. + var order int + for _, name := range allPipelines { + idx := slices.Index(pmNames, name) + assert.True(idx >= order, "%s not in order %v", name, pmNames) + order = idx + } }) } } diff --git a/pkg/distro/fedora/distro.go b/pkg/distro/fedora/distro.go index 37660d3d64..744d8baa00 100644 --- a/pkg/distro/fedora/distro.go +++ b/pkg/distro/fedora/distro.go @@ -100,7 +100,7 @@ var ( // We don't know the variant of the OS pipeline being installed isoLabel: getISOLabelFunc("Unknown"), buildPipelines: []string{"build"}, - payloadPipelines: []string{"anaconda-tree", "rootfs-image", "efiboot-tree", "os", "bootiso-tree", "bootiso"}, + payloadPipelines: []string{"anaconda-tree", "efiboot-tree", "os", "bootiso-tree", "bootiso"}, exports: []string{"bootiso"}, requiredPartitionSizes: requiredDirectorySizes, } @@ -119,7 +119,7 @@ var ( image: liveInstallerImage, isoLabel: getISOLabelFunc("Workstation"), buildPipelines: []string{"build"}, - payloadPipelines: []string{"anaconda-tree", "rootfs-image", "efiboot-tree", "bootiso-tree", "bootiso"}, + payloadPipelines: []string{"anaconda-tree", "efiboot-tree", "bootiso-tree", "bootiso"}, exports: []string{"bootiso"}, requiredPartitionSizes: requiredDirectorySizes, } @@ -200,7 +200,7 @@ var ( image: iotInstallerImage, isoLabel: getISOLabelFunc("IoT"), buildPipelines: []string{"build"}, - payloadPipelines: []string{"anaconda-tree", "rootfs-image", "efiboot-tree", "bootiso-tree", "bootiso"}, + payloadPipelines: []string{"anaconda-tree", "efiboot-tree", "bootiso-tree", "bootiso"}, exports: []string{"bootiso"}, requiredPartitionSizes: requiredDirectorySizes, } diff --git a/pkg/distro/fedora/images.go b/pkg/distro/fedora/images.go index 5c56a94316..8b1be8b618 100644 --- a/pkg/distro/fedora/images.go +++ b/pkg/distro/fedora/images.go @@ -403,6 +403,10 @@ func liveInstallerImage(workload workload.Workload, img.Filename = t.Filename() + if common.VersionGreaterThanOrEqual(img.OSVersion, VERSION_ROOTFS_SQUASHFS) { + img.RootfsType = manifest.SquashfsRootfs + } + return img, nil } @@ -456,8 +460,6 @@ func imageInstallerImage(workload workload.Workload, img.ExtraBasePackages = packageSets[installerPkgsKey] - img.SquashfsCompression = "lz4" - d := t.arch.distro img.Product = d.product @@ -477,6 +479,11 @@ func imageInstallerImage(workload workload.Workload, img.Filename = t.Filename() + img.SquashfsCompression = "lz4" + if common.VersionGreaterThanOrEqual(img.OSVersion, VERSION_ROOTFS_SQUASHFS) { + img.RootfsType = manifest.SquashfsRootfs + } + return img, nil } @@ -660,8 +667,6 @@ func iotInstallerImage(workload workload.Workload, anaconda.ModuleUsers, }...) - img.SquashfsCompression = "lz4" - img.Product = d.product img.Variant = "IoT" img.OSVersion = d.osVersion @@ -675,6 +680,11 @@ func iotInstallerImage(workload workload.Workload, img.Filename = t.Filename() + img.SquashfsCompression = "lz4" + if common.VersionGreaterThanOrEqual(img.OSVersion, VERSION_ROOTFS_SQUASHFS) { + img.RootfsType = manifest.SquashfsRootfs + } + return img, nil } diff --git a/pkg/distro/fedora/version.go b/pkg/distro/fedora/version.go index 3459eae672..20440932a0 100644 --- a/pkg/distro/fedora/version.go +++ b/pkg/distro/fedora/version.go @@ -2,3 +2,7 @@ package fedora const VERSION_BRANCHED = "42" const VERSION_RAWHIDE = "42" + +// Fedora version 41 and later use a plain squashfs rootfs on the iso instead of +// compressing an ext4 filesystem. +const VERSION_ROOTFS_SQUASHFS = "41" diff --git a/pkg/distro/rhel/images.go b/pkg/distro/rhel/images.go index 8aec7770e4..e043250019 100644 --- a/pkg/distro/rhel/images.go +++ b/pkg/distro/rhel/images.go @@ -496,6 +496,9 @@ func EdgeInstallerImage(workload workload.Workload, img.Kickstart.Timezone, _ = customizations.GetTimezoneSettings() img.SquashfsCompression = "xz" + if t.Arch().Distro().Releasever() == "10" { + img.RootfsType = manifest.SquashfsRootfs + } installerConfig, err := t.getDefaultInstallerConfig() if err != nil { @@ -714,6 +717,9 @@ func ImageInstallerImage(workload workload.Workload, img.AdditionalAnacondaModules = append(img.AdditionalAnacondaModules, anaconda.ModuleUsers) img.SquashfsCompression = "xz" + if t.Arch().Distro().Releasever() == "10" { + img.RootfsType = manifest.SquashfsRootfs + } // put the kickstart file in the root of the iso img.ISORootKickstart = true diff --git a/pkg/distro/rhel/rhel10/bare_metal.go b/pkg/distro/rhel/rhel10/bare_metal.go index 81b9a7eea0..dac600d31c 100644 --- a/pkg/distro/rhel/rhel10/bare_metal.go +++ b/pkg/distro/rhel/rhel10/bare_metal.go @@ -40,7 +40,7 @@ func mkImageInstallerImgType() *rhel.ImageType { }, rhel.ImageInstallerImage, []string{"build"}, - []string{"anaconda-tree", "rootfs-image", "efiboot-tree", "os", "bootiso-tree", "bootiso"}, + []string{"anaconda-tree", "efiboot-tree", "os", "bootiso-tree", "bootiso"}, []string{"bootiso"}, ) diff --git a/pkg/image/anaconda_container_installer.go b/pkg/image/anaconda_container_installer.go index 6e8de770aa..4d7b177d3f 100644 --- a/pkg/image/anaconda_container_installer.go +++ b/pkg/image/anaconda_container_installer.go @@ -23,6 +23,7 @@ type AnacondaContainerInstaller struct { ExtraBasePackages rpmmd.PackageSet SquashfsCompression string + RootfsType manifest.RootfsType ISOLabel string Product string @@ -98,8 +99,13 @@ func (img *AnacondaContainerInstaller) InstantiateManifest(m *manifest.Manifest, } anacondaPipeline.AdditionalDrivers = img.AdditionalDrivers - rootfsImagePipeline := manifest.NewISORootfsImg(buildPipeline, anacondaPipeline) - rootfsImagePipeline.Size = 4 * datasizes.GibiByte + var rootfsImagePipeline *manifest.ISORootfsImg + switch img.RootfsType { + case manifest.SquashfsExt4Rootfs: + rootfsImagePipeline = manifest.NewISORootfsImg(buildPipeline, anacondaPipeline) + rootfsImagePipeline.Size = 4 * datasizes.GibiByte + default: + } bootTreePipeline := manifest.NewEFIBootTree(buildPipeline, img.Product, img.OSVersion) bootTreePipeline.Platform = img.Platform diff --git a/pkg/image/anaconda_live_installer.go b/pkg/image/anaconda_live_installer.go index 393030c991..77738d98c2 100644 --- a/pkg/image/anaconda_live_installer.go +++ b/pkg/image/anaconda_live_installer.go @@ -23,6 +23,9 @@ type AnacondaLiveInstaller struct { ExtraBasePackages rpmmd.PackageSet + SquashfsCompression string + RootfsType manifest.RootfsType + ISOLabel string Product string Variant string @@ -70,8 +73,13 @@ func (img *AnacondaLiveInstaller) InstantiateManifest(m *manifest.Manifest, livePipeline.Checkpoint() - rootfsImagePipeline := manifest.NewISORootfsImg(buildPipeline, livePipeline) - rootfsImagePipeline.Size = 8 * datasizes.GibiByte + var rootfsImagePipeline *manifest.ISORootfsImg + switch img.RootfsType { + case manifest.SquashfsExt4Rootfs: + rootfsImagePipeline = manifest.NewISORootfsImg(buildPipeline, livePipeline) + rootfsImagePipeline.Size = 8 * datasizes.GibiByte + default: + } bootTreePipeline := manifest.NewEFIBootTree(buildPipeline, img.Product, img.OSVersion) bootTreePipeline.Platform = img.Platform @@ -99,6 +107,8 @@ func (img *AnacondaLiveInstaller) InstantiateManifest(m *manifest.Manifest, isoTreePipeline.KernelOpts = kernelOpts isoTreePipeline.ISOLinux = isoLinuxEnabled + isoTreePipeline.SquashfsCompression = img.SquashfsCompression + isoPipeline := manifest.NewISO(buildPipeline, isoTreePipeline, img.ISOLabel) isoPipeline.SetFilename(img.Filename) isoPipeline.ISOLinux = isoLinuxEnabled diff --git a/pkg/image/anaconda_ostree_installer.go b/pkg/image/anaconda_ostree_installer.go index 46f2142b57..fc53cc25d8 100644 --- a/pkg/image/anaconda_ostree_installer.go +++ b/pkg/image/anaconda_ostree_installer.go @@ -29,6 +29,7 @@ type AnacondaOSTreeInstaller struct { Subscription *subscription.ImageOptions SquashfsCompression string + RootfsType manifest.RootfsType ISOLabel string Product string @@ -101,8 +102,13 @@ func (img *AnacondaOSTreeInstaller) InstantiateManifest(m *manifest.Manifest, anacondaPipeline.DisabledAnacondaModules = img.DisabledAnacondaModules anacondaPipeline.AdditionalDrivers = img.AdditionalDrivers - rootfsImagePipeline := manifest.NewISORootfsImg(buildPipeline, anacondaPipeline) - rootfsImagePipeline.Size = 4 * datasizes.GibiByte + var rootfsImagePipeline *manifest.ISORootfsImg + switch img.RootfsType { + case manifest.SquashfsExt4Rootfs: + rootfsImagePipeline = manifest.NewISORootfsImg(buildPipeline, anacondaPipeline) + rootfsImagePipeline.Size = 4 * datasizes.GibiByte + default: + } bootTreePipeline := manifest.NewEFIBootTree(buildPipeline, img.Product, img.OSVersion) bootTreePipeline.Platform = img.Platform diff --git a/pkg/image/anaconda_tar_installer.go b/pkg/image/anaconda_tar_installer.go index 5f3e330466..ad3d3b5c59 100644 --- a/pkg/image/anaconda_tar_installer.go +++ b/pkg/image/anaconda_tar_installer.go @@ -57,6 +57,7 @@ type AnacondaTarInstaller struct { Kickstart *kickstart.Options SquashfsCompression string + RootfsType manifest.RootfsType ISOLabel string Product string @@ -153,8 +154,13 @@ func (img *AnacondaTarInstaller) InstantiateManifest(m *manifest.Manifest, anacondaPipeline.Checkpoint() - rootfsImagePipeline := manifest.NewISORootfsImg(buildPipeline, anacondaPipeline) - rootfsImagePipeline.Size = 5 * datasizes.GibiByte + var rootfsImagePipeline *manifest.ISORootfsImg + switch img.RootfsType { + case manifest.SquashfsExt4Rootfs: + rootfsImagePipeline = manifest.NewISORootfsImg(buildPipeline, anacondaPipeline) + rootfsImagePipeline.Size = 5 * datasizes.GibiByte + default: + } bootTreePipeline := manifest.NewEFIBootTree(buildPipeline, img.Product, img.OSVersion) bootTreePipeline.Platform = img.Platform diff --git a/pkg/image/installer_image_test.go b/pkg/image/installer_image_test.go index 7ebd17cecc..147cd78ad4 100644 --- a/pkg/image/installer_image_test.go +++ b/pkg/image/installer_image_test.go @@ -122,6 +122,37 @@ func TestContainerInstallerSetKSPath(t *testing.T) { assert.NotContains(t, mfs, "osbuild.ks") // no mention of the default value anywhere } +func TestContainerInstallerExt4Rootfs(t *testing.T) { + img := image.NewAnacondaContainerInstaller(container.SourceSpec{}, "") + img.Product = product + img.OSVersion = osversion + img.ISOLabel = isolabel + + assert.NotNil(t, img) + img.Platform = testPlatform + mfs := instantiateAndSerialize(t, img, mockPackageSets(), mockContainerSpecs(), nil) + + // Confirm that it includes the rootfs-image pipeline that makes the ext4 rootfs + assert.Contains(t, mfs, `"name":"rootfs-image"`) + assert.Contains(t, mfs, `"name:rootfs-image"`) +} + +func TestContainerInstallerSquashfsRootfs(t *testing.T) { + img := image.NewAnacondaContainerInstaller(container.SourceSpec{}, "") + img.Product = product + img.OSVersion = osversion + img.ISOLabel = isolabel + img.RootfsType = manifest.SquashfsRootfs + + assert.NotNil(t, img) + img.Platform = testPlatform + mfs := instantiateAndSerialize(t, img, mockPackageSets(), mockContainerSpecs(), nil) + + // Confirm that it does not include rootfs-image pipeline + assert.NotContains(t, mfs, `"name":"rootfs-image"`) + assert.NotContains(t, mfs, `"name:rootfs-image"`) +} + func TestOSTreeInstallerUnsetKSPath(t *testing.T) { img := image.NewAnacondaOSTreeInstaller(ostree.SourceSpec{}) img.Product = product @@ -158,6 +189,47 @@ func TestOSTreeInstallerSetKSPath(t *testing.T) { assert.NotContains(t, mfs, "osbuild.ks") // no mention of the default value anywhere } +func TestOSTreeInstallerExt4Rootfs(t *testing.T) { + img := image.NewAnacondaOSTreeInstaller(ostree.SourceSpec{}) + img.Product = product + img.OSVersion = osversion + img.ISOLabel = isolabel + + assert.NotNil(t, img) + img.Platform = testPlatform + img.Kickstart = &kickstart.Options{ + // the ostree options must be non-nil + OSTree: &kickstart.OSTree{}, + } + + mfs := instantiateAndSerialize(t, img, mockPackageSets(), nil, mockOSTreeCommitSpecs()) + + // Confirm that it includes the rootfs-image pipeline that makes the ext4 rootfs + assert.Contains(t, mfs, `"name":"rootfs-image"`) + assert.Contains(t, mfs, `"name:rootfs-image"`) +} + +func TestOSTreeInstallerSquashfsRootfs(t *testing.T) { + img := image.NewAnacondaOSTreeInstaller(ostree.SourceSpec{}) + img.Product = product + img.OSVersion = osversion + img.ISOLabel = isolabel + img.RootfsType = manifest.SquashfsRootfs + + assert.NotNil(t, img) + img.Platform = testPlatform + img.Kickstart = &kickstart.Options{ + // the ostree options must be non-nil + OSTree: &kickstart.OSTree{}, + } + + mfs := instantiateAndSerialize(t, img, mockPackageSets(), nil, mockOSTreeCommitSpecs()) + + // Confirm that it does not include rootfs-image pipeline + assert.NotContains(t, mfs, `"name":"rootfs-image"`) + assert.NotContains(t, mfs, `"name:rootfs-image"`) +} + func TestTarInstallerUnsetKSOptions(t *testing.T) { img := image.NewAnacondaTarInstaller() img.Product = product @@ -220,6 +292,68 @@ func TestTarInstallerSetKSPath(t *testing.T) { assert.NotContains(t, mfs, "osbuild.ks") // no mention of the default value anywhere } +func TestTarInstallerExt4Rootfs(t *testing.T) { + img := image.NewAnacondaTarInstaller() + img.Product = product + img.OSVersion = osversion + img.ISOLabel = isolabel + + assert.NotNil(t, img) + img.Platform = testPlatform + + mfs := instantiateAndSerialize(t, img, mockPackageSets(), nil, nil) + // Confirm that it includes the rootfs-image pipeline that makes the ext4 rootfs + assert.Contains(t, mfs, `"name":"rootfs-image"`) + assert.Contains(t, mfs, `"name:rootfs-image"`) +} + +func TestTarInstallerSquashfsRootfs(t *testing.T) { + img := image.NewAnacondaTarInstaller() + img.Product = product + img.OSVersion = osversion + img.ISOLabel = isolabel + img.RootfsType = manifest.SquashfsRootfs + + assert.NotNil(t, img) + img.Platform = testPlatform + + mfs := instantiateAndSerialize(t, img, mockPackageSets(), nil, nil) + // Confirm that it does not include rootfs-image pipeline + assert.NotContains(t, mfs, `"name":"rootfs-image"`) + assert.NotContains(t, mfs, `"name:rootfs-image"`) +} + +func TestLiveInstallerExt4Rootfs(t *testing.T) { + img := image.NewAnacondaLiveInstaller() + img.Product = product + img.OSVersion = osversion + img.ISOLabel = isolabel + + assert.NotNil(t, img) + img.Platform = testPlatform + + mfs := instantiateAndSerialize(t, img, mockPackageSets(), nil, nil) + // Confirm that it includes the rootfs-image pipeline that makes the ext4 rootfs + assert.Contains(t, mfs, `"name":"rootfs-image"`) + assert.Contains(t, mfs, `"name:rootfs-image"`) +} + +func TestLiveInstallerSquashfsRootfs(t *testing.T) { + img := image.NewAnacondaLiveInstaller() + img.Product = product + img.OSVersion = osversion + img.ISOLabel = isolabel + img.RootfsType = manifest.SquashfsRootfs + + assert.NotNil(t, img) + img.Platform = testPlatform + + mfs := instantiateAndSerialize(t, img, mockPackageSets(), nil, nil) + // Confirm that it does not include rootfs-image pipeline + assert.NotContains(t, mfs, `"name":"rootfs-image"`) + assert.NotContains(t, mfs, `"name:rootfs-image"`) +} + func instantiateAndSerialize(t *testing.T, img image.ImageKind, packages map[string][]rpmmd.PackageSpec, containers map[string][]container.Spec, commits map[string][]ostree.CommitSpec) string { source := rand.NewSource(int64(0)) // math/rand is good enough in this case diff --git a/pkg/manifest/anaconda_installer_iso_tree.go b/pkg/manifest/anaconda_installer_iso_tree.go index 65d6fa2bff..403edb8a41 100644 --- a/pkg/manifest/anaconda_installer_iso_tree.go +++ b/pkg/manifest/anaconda_installer_iso_tree.go @@ -17,6 +17,14 @@ import ( "github.com/osbuild/images/pkg/rpmmd" ) +type RootfsType uint64 + +// These constants are used by the ISO images to control the style of the root filesystem +const ( // Rootfs type enum + SquashfsExt4Rootfs RootfsType = iota // Create an EXT4 rootfs compressed by Squashfs + SquashfsRootfs // Create a plain squashfs rootfs +) + // An AnacondaInstallerISOTree represents a tree containing the anaconda installer, // configuration in terms of a kickstart file, as well as an embedded // payload to be installed, this payload can either be an ostree @@ -30,7 +38,7 @@ type AnacondaInstallerISOTree struct { PartitionTable *disk.PartitionTable anacondaPipeline *AnacondaInstaller - rootfsPipeline *ISORootfsImg + rootfsPipeline *ISORootfsImg // May be nil for plain squashfs rootfs bootTreePipeline *EFIBootTree // The path where the payload (tarball, ostree repo, or container) will be stored. @@ -68,7 +76,7 @@ type AnacondaInstallerISOTree struct { func NewAnacondaInstallerISOTree(buildPipeline Build, anacondaPipeline *AnacondaInstaller, rootfsPipeline *ISORootfsImg, bootTreePipeline *EFIBootTree) *AnacondaInstallerISOTree { // the three pipelines should all belong to the same manifest - if anacondaPipeline.Manifest() != rootfsPipeline.Manifest() || + if (rootfsPipeline != nil && anacondaPipeline.Manifest() != rootfsPipeline.Manifest()) || anacondaPipeline.Manifest() != bootTreePipeline.Manifest() { panic("pipelines from different manifests") } @@ -278,7 +286,14 @@ func (p *AnacondaInstallerISOTree) serialize() osbuild.Pipeline { } } - squashfsStage := osbuild.NewSquashfsStage(&squashfsOptions, p.rootfsPipeline.Name()) + // The iso's rootfs can either be an ext4 filesystem compressed with squashfs, or + // a squashfs of the plain directory tree + var squashfsStage *osbuild.Stage + if p.rootfsPipeline != nil { + squashfsStage = osbuild.NewSquashfsStage(&squashfsOptions, p.rootfsPipeline.Name()) + } else { + squashfsStage = osbuild.NewSquashfsStage(&squashfsOptions, p.anacondaPipeline.Name()) + } pipeline.AddStage(squashfsStage) if p.ISOLinux {