diff --git a/cmd/gen-manifests/main.go b/cmd/gen-manifests/main.go index c3cf112751..3631793840 100644 --- a/cmd/gen-manifests/main.go +++ b/cmd/gen-manifests/main.go @@ -14,6 +14,7 @@ import ( "os" "path/filepath" "runtime/debug" + "slices" "strings" "time" @@ -611,9 +612,11 @@ func main() { DefaultFs string `yaml:"default_fs"` ContainerSize uint64 `yaml:"container_size"` ImageRef string `yaml:"image_ref"` + ImageTypes []string `yaml:"image_types"` - BuildContainerRef string `yaml:"build_container_ref"` - BuildContainerInfo osinfo.Info `yaml:"build_container_info"` + BuildContainerRef string `yaml:"build_container_ref"` + BuildContainerInfo osinfo.Info `yaml:"build_container_info"` + PayloadContainerRef string `yaml:"payload_container_ref"` } type fakeContainersYAML struct { Containers []fakeBootcContainerYAML @@ -629,11 +632,6 @@ func main() { if err != nil { panic(err) } - if fakeBootcCnt.BuildContainerRef != "" { - if err := distribution.SetBuildContainerForTesting(fakeBootcCnt.BuildContainerRef, &fakeBootcCnt.BuildContainerInfo); err != nil { - panic(err) - } - } arches, _ := arches.ResolveArgValues(distribution.ListArches()) for _, archName := range arches { @@ -643,6 +641,16 @@ func main() { } imgTypes, _ := imgTypes.ResolveArgValues(archi.ListImageTypes()) for _, imgTypeName := range imgTypes { + if !slices.Contains(fakeBootcCnt.ImageTypes, imgTypeName) { + continue + } + + if fakeBootcCnt.BuildContainerRef != "" { + if err := distribution.SetBuildContainerForTesting(fakeBootcCnt.BuildContainerRef, &fakeBootcCnt.BuildContainerInfo); err != nil { + panic(err) + } + } + imgType, err := archi.GetImageType(imgTypeName) if err != nil { panic(err) @@ -660,6 +668,11 @@ func main() { fmt.Printf("Skipping %s for %s/%s (reason: %v)\n", itConfig.Name, imgTypeName, distribution.Name(), reason) continue } + if fakeBootcCnt.PayloadContainerRef != "" { + itConfig.Options.Bootc = &distro.BootcImageOptions{ + InstallerPayloadRef: fakeBootcCnt.PayloadContainerRef, + } + } var repos []rpmmd.RepoConfig job := makeManifestJob(itConfig, imgType, distribution, repos, archName, cacheRoot, outputDir, contentResolve, metadata, tmpdirRoot) diff --git a/pkg/bib/osinfo/osinfo.go b/pkg/bib/osinfo/osinfo.go index 693a24dd84..6dd4c1be33 100644 --- a/pkg/bib/osinfo/osinfo.go +++ b/pkg/bib/osinfo/osinfo.go @@ -40,7 +40,7 @@ type Info struct { UEFIVendor string `yaml:"uefi_vendor"` SELinuxPolicy string `yaml:"selinux_policy"` ImageCustomization *blueprint.Customizations - KernelInfo *KernelInfo + KernelInfo *KernelInfo `yaml:"kernel_info"` MountConfiguration *osbuild.MountConfiguration PartitionTable *disk.PartitionTable diff --git a/pkg/distro/bootc/bootc.go b/pkg/distro/bootc/bootc.go index f4101dad94..c8c2d51e9e 100644 --- a/pkg/distro/bootc/bootc.go +++ b/pkg/distro/bootc/bootc.go @@ -312,20 +312,7 @@ func (t *BootcImageType) Manifest(bp *blueprint.Blueprint, options distro.ImageO //nolint:gosec rng := rand.New(rand.NewSource(seed)) - archi := common.Must(arch.FromString(t.arch.Name())) - platform := &platform.Data{ - Arch: archi, - UEFIVendor: t.arch.distro.sourceInfo.UEFIVendor, - QCOW2Compat: "1.1", - } - switch archi { - case arch.ARCH_X86_64: - platform.BIOSPlatform = "i386-pc" - case arch.ARCH_PPC64LE: - platform.BIOSPlatform = "powerpc-ieee1275" - case arch.ARCH_S390X: - platform.ZiplSupport = true - } + platform := PlatformFor(t.arch.Name(), t.arch.distro.sourceInfo.UEFIVendor) // For the bootc-disk image, the filename is the basename and // the extension is added automatically for each disk format filename := strings.Split(t.filename, ".")[0] @@ -487,6 +474,12 @@ func newBootcDistroAfterIntrospect(archStr string, info *osinfo.Info, imgref, de filename: "image.ova", }, ) + ba.imageTypes["bootc-installer"] = &BootcAnacondaInstaller{ + arch: ba, + name: "bootc-installer", + export: "bootiso", + } + bd.addArches(ba) return bd, nil diff --git a/pkg/distro/bootc/iso.go b/pkg/distro/bootc/iso.go new file mode 100644 index 0000000000..dc0ce0a0a5 --- /dev/null +++ b/pkg/distro/bootc/iso.go @@ -0,0 +1,233 @@ +package bootc + +import ( + "fmt" + "math/rand" + + "github.com/osbuild/blueprint/pkg/blueprint" + "github.com/osbuild/images/internal/cmdutil" + "github.com/osbuild/images/pkg/arch" + "github.com/osbuild/images/pkg/container" + "github.com/osbuild/images/pkg/customizations/anaconda" + "github.com/osbuild/images/pkg/customizations/kickstart" + "github.com/osbuild/images/pkg/disk" + "github.com/osbuild/images/pkg/distro" + "github.com/osbuild/images/pkg/image" + "github.com/osbuild/images/pkg/manifest" + "github.com/osbuild/images/pkg/osbuild" + "github.com/osbuild/images/pkg/platform" + "github.com/osbuild/images/pkg/rpmmd" +) + +var _ = distro.ImageType(&BootcAnacondaInstaller{}) + +// BootcAnacondaInstaller is an image-type for a bootc +// container based ISO installer. +type BootcAnacondaInstaller struct { + arch *BootcArch + + name string + export string +} + +func (t *BootcAnacondaInstaller) Name() string { + return t.name +} + +func (t *BootcAnacondaInstaller) Aliases() []string { + return nil +} + +func (t *BootcAnacondaInstaller) Arch() distro.Arch { + return t.arch +} + +func (t *BootcAnacondaInstaller) Filename() string { + return "installer.iso" +} + +func (t *BootcAnacondaInstaller) MIMEType() string { + return "application/x-iso9660-image" +} + +func (t *BootcAnacondaInstaller) OSTreeRef() string { + return "" +} + +func (t *BootcAnacondaInstaller) ISOLabel() (string, error) { + return "Unknown", nil +} + +func (t *BootcAnacondaInstaller) Size(size uint64) uint64 { + return size +} + +func (t *BootcAnacondaInstaller) PartitionType() disk.PartitionTableType { + return disk.PT_NONE +} + +func (t *BootcAnacondaInstaller) BasePartitionTable() (*disk.PartitionTable, error) { + return nil, nil +} + +func (t *BootcAnacondaInstaller) BootMode() platform.BootMode { + return platform.BOOT_HYBRID +} + +func (t *BootcAnacondaInstaller) BuildPipelines() []string { + return []string{"build"} +} + +func (t *BootcAnacondaInstaller) PayloadPipelines() []string { + return []string{""} +} + +func (t *BootcAnacondaInstaller) PayloadPackageSets() []string { + return nil +} + +func (t *BootcAnacondaInstaller) Exports() []string { + return []string{t.export} +} + +func (t *BootcAnacondaInstaller) SupportedBlueprintOptions() []string { + // XXX: this is probably too minimal but lets start small + // and expand + return []string{ + "customizations.fips", + "customizations.group", + "customizations.installer", + "customizations.kernel.append", + "customizations.user", + } +} +func (t *BootcAnacondaInstaller) RequiredBlueprintOptions() []string { + return nil +} + +// XXX: duplication with BootcImageType +func (t *BootcAnacondaInstaller) Manifest(bp *blueprint.Blueprint, options distro.ImageOptions, repos []rpmmd.RepoConfig, seedp *int64) (*manifest.Manifest, []string, error) { + if t.arch.distro.imgref == "" { + return nil, nil, fmt.Errorf("internal error: no base image defined") + } + if options.Bootc == nil || options.Bootc.InstallerPayloadRef == "" { + return nil, nil, fmt.Errorf("no installer payload bootc ref set") + } + payloadRef := options.Bootc.InstallerPayloadRef + + containerSource := container.SourceSpec{ + Source: t.arch.distro.imgref, + Name: t.arch.distro.imgref, + Local: true, + } + // XXX: keep it simple for now, we may allow this in the future + if t.arch.distro.buildImgref != t.arch.distro.imgref { + return nil, nil, fmt.Errorf("cannot use build-containers with anaconda installer images") + } + + var customizations *blueprint.Customizations + if bp != nil { + customizations = bp.Customizations + } + seed, err := cmdutil.SeedArgFor(nil, t.Name(), t.arch.Name(), t.arch.distro.Name()) + if err != nil { + return nil, nil, err + } + //nolint:gosec + rng := rand.New(rand.NewSource(seed)) + + platformi := PlatformFor(t.arch.Name(), t.arch.distro.sourceInfo.UEFIVendor) + platformi.ImageFormat = platform.FORMAT_ISO + + // XXX: tons of copied code from + // bootc-image-builder:‎bib/cmd/bootc-image-builder/legacy_iso.go + // but sharing is hard because AnacondaContainerInstaller and + // AnacondaContainerInstallerLegacy are different types so + // a shared helper to set the fields won't work (unless + // reflection urgh). + filename := "install.iso" + + // The ref is not needed and will be removed from the ctor later + // in time + img := image.NewAnacondaContainerInstaller(platformi, filename, containerSource, "") + img.ContainerRemoveSignatures = true + img.RootfsCompression = "zstd" + // kernelVer is used by dracut + img.KernelVer = t.arch.distro.sourceInfo.KernelInfo.Version + img.KernelPath = fmt.Sprintf("lib/modules/%s/vmlinuz", t.arch.distro.sourceInfo.KernelInfo.Version) + img.InitramfsPath = fmt.Sprintf("lib/modules/%s/initramfs.img", t.arch.distro.sourceInfo.KernelInfo.Version) + img.InstallerHome = "/var/roothome" + payloadSource := container.SourceSpec{ + Source: payloadRef, + Name: payloadRef, + Local: true, + } + img.InstallerPayload = payloadSource + + if t.arch.Name() == arch.ARCH_X86_64.String() { + img.InstallerCustomizations.ISOBoot = manifest.Grub2ISOBoot + } + + img.InstallerCustomizations.Product = t.arch.distro.sourceInfo.OSRelease.Name + img.InstallerCustomizations.OSVersion = t.arch.distro.sourceInfo.OSRelease.VersionID + img.InstallerCustomizations.ISOLabel = LabelForISO(&t.arch.distro.sourceInfo.OSRelease, t.arch.Name()) + + img.InstallerCustomizations.FIPS = customizations.GetFIPS() + img.Kickstart, err = kickstart.New(customizations) + if err != nil { + return nil, nil, err + } + img.Kickstart.Path = osbuild.KickstartPathOSBuild + if kopts := customizations.GetKernel(); kopts != nil && kopts.Append != "" { + img.Kickstart.KernelOptionsAppend = append(img.Kickstart.KernelOptionsAppend, kopts.Append) + } + img.Kickstart.NetworkOnBoot = true + + instCust, err := customizations.GetInstaller() + if err != nil { + return nil, nil, err + } + if instCust != nil && instCust.Modules != nil { + img.InstallerCustomizations.EnabledAnacondaModules = append(img.InstallerCustomizations.EnabledAnacondaModules, instCust.Modules.Enable...) + img.InstallerCustomizations.DisabledAnacondaModules = append(img.InstallerCustomizations.DisabledAnacondaModules, instCust.Modules.Disable...) + } + img.InstallerCustomizations.EnabledAnacondaModules = append(img.InstallerCustomizations.EnabledAnacondaModules, + anaconda.ModuleUsers, + anaconda.ModuleServices, + anaconda.ModuleSecurity, + // XXX: get from the imagedefs + anaconda.ModuleNetwork, + anaconda.ModulePayloads, + anaconda.ModuleRuntime, + anaconda.ModuleStorage, + ) + if bpKernel := customizations.GetKernel(); bpKernel.Append != "" { + img.InstallerCustomizations.KernelOptionsAppend = append(img.InstallerCustomizations.KernelOptionsAppend, bpKernel.Append) + } + + img.Kickstart.OSTree = &kickstart.OSTree{ + OSName: "default", + } + img.InstallerCustomizations.LoraxTemplates = LoraxTemplates(t.arch.distro.sourceInfo.OSRelease) + img.InstallerCustomizations.LoraxTemplatePackage = LoraxTemplatePackage(t.arch.distro.sourceInfo.OSRelease) + + // see https://github.com/osbuild/bootc-image-builder/issues/733 + img.InstallerCustomizations.ISORootfsType = manifest.SquashfsRootfs + + installRootfsType, err := disk.NewFSType(t.arch.distro.defaultFs) + if err != nil { + return nil, nil, err + } + img.InstallRootfsType = installRootfsType + + mf := manifest.New() + + foundDistro, foundRunner, err := GetDistroAndRunner(t.arch.distro.sourceInfo.OSRelease) + if err != nil { + return nil, nil, fmt.Errorf("failed to infer distro and runner: %w", err) + } + mf.Distro = foundDistro + + _, err = img.InstantiateManifestFromContainer(&mf, []container.SourceSpec{containerSource}, foundRunner, rng) + return &mf, nil, err +} diff --git a/pkg/distro/bootc/shared.go b/pkg/distro/bootc/shared.go new file mode 100644 index 0000000000..560476ca53 --- /dev/null +++ b/pkg/distro/bootc/shared.go @@ -0,0 +1,156 @@ +package bootc + +import ( + "fmt" + "slices" + "strconv" + "strings" + + "github.com/osbuild/images/internal/common" + "github.com/osbuild/images/pkg/arch" + "github.com/osbuild/images/pkg/bib/osinfo" + "github.com/osbuild/images/pkg/manifest" + "github.com/osbuild/images/pkg/olog" + "github.com/osbuild/images/pkg/platform" + "github.com/osbuild/images/pkg/runner" +) + +// This file contains shared helpers between the various bootc +// image types, both here and in bootc-image-builder. +// Once the legacy bootc ISO type has moved into images we can +// unexport most (all?) of these helpers. + +// TODO: find a way to move them into YAML to make sharing easier +// between package and image based image types +// +// from:https://github.com/osbuild/images/blob/v0.207.0/data/distrodefs/rhel-10/imagetypes.yaml#L169 +var loraxRhelTemplates = []manifest.InstallerLoraxTemplate{ + manifest.InstallerLoraxTemplate{Path: "80-rhel/runtime-postinstall.tmpl"}, + manifest.InstallerLoraxTemplate{Path: "80-rhel/runtime-cleanup.tmpl", AfterDracut: true}, +} + +// from:https://github.com/osbuild/images/blob/v0.207.0/data/distrodefs/fedora/imagetypes.yaml#L408 +var loraxFedoraTemplates = []manifest.InstallerLoraxTemplate{ + manifest.InstallerLoraxTemplate{Path: "99-generic/runtime-postinstall.tmpl"}, + manifest.InstallerLoraxTemplate{Path: "99-generic/runtime-cleanup.tmpl", AfterDracut: true}, +} + +// This will be reused by bootc-image-builder + +func LoraxTemplates(si osinfo.OSRelease) []manifest.InstallerLoraxTemplate { + switch { + case si.ID == "rhel" || slices.Contains(si.IDLike, "rhel") || si.VersionID == "eln": + return loraxRhelTemplates + default: + return loraxFedoraTemplates + } +} +func LoraxTemplatePackage(si osinfo.OSRelease) string { + switch { + case si.ID == "rhel" || slices.Contains(si.IDLike, "rhel") || si.VersionID == "eln": + return "lorax-templates-rhel" + default: + return "lorax-templates-generic" + } +} + +func PlatformFor(archStr, uefiVendor string) *platform.Data { + archi := common.Must(arch.FromString(archStr)) + platform := &platform.Data{ + Arch: archi, + UEFIVendor: uefiVendor, + QCOW2Compat: "1.1", + } + switch archi { + case arch.ARCH_X86_64: + platform.BIOSPlatform = "i386-pc" + case arch.ARCH_PPC64LE: + platform.BIOSPlatform = "powerpc-ieee1275" + case arch.ARCH_S390X: + platform.ZiplSupport = true + } + return platform +} + +func GetDistroAndRunner(osRelease osinfo.OSRelease) (manifest.Distro, runner.Runner, error) { + switch osRelease.ID { + case "fedora": + version, err := strconv.ParseUint(osRelease.VersionID, 10, 64) + if err != nil { + return manifest.DISTRO_NULL, nil, fmt.Errorf("cannot parse Fedora version (%s): %w", osRelease.VersionID, err) + } + + return manifest.DISTRO_FEDORA, &runner.Fedora{ + Version: version, + }, nil + case "centos": + version, err := strconv.ParseUint(osRelease.VersionID, 10, 64) + if err != nil { + return manifest.DISTRO_NULL, nil, fmt.Errorf("cannot parse CentOS version (%s): %w", osRelease.VersionID, err) + } + r := &runner.CentOS{ + Version: version, + } + switch version { + case 9: + return manifest.DISTRO_EL9, r, nil + case 10: + return manifest.DISTRO_EL10, r, nil + default: + olog.Printf("Unknown CentOS version %d, using default distro for manifest generation", version) + return manifest.DISTRO_NULL, r, nil + } + + case "rhel": + versionParts := strings.Split(osRelease.VersionID, ".") + if len(versionParts) != 2 { + return manifest.DISTRO_NULL, nil, fmt.Errorf("invalid RHEL version format: %s", osRelease.VersionID) + } + major, err := strconv.ParseUint(versionParts[0], 10, 64) + if err != nil { + return manifest.DISTRO_NULL, nil, fmt.Errorf("cannot parse RHEL major version (%s): %w", versionParts[0], err) + } + minor, err := strconv.ParseUint(versionParts[1], 10, 64) + if err != nil { + return manifest.DISTRO_NULL, nil, fmt.Errorf("cannot parse RHEL minor version (%s): %w", versionParts[1], err) + } + r := &runner.RHEL{ + Major: major, + Minor: minor, + } + switch major { + case 9: + return manifest.DISTRO_EL9, r, nil + case 10: + return manifest.DISTRO_EL10, r, nil + default: + olog.Printf("Unknown RHEL version %d, using default distro for manifest generation", major) + return manifest.DISTRO_NULL, r, nil + } + } + + olog.Printf("Unknown distro %s, using default runner", osRelease.ID) + return manifest.DISTRO_NULL, &runner.Linux{}, nil +} + +func NeedsRHELLoraxTemplates(si osinfo.OSRelease) bool { + return si.ID == "rhel" || slices.Contains(si.IDLike, "rhel") || si.VersionID == "eln" +} + +func LabelForISO(os *osinfo.OSRelease, arch string) string { + switch os.ID { + case "fedora": + return fmt.Sprintf("Fedora-S-dvd-%s-%s", arch, os.VersionID) + case "centos": + labelTemplate := "CentOS-Stream-%s-BaseOS-%s" + if os.VersionID == "8" { + labelTemplate = "CentOS-Stream-%s-%s-dvd" + } + return fmt.Sprintf(labelTemplate, os.VersionID, arch) + case "rhel": + version := strings.ReplaceAll(os.VersionID, ".", "-") + return fmt.Sprintf("RHEL-%s-BaseOS-%s", version, arch) + default: + return fmt.Sprintf("Container-Installer-%s", arch) + } +} diff --git a/pkg/distro/bootc/shared_test.go b/pkg/distro/bootc/shared_test.go new file mode 100644 index 0000000000..311dda0d72 --- /dev/null +++ b/pkg/distro/bootc/shared_test.go @@ -0,0 +1,58 @@ +package bootc_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/osbuild/images/pkg/manifest" + "github.com/osbuild/images/pkg/runner" + + "github.com/osbuild/images/pkg/bib/osinfo" + "github.com/osbuild/images/pkg/distro/bootc" +) + +func TestGetDistroAndRunner(t *testing.T) { + cases := []struct { + id string + versionID string + expectedDistro manifest.Distro + expectedRunner runner.Runner + expectedErr string + }{ + // Happy + {"fedora", "40", manifest.DISTRO_FEDORA, &runner.Fedora{Version: 40}, ""}, + {"centos", "9", manifest.DISTRO_EL9, &runner.CentOS{Version: 9}, ""}, + {"centos", "10", manifest.DISTRO_EL10, &runner.CentOS{Version: 10}, ""}, + {"centos", "11", manifest.DISTRO_NULL, &runner.CentOS{Version: 11}, ""}, + {"rhel", "9.4", manifest.DISTRO_EL9, &runner.RHEL{Major: 9, Minor: 4}, ""}, + {"rhel", "10.4", manifest.DISTRO_EL10, &runner.RHEL{Major: 10, Minor: 4}, ""}, + {"rhel", "11.4", manifest.DISTRO_NULL, &runner.RHEL{Major: 11, Minor: 4}, ""}, + {"toucanos", "42", manifest.DISTRO_NULL, &runner.Linux{}, ""}, + + // Sad + {"fedora", "asdf", manifest.DISTRO_NULL, nil, "cannot parse Fedora version (asdf)"}, + {"centos", "asdf", manifest.DISTRO_NULL, nil, "cannot parse CentOS version (asdf)"}, + {"rhel", "10", manifest.DISTRO_NULL, nil, "invalid RHEL version format: 10"}, + {"rhel", "10.asdf", manifest.DISTRO_NULL, nil, "cannot parse RHEL minor version (asdf)"}, + } + + for _, c := range cases { + t.Run(fmt.Sprintf("%s-%s", c.id, c.versionID), func(t *testing.T) { + osRelease := osinfo.OSRelease{ + ID: c.id, + VersionID: c.versionID, + } + distro, runner, err := bootc.GetDistroAndRunner(osRelease) + if c.expectedErr != "" { + assert.ErrorContains(t, err, c.expectedErr) + } else { + require.NoError(t, err) + assert.Equal(t, c.expectedDistro, distro) + assert.Equal(t, c.expectedRunner, runner) + } + }) + } +} diff --git a/pkg/distro/distro.go b/pkg/distro/distro.go index e6e41049c7..1a2fa0d2e7 100644 --- a/pkg/distro/distro.go +++ b/pkg/distro/distro.go @@ -135,10 +135,15 @@ type ImageType interface { Manifest(bp *blueprint.Blueprint, options ImageOptions, repos []rpmmd.RepoConfig, seed *int64) (*manifest.Manifest, []string, error) } +type BootcImageOptions struct { + InstallerPayloadRef string `json:"installer_payload_ref,omitempty"` +} + // The ImageOptions specify options for a specific image build type ImageOptions struct { Size uint64 `json:"size"` OSTree *ostree.ImageOptions `json:"ostree,omitempty"` + Bootc *BootcImageOptions `json:"bootc,omitempty"` Subscription *subscription.ImageOptions `json:"subscription,omitempty"` Facts *facts.ImageOptions `json:"facts,omitempty"` PartitioningMode partition.PartitioningMode `json:"partitioning-mode,omitempty"` diff --git a/pkg/image/anaconda_container_installer.go b/pkg/image/anaconda_container_installer.go index a7c933da77..97f4d51945 100644 --- a/pkg/image/anaconda_container_installer.go +++ b/pkg/image/anaconda_container_installer.go @@ -14,7 +14,6 @@ import ( "github.com/osbuild/images/pkg/manifest" "github.com/osbuild/images/pkg/osbuild" "github.com/osbuild/images/pkg/platform" - "github.com/osbuild/images/pkg/rpmmd" "github.com/osbuild/images/pkg/runner" ) @@ -22,13 +21,13 @@ type AnacondaContainerInstaller struct { Base InstallerCustomizations manifest.InstallerCustomizations - ExtraBasePackages rpmmd.PackageSet RootfsCompression string Ref string ContainerSource container.SourceSpec + InstallerPayload container.SourceSpec ContainerRemoveSignatures bool Kickstart *kickstart.Options @@ -39,35 +38,49 @@ type AnacondaContainerInstaller struct { // Filesystem type for the installed system as opposed to that of the ISO. InstallRootfsType disk.FSType + + // KernelVer is needed so that dracut finds it files + KernelVer string + // {Kernel,Initramfs}Path is needed for grub2.iso + KernelPath string + InitramfsPath string + // bootc installer cannot use /root as installer home + InstallerHome string } func NewAnacondaContainerInstaller(platform platform.Platform, filename string, container container.SourceSpec, ref string) *AnacondaContainerInstaller { return &AnacondaContainerInstaller{ - Base: NewBase("container-installer", platform, filename), + Base: NewBase("bootc-installer", platform, filename), ContainerSource: container, Ref: ref, } } -func (img *AnacondaContainerInstaller) InstantiateManifest(m *manifest.Manifest, - repos []rpmmd.RepoConfig, +func (img *AnacondaContainerInstaller) InstantiateManifestFromContainer(m *manifest.Manifest, + containers []container.SourceSpec, runner runner.Runner, rng *rand.Rand) (*artifact.Artifact, error) { - buildPipeline := addBuildBootstrapPipelines(m, runner, repos, &manifest.BuildOptions{ContainerBuildable: true}) - buildPipeline.Checkpoint() + cnts := []container.SourceSpec{img.ContainerSource} + buildPipeline := manifest.NewBuildFromContainer(m, runner, cnts, + &manifest.BuildOptions{ + ContainerBuildable: true, + }) anacondaPipeline := manifest.NewAnacondaInstaller( manifest.AnacondaInstallerTypePayload, buildPipeline, img.platform, - repos, + nil, // repos "kernel", img.InstallerCustomizations, ) + // with bootc we need different kernel/initramfs paths + anacondaPipeline.BootcLivefsContainer = &img.ContainerSource + anacondaPipeline.KernelPath = img.KernelPath + anacondaPipeline.InitramfsPath = img.InitramfsPath + anacondaPipeline.SetKernelVer(img.KernelVer) + anacondaPipeline.InstallerHome = img.InstallerHome - anacondaPipeline.ExtraPackages = img.ExtraBasePackages.Include - anacondaPipeline.ExcludePackages = img.ExtraBasePackages.Exclude - anacondaPipeline.ExtraRepos = img.ExtraBasePackages.Repositories anacondaPipeline.Biosdevname = (img.platform.GetArch() == arch.ARCH_X86_64) anacondaPipeline.Checkpoint() @@ -100,7 +113,14 @@ func (img *AnacondaContainerInstaller) InstantiateManifest(m *manifest.Manifest, img.Kickstart.Path = osbuild.KickstartPathOSBuild } - kernelOpts := []string{fmt.Sprintf("inst.stage2=hd:LABEL=%s", img.InstallerCustomizations.ISOLabel), fmt.Sprintf("inst.ks=hd:LABEL=%s:%s", img.InstallerCustomizations.ISOLabel, img.Kickstart.Path)} + kernelOpts := []string{ + fmt.Sprintf("inst.stage2=hd:LABEL=%s", img.InstallerCustomizations.ISOLabel), + fmt.Sprintf("inst.ks=hd:LABEL=%s:%s", img.InstallerCustomizations.ISOLabel, img.Kickstart.Path), + "console=tty0", + // XXX: we want the graphical installer eventually, just + // need to figure out the dependencies + "inst.text", + } if anacondaPipeline.InstallerCustomizations.FIPS { kernelOpts = append(kernelOpts, "fips=1") } @@ -119,7 +139,7 @@ func (img *AnacondaContainerInstaller) InstantiateManifest(m *manifest.Manifest, isoTreePipeline.PayloadPath = "/container" isoTreePipeline.PayloadRemoveSignatures = img.ContainerRemoveSignatures - isoTreePipeline.ContainerSource = &img.ContainerSource + isoTreePipeline.ContainerSource = &img.InstallerPayload isoTreePipeline.ISOBoot = img.InstallerCustomizations.ISOBoot if anacondaPipeline.InstallerCustomizations.FIPS { isoTreePipeline.KernelOpts = append(isoTreePipeline.KernelOpts, "fips=1") diff --git a/pkg/image/anaconda_container_installer_legacy.go b/pkg/image/anaconda_container_installer_legacy.go new file mode 100644 index 0000000000..041457ce8d --- /dev/null +++ b/pkg/image/anaconda_container_installer_legacy.go @@ -0,0 +1,136 @@ +package image + +import ( + "fmt" + "math/rand" + + "github.com/osbuild/images/pkg/arch" + "github.com/osbuild/images/pkg/artifact" + "github.com/osbuild/images/pkg/container" + "github.com/osbuild/images/pkg/customizations/anaconda" + "github.com/osbuild/images/pkg/customizations/kickstart" + "github.com/osbuild/images/pkg/datasizes" + "github.com/osbuild/images/pkg/disk" + "github.com/osbuild/images/pkg/manifest" + "github.com/osbuild/images/pkg/osbuild" + "github.com/osbuild/images/pkg/platform" + "github.com/osbuild/images/pkg/rpmmd" + "github.com/osbuild/images/pkg/runner" +) + +type AnacondaContainerInstallerLegacy struct { + Base + + InstallerCustomizations manifest.InstallerCustomizations + ExtraBasePackages rpmmd.PackageSet + + RootfsCompression string + + Ref string + + ContainerSource container.SourceSpec + ContainerRemoveSignatures bool + + Kickstart *kickstart.Options + + // Locale for the installer. This should be set to the same locale as the + // ISO OS payload, if known. + Locale string + + // Filesystem type for the installed system as opposed to that of the ISO. + InstallRootfsType disk.FSType +} + +func NewAnacondaContainerInstallerLegacy(platform platform.Platform, filename string, container container.SourceSpec, ref string) *AnacondaContainerInstallerLegacy { + return &AnacondaContainerInstallerLegacy{ + Base: NewBase("container-installer", platform, filename), + ContainerSource: container, + Ref: ref, + } +} + +func (img *AnacondaContainerInstallerLegacy) InstantiateManifest(m *manifest.Manifest, + repos []rpmmd.RepoConfig, + runner runner.Runner, + rng *rand.Rand) (*artifact.Artifact, error) { + buildPipeline := addBuildBootstrapPipelines(m, runner, repos, &manifest.BuildOptions{ContainerBuildable: true}) + buildPipeline.Checkpoint() + + anacondaPipeline := manifest.NewAnacondaInstaller( + manifest.AnacondaInstallerTypePayload, + buildPipeline, + img.platform, + repos, + "kernel", + img.InstallerCustomizations, + ) + + anacondaPipeline.ExtraPackages = img.ExtraBasePackages.Include + anacondaPipeline.ExcludePackages = img.ExtraBasePackages.Exclude + anacondaPipeline.ExtraRepos = img.ExtraBasePackages.Repositories + anacondaPipeline.Biosdevname = (img.platform.GetArch() == arch.ARCH_X86_64) + anacondaPipeline.Checkpoint() + + if anacondaPipeline.InstallerCustomizations.FIPS { + anacondaPipeline.InstallerCustomizations.EnabledAnacondaModules = append( + anacondaPipeline.InstallerCustomizations.EnabledAnacondaModules, + anaconda.ModuleSecurity, + ) + } + + anacondaPipeline.Locale = img.Locale + + var rootfsImagePipeline *manifest.ISORootfsImg + switch img.InstallerCustomizations.ISORootfsType { + case manifest.SquashfsExt4Rootfs: + rootfsImagePipeline = manifest.NewISORootfsImg(buildPipeline, anacondaPipeline) + rootfsImagePipeline.Size = 4 * datasizes.GibiByte + default: + } + + bootTreePipeline := manifest.NewEFIBootTree(buildPipeline, img.InstallerCustomizations.Product, img.InstallerCustomizations.OSVersion) + bootTreePipeline.Platform = img.platform + bootTreePipeline.UEFIVendor = img.platform.GetUEFIVendor() + bootTreePipeline.ISOLabel = img.InstallerCustomizations.ISOLabel + + if img.Kickstart == nil { + img.Kickstart = &kickstart.Options{} + } + if img.Kickstart.Path == "" { + img.Kickstart.Path = osbuild.KickstartPathOSBuild + } + + kernelOpts := []string{fmt.Sprintf("inst.stage2=hd:LABEL=%s", img.InstallerCustomizations.ISOLabel), fmt.Sprintf("inst.ks=hd:LABEL=%s:%s", img.InstallerCustomizations.ISOLabel, img.Kickstart.Path)} + if anacondaPipeline.InstallerCustomizations.FIPS { + kernelOpts = append(kernelOpts, "fips=1") + } + kernelOpts = append(kernelOpts, img.InstallerCustomizations.KernelOptionsAppend...) + bootTreePipeline.KernelOpts = kernelOpts + + isoTreePipeline := manifest.NewAnacondaInstallerISOTree(buildPipeline, anacondaPipeline, rootfsImagePipeline, bootTreePipeline) + isoTreePipeline.PartitionTable = efiBootPartitionTable(rng) + isoTreePipeline.Release = img.InstallerCustomizations.Release + isoTreePipeline.Kickstart = img.Kickstart + + isoTreePipeline.RootfsCompression = img.RootfsCompression + isoTreePipeline.RootfsType = img.InstallerCustomizations.ISORootfsType + + // For ostree installers, always put the kickstart file in the root of the ISO + isoTreePipeline.PayloadPath = "/container" + isoTreePipeline.PayloadRemoveSignatures = img.ContainerRemoveSignatures + + isoTreePipeline.ContainerSource = &img.ContainerSource + isoTreePipeline.ISOBoot = img.InstallerCustomizations.ISOBoot + if anacondaPipeline.InstallerCustomizations.FIPS { + isoTreePipeline.KernelOpts = append(isoTreePipeline.KernelOpts, "fips=1") + } + + isoTreePipeline.InstallRootfsType = img.InstallRootfsType + + isoPipeline := manifest.NewISO(buildPipeline, isoTreePipeline, img.InstallerCustomizations.ISOLabel) + isoPipeline.SetFilename(img.filename) + isoPipeline.ISOBoot = img.InstallerCustomizations.ISOBoot + artifact := isoPipeline.Export() + + return artifact, nil +} diff --git a/pkg/image/installer_image_test.go b/pkg/image/installer_image_test.go index 167f25b94b..00a37e706d 100644 --- a/pkg/image/installer_image_test.go +++ b/pkg/image/installer_image_test.go @@ -99,7 +99,7 @@ const ( ) func TestContainerInstallerUnsetKSOptions(t *testing.T) { - img := image.NewAnacondaContainerInstaller(testPlatform, "filename", container.SourceSpec{}, "") + img := image.NewAnacondaContainerInstallerLegacy(testPlatform, "filename", container.SourceSpec{}, "") assert.NotNil(t, img) img.InstallerCustomizations.Product = product @@ -111,7 +111,7 @@ func TestContainerInstallerUnsetKSOptions(t *testing.T) { } func TestContainerInstallerUnsetKSPath(t *testing.T) { - img := image.NewAnacondaContainerInstaller(testPlatform, "filename", container.SourceSpec{}, "") + img := image.NewAnacondaContainerInstallerLegacy(testPlatform, "filename", container.SourceSpec{}, "") assert.NotNil(t, img) img.InstallerCustomizations.Product = product @@ -125,7 +125,7 @@ func TestContainerInstallerUnsetKSPath(t *testing.T) { } func TestContainerInstallerSetKSPath(t *testing.T) { - img := image.NewAnacondaContainerInstaller(testPlatform, "filename", container.SourceSpec{}, "") + img := image.NewAnacondaContainerInstallerLegacy(testPlatform, "filename", container.SourceSpec{}, "") assert.NotNil(t, img) img.InstallerCustomizations.Product = product @@ -141,7 +141,7 @@ func TestContainerInstallerSetKSPath(t *testing.T) { } func TestContainerInstallerExt4Rootfs(t *testing.T) { - img := image.NewAnacondaContainerInstaller(testPlatform, "filename", container.SourceSpec{}, "") + img := image.NewAnacondaContainerInstallerLegacy(testPlatform, "filename", container.SourceSpec{}, "") assert.NotNil(t, img) img.InstallerCustomizations.Product = product @@ -156,7 +156,7 @@ func TestContainerInstallerExt4Rootfs(t *testing.T) { } func TestContainerInstallerSquashfsRootfs(t *testing.T) { - img := image.NewAnacondaContainerInstaller(testPlatform, "filename", container.SourceSpec{}, "") + img := image.NewAnacondaContainerInstallerLegacy(testPlatform, "filename", container.SourceSpec{}, "") assert.NotNil(t, img) img.InstallerCustomizations.Product = product @@ -375,7 +375,7 @@ func instantiateAndSerialize(t *testing.T, img image.ImageKind, depsolved map[st func TestContainerInstallerPanics(t *testing.T) { assert := assert.New(t) - img := image.NewAnacondaContainerInstaller(testPlatform, "filename", container.SourceSpec{}, "") + img := image.NewAnacondaContainerInstallerLegacy(testPlatform, "filename", container.SourceSpec{}, "") assert.PanicsWithError("org.osbuild.grub2.iso: product.name option is required", func() { instantiateAndSerialize(t, img, mockPackageSets(), mockContainerSpecs(), nil) }) img.InstallerCustomizations.Product = product assert.PanicsWithError("org.osbuild.grub2.iso: product.version option is required", func() { instantiateAndSerialize(t, img, mockPackageSets(), mockContainerSpecs(), nil) }) @@ -486,7 +486,7 @@ func TestContainerInstallerModules(t *testing.T) { // Remove this when we drop support for RHEL 8. for _, legacy := range []bool{true, false} { t.Run(name, func(t *testing.T) { - img := image.NewAnacondaContainerInstaller(testPlatform, "filename", container.SourceSpec{}, "") + img := image.NewAnacondaContainerInstallerLegacy(testPlatform, "filename", container.SourceSpec{}, "") img.InstallerCustomizations.Product = product img.InstallerCustomizations.OSVersion = osversion img.InstallerCustomizations.ISOLabel = isolabel @@ -582,7 +582,7 @@ func TestInstallerLocales(t *testing.T) { for input, expected := range locales { { // Container - img := image.NewAnacondaContainerInstaller(testPlatform, "filename", container.SourceSpec{}, "") + img := image.NewAnacondaContainerInstallerLegacy(testPlatform, "filename", container.SourceSpec{}, "") assert.NotNil(t, img) img.InstallerCustomizations.Product = product @@ -713,7 +713,7 @@ func findGrub2IsoStageOptions(t *testing.T, mf manifest.OSBuildManifest, pipelin } func TestContainerInstallerDracut(t *testing.T) { - img := image.NewAnacondaContainerInstaller(testPlatform, "filename", container.SourceSpec{}, "") + img := image.NewAnacondaContainerInstallerLegacy(testPlatform, "filename", container.SourceSpec{}, "") img.InstallerCustomizations.Product = product img.InstallerCustomizations.OSVersion = osversion img.InstallerCustomizations.ISOLabel = isolabel diff --git a/pkg/manifest/anaconda_installer.go b/pkg/manifest/anaconda_installer.go index af53d254f7..44e560ccb1 100644 --- a/pkg/manifest/anaconda_installer.go +++ b/pkg/manifest/anaconda_installer.go @@ -7,6 +7,7 @@ import ( "github.com/osbuild/images/internal/common" "github.com/osbuild/images/pkg/arch" + "github.com/osbuild/images/pkg/container" "github.com/osbuild/images/pkg/customizations/fsnode" "github.com/osbuild/images/pkg/customizations/kickstart" "github.com/osbuild/images/pkg/customizations/users" @@ -55,6 +56,19 @@ type AnacondaInstaller struct { kernelName string kernelVer string + // some images (like bootc installers) know their path in + // advance + KernelPath string + InitramfsPath string + // bootc installer cannot use /root as installer home + InstallerHome string + + // BootcLivefsContainer is the container to use to + // as the base live filesystem. This is mutally exclusive + // with using RPMs. + BootcLivefsContainer *container.SourceSpec + bootcLivefsContainerSpecs []container.Spec + // Interactive defaults is a kickstart stage that can be provided, it // will be written to /usr/share/anaconda/interactive-defaults InteractiveDefaults *AnacondaInteractiveDefaults @@ -130,7 +144,20 @@ func (p *AnacondaInstaller) anacondaBootPackageSet() ([]string, error) { return packages, nil } +func (p *AnacondaInstaller) getContainerSources() []container.SourceSpec { + if p.BootcLivefsContainer == nil { + return nil + } + return []container.SourceSpec{*p.BootcLivefsContainer} +} + func (p *AnacondaInstaller) getBuildPackages(Distro) ([]string, error) { + // when using a bootc container for the livefs there is no + // need to get packages + if p.BootcLivefsContainer != nil { + return nil, nil + } + packages, err := p.anacondaBootPackageSet() if err != nil { return nil, fmt.Errorf("cannot get anaconda boot packages: %w", err) @@ -153,9 +180,19 @@ func (p *AnacondaInstaller) getBuildPackages(Distro) ([]string, error) { return packages, nil } +func (p *AnacondaInstaller) SetKernelVer(kVer string) { + p.kernelVer = kVer +} + // getPackageSetChain returns the packages to install // It will also include weak deps for the Live installer type func (p *AnacondaInstaller) getPackageSetChain(Distro) ([]rpmmd.PackageSet, error) { + // when using a bootc container for the livefs there is no + // need to get packages + if p.BootcLivefsContainer != nil { + return nil, nil + } + packages, err := p.anacondaBootPackageSet() if err != nil { return nil, fmt.Errorf("cannot get anaconda boot packages: %w", err) @@ -187,11 +224,12 @@ func (p *AnacondaInstaller) getPackageSpecs() rpmmd.PackageList { } func (p *AnacondaInstaller) serializeStart(inputs Inputs) error { - if len(p.packageSpecs) > 0 { + if len(p.packageSpecs) > 0 && len(p.bootcLivefsContainerSpecs) > 0 { return errors.New("AnacondaInstaller: double call to serializeStart()") } p.packageSpecs = inputs.Depsolved.Packages - if p.kernelName != "" { + // bootc-installers will get the kernelVer via introspection + if len(p.packageSpecs) > 0 && p.kernelName != "" { kernelPkg, err := p.packageSpecs.Package(p.kernelName) if err != nil { return fmt.Errorf("AnacondaInstaller: %w", err) @@ -199,15 +237,17 @@ func (p *AnacondaInstaller) serializeStart(inputs Inputs) error { p.kernelVer = kernelPkg.EVRA() } p.repos = append(p.repos, inputs.Depsolved.Repos...) + p.bootcLivefsContainerSpecs = inputs.Containers return nil } func (p *AnacondaInstaller) serializeEnd() { - if len(p.packageSpecs) == 0 { + if len(p.packageSpecs) == 0 && len(p.bootcLivefsContainerSpecs) == 0 { panic("serializeEnd() call when serialization not in progress") } p.kernelVer = "" p.packageSpecs = nil + p.bootcLivefsContainerSpecs = nil } func installerRootUser() osbuild.UsersStageOptionsUser { @@ -217,22 +257,35 @@ func installerRootUser() osbuild.UsersStageOptionsUser { } func (p *AnacondaInstaller) serialize() (osbuild.Pipeline, error) { - if len(p.packageSpecs) == 0 { + if len(p.packageSpecs) == 0 && len(p.bootcLivefsContainerSpecs) == 0 { return osbuild.Pipeline{}, fmt.Errorf("AnacondaInstaller: serialization not started") } + if len(p.packageSpecs) > 0 && len(p.bootcLivefsContainerSpecs) > 0 { + return osbuild.Pipeline{}, fmt.Errorf("AnacondaInstaller: using packages and containers at the same time is n") + } pipeline, err := p.Base.serialize() if err != nil { return osbuild.Pipeline{}, err } - options := osbuild.NewRPMStageOptions(p.repos) - // Documentation is only installed on live installer images - if p.Type != AnacondaInstallerTypeLive { - options.Exclude = &osbuild.Exclude{Docs: true} + if len(p.packageSpecs) > 0 { + options := osbuild.NewRPMStageOptions(p.repos) + // Documentation is only installed on live installer images + if p.Type != AnacondaInstallerTypeLive { + options.Exclude = &osbuild.Exclude{Docs: true} + } + + pipeline.AddStage(osbuild.NewRPMStage(options, osbuild.NewRpmStageSourceFilesInputs(p.packageSpecs))) + } else { + image := osbuild.NewContainersInputForSingleSource(p.bootcLivefsContainerSpecs[0]) + stage, err := osbuild.NewContainerDeployStage(image, &osbuild.ContainerDeployOptions{RemoveSignatures: true}) + if err != nil { + return pipeline, err + } + pipeline.AddStage(stage) } - pipeline.AddStage(osbuild.NewRPMStage(options, osbuild.NewRpmStageSourceFilesInputs(p.packageSpecs))) pipeline.AddStage(osbuild.NewBuildstampStage(&osbuild.BuildstampStageOptions{ Arch: p.platform.GetArch().String(), Product: p.InstallerCustomizations.Product, @@ -288,7 +341,11 @@ func (p *AnacondaInstaller) payloadStages() ([]*osbuild.Stage, error) { installUID := 0 installGID := 0 - installHome := "/root" + // bootc systems needs to be able to override this to /var/roothome + installHome := p.InstallerHome + if installHome == "" { + installHome = "/root" + } installShell := "/usr/libexec/anaconda/run-anaconda" installPassword := "" installUser := osbuild.UsersStageOptionsUser{ @@ -463,6 +520,12 @@ func (p *AnacondaInstaller) dracutStageOptions() (*osbuild.DracutStageOptions, e Extra: []string{"--xz"}, AddDrivers: p.InstallerCustomizations.AdditionalDrivers, } + if p.InitramfsPath != "" { + // dracut will by default write to /boot/initrmfs-$ver + // so we need to override if we have explicit paths + options.Extra = append(options.Extra, p.InitramfsPath) + } + options.AddModules = append(options.AddModules, p.InstallerCustomizations.AdditionalDracutModules...) if p.Biosdevname { diff --git a/pkg/manifest/anaconda_installer_iso_tree.go b/pkg/manifest/anaconda_installer_iso_tree.go index ce605da2a9..da8b7f9fe7 100644 --- a/pkg/manifest/anaconda_installer_iso_tree.go +++ b/pkg/manifest/anaconda_installer_iso_tree.go @@ -195,6 +195,10 @@ func (p *AnacondaInstallerISOTree) getInline() []string { return inlineData } func (p *AnacondaInstallerISOTree) getBuildPackages(_ Distro) ([]string, error) { + if p.anacondaPipeline.BootcLivefsContainer != nil { + return nil, nil + } + var packages []string switch p.RootfsType { case SquashfsExt4Rootfs, SquashfsRootfs: @@ -419,15 +423,23 @@ func (p *AnacondaInstallerISOTree) serialize() (osbuild.Pipeline, error) { })) } + // XXX: too indirect, ugly + if p.anacondaPipeline.KernelPath == "" { + p.anacondaPipeline.KernelPath = fmt.Sprintf("boot/vmlinuz-%s", p.anacondaPipeline.kernelVer) + } + if p.anacondaPipeline.InitramfsPath == "" { + p.anacondaPipeline.InitramfsPath = fmt.Sprintf("boot/initramfs-%s.img", p.anacondaPipeline.kernelVer) + } + inputName := "tree" copyStageOptions := &osbuild.CopyStageOptions{ Paths: []osbuild.CopyStagePath{ { - From: fmt.Sprintf("input://%s/boot/vmlinuz-%s", inputName, p.anacondaPipeline.kernelVer), + From: fmt.Sprintf("input://%s/%s", inputName, p.anacondaPipeline.KernelPath), To: "tree:///images/pxeboot/vmlinuz", }, { - From: fmt.Sprintf("input://%s/boot/initramfs-%s.img", inputName, p.anacondaPipeline.kernelVer), + From: fmt.Sprintf("input://%s/%s", inputName, p.anacondaPipeline.InitramfsPath), To: "tree:///images/pxeboot/initrd.img", }, }, diff --git a/test/bootc-fake-containers.yaml b/test/bootc-fake-containers.yaml index bc211668dc..c406864e87 100644 --- a/test/bootc-fake-containers.yaml +++ b/test/bootc-fake-containers.yaml @@ -1,12 +1,16 @@ --- containers: - &default_fake_container + image_types: ["qcow2", "ami", "raw", "gce", "vhd", "vmdk", "ova"] image_ref: "bootc-fake-imgref" arch: riscv64 info: &default_info os_release: + name: "bootc Test OS" id: "test-os" versionid: "1" + kernel_info: + version: 6.6 default_fs: "ext4" container_size: 10_000_000_000 - <<: *default_fake_container @@ -24,6 +28,7 @@ containers: <<: *default_info uefi_vendor: "uefi-vendor-aarch64" + # test build container - <<: *default_fake_container arch: x86_64 info: @@ -34,3 +39,13 @@ containers: build_container_info: os_release: id: "build-test-os" + + # test ISO installer + - <<: *default_fake_container + image_types: ["bootc-installer"] + arch: x86_64 + payload_container_ref: "payload-container-fake-ref" + - <<: *default_fake_container + image_types: ["bootc-installer"] + arch: aarch64 + payload_container_ref: "payload-container-fake-ref" diff --git a/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_installer-empty b/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_installer-empty new file mode 100644 index 0000000000..7d6a8cefaf --- /dev/null +++ b/test/data/manifest-checksums/bootc_test_os_1-aarch64-bootc_installer-empty @@ -0,0 +1 @@ +0e5ba8dd30f80a431cf48405a7ff84eab6cc7a96 diff --git a/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_installer-empty b/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_installer-empty new file mode 100644 index 0000000000..5d9468d072 --- /dev/null +++ b/test/data/manifest-checksums/bootc_test_os_1-x86_64-bootc_installer-empty @@ -0,0 +1 @@ +57a85fa7b71308ed92a9d2ba6acac2fef1fa6559