diff --git a/bib/cmd/bootc-image-builder/export_test.go b/bib/cmd/bootc-image-builder/export_test.go index ae62449d2..8dcbaef91 100644 --- a/bib/cmd/bootc-image-builder/export_test.go +++ b/bib/cmd/bootc-image-builder/export_test.go @@ -1,16 +1,10 @@ package main var ( - CanChownInPath = canChownInPath - CheckFilesystemCustomizations = checkFilesystemCustomizations - GetDistroAndRunner = getDistroAndRunner - CheckMountpoints = checkMountpoints - PartitionTables = partitionTables - UpdateFilesystemSizes = updateFilesystemSizes - GenPartitionTable = genPartitionTable - CreateRand = createRand - BuildCobraCmdline = buildCobraCmdline - CalcRequiredDirectorySizes = calcRequiredDirectorySizes + CanChownInPath = canChownInPath + GetDistroAndRunner = getDistroAndRunner + CreateRand = createRand + BuildCobraCmdline = buildCobraCmdline ) func MockOsGetuid(new func() int) (restore func()) { diff --git a/bib/cmd/bootc-image-builder/image.go b/bib/cmd/bootc-image-builder/image.go index 8ea203191..2e6561335 100644 --- a/bib/cmd/bootc-image-builder/image.go +++ b/bib/cmd/bootc-image-builder/image.go @@ -2,7 +2,6 @@ package main import ( cryptorand "crypto/rand" - "errors" "fmt" "math" "math/big" @@ -11,32 +10,17 @@ import ( "strconv" "strings" + "github.com/sirupsen/logrus" + "github.com/osbuild/blueprint/pkg/blueprint" "github.com/osbuild/images/pkg/arch" "github.com/osbuild/images/pkg/bib/osinfo" - "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/customizations/users" - "github.com/osbuild/images/pkg/disk" - "github.com/osbuild/images/pkg/disk/partition" - "github.com/osbuild/images/pkg/image" "github.com/osbuild/images/pkg/manifest" - "github.com/osbuild/images/pkg/osbuild" - "github.com/osbuild/images/pkg/pathpolicy" - "github.com/osbuild/images/pkg/platform" - "github.com/osbuild/images/pkg/policies" - "github.com/osbuild/images/pkg/rpmmd" "github.com/osbuild/images/pkg/runner" - "github.com/sirupsen/logrus" - "github.com/osbuild/bootc-image-builder/bib/internal/distrodef" "github.com/osbuild/bootc-image-builder/bib/internal/imagetypes" ) -// TODO: Auto-detect this from container image metadata -const DEFAULT_SIZE = uint64(10 * GibiByte) - type ManifestConfig struct { // OCI image path (without the transport, that is always docker://) Imgref string @@ -50,10 +34,6 @@ type ManifestConfig struct { // CPU architecture of the image Architecture arch.Arch - // The minimum size required for the root fs in order to fit the container - // contents - RootfsMinsize uint64 - // Paths to the directory with the distro definitions DistroDefPaths []string @@ -68,383 +48,6 @@ type ManifestConfig struct { UseLibrepo bool } -func Manifest(c *ManifestConfig) (*manifest.Manifest, error) { - rng := createRand() - - if c.ImageTypes.BuildsISO() { - return manifestForISO(c, rng) - } - return manifestForDiskImage(c, rng) -} - -var ( - // The mountpoint policy for bootc images is more restrictive than the - // ostree mountpoint policy defined in osbuild/images. It only allows / - // (for sizing the root partition) and custom mountpoints under /var but - // not /var itself. - - // Since our policy library doesn't support denying a path while allowing - // its subpaths (only the opposite), we augment the standard policy check - // with a simple search through the custom mountpoints to deny /var - // specifically. - mountpointPolicy = pathpolicy.NewPathPolicies(map[string]pathpolicy.PathPolicy{ - // allow all existing mountpoints (but no subdirs) to support size customizations - "/": {Deny: false, Exact: true}, - "/boot": {Deny: false, Exact: true}, - - // /var is not allowed, but we need to allow any subdirectories that - // are not denied below, so we allow it initially and then check it - // separately (in checkMountpoints()) - "/var": {Deny: false}, - - // /var subdir denials - "/var/home": {Deny: true}, - "/var/lock": {Deny: true}, // symlink to ../run/lock which is on tmpfs - "/var/mail": {Deny: true}, // symlink to spool/mail - "/var/mnt": {Deny: true}, - "/var/roothome": {Deny: true}, - "/var/run": {Deny: true}, // symlink to ../run which is on tmpfs - "/var/srv": {Deny: true}, - "/var/usrlocal": {Deny: true}, - }) - - mountpointMinimalPolicy = pathpolicy.NewPathPolicies(map[string]pathpolicy.PathPolicy{ - // allow all existing mountpoints to support size customizations - "/": {Deny: false, Exact: true}, - "/boot": {Deny: false, Exact: true}, - }) -) - -func checkMountpoints(filesystems []blueprint.FilesystemCustomization, policy *pathpolicy.PathPolicies) error { - errs := []error{} - for _, fs := range filesystems { - if err := policy.Check(fs.Mountpoint); err != nil { - errs = append(errs, err) - } - if fs.Mountpoint == "/var" { - // this error message is consistent with the errors returned by policy.Check() - // TODO: remove trailing space inside the quoted path when the function is fixed in osbuild/images. - errs = append(errs, fmt.Errorf(`path "/var" is not allowed`)) - } - } - if len(errs) > 0 { - return fmt.Errorf("the following errors occurred while validating custom mountpoints:\n%w", errors.Join(errs...)) - } - return nil -} - -func checkFilesystemCustomizations(fsCustomizations []blueprint.FilesystemCustomization, ptmode partition.PartitioningMode) error { - var policy *pathpolicy.PathPolicies - switch ptmode { - case partition.BtrfsPartitioningMode: - // btrfs subvolumes are not supported at build time yet, so we only - // allow / and /boot to be customized when building a btrfs disk (the - // minimal policy) - policy = mountpointMinimalPolicy - default: - policy = mountpointPolicy - } - if err := checkMountpoints(fsCustomizations, policy); err != nil { - return err - } - return nil -} - -// updateFilesystemSizes updates the size of the root filesystem customization -// based on the minRootSize. The new min size whichever is larger between the -// existing size and the minRootSize. If the root filesystem is not already -// configured, a new customization is added. -func updateFilesystemSizes(fsCustomizations []blueprint.FilesystemCustomization, minRootSize uint64) []blueprint.FilesystemCustomization { - updated := make([]blueprint.FilesystemCustomization, len(fsCustomizations), len(fsCustomizations)+1) - hasRoot := false - for idx, fsc := range fsCustomizations { - updated[idx] = fsc - if updated[idx].Mountpoint == "/" { - updated[idx].MinSize = max(updated[idx].MinSize, minRootSize) - hasRoot = true - } - } - - if !hasRoot { - // no root customization found: add it - updated = append(updated, blueprint.FilesystemCustomization{Mountpoint: "/", MinSize: minRootSize}) - } - return updated -} - -// setFSTypes sets the filesystem types for all mountable entities to match the -// selected rootfs type. -// If rootfs is 'btrfs', the function will keep '/boot' to its default. -func setFSTypes(pt *disk.PartitionTable, rootfs string) error { - if rootfs == "" { - return fmt.Errorf("root filesystem type is empty") - } - - return pt.ForEachMountable(func(mnt disk.Mountable, _ []disk.Entity) error { - switch mnt.GetMountpoint() { - case "/boot/efi": - // never change the efi partition's type - return nil - case "/boot": - // change only if we're not doing btrfs - if rootfs == "btrfs" { - return nil - } - fallthrough - default: - switch elem := mnt.(type) { - case *disk.Filesystem: - elem.Type = rootfs - case *disk.BtrfsSubvolume: - // nothing to do - default: - return fmt.Errorf("the mountable disk entity for %q of the base partition table is not an ordinary filesystem but %T", mnt.GetMountpoint(), mnt) - } - return nil - } - }) -} - -func genPartitionTable(c *ManifestConfig, customizations *blueprint.Customizations, rng *rand.Rand) (*disk.PartitionTable, error) { - fsCust := customizations.GetFilesystems() - diskCust, err := customizations.GetPartitioning() - if err != nil { - return nil, fmt.Errorf("error reading disk customizations: %w", err) - } - - // Embedded disk customization applies if there was no local customization - if fsCust == nil && diskCust == nil && c.SourceInfo != nil && c.SourceInfo.ImageCustomization != nil { - imageCustomizations := c.SourceInfo.ImageCustomization - - fsCust = imageCustomizations.GetFilesystems() - diskCust, err = imageCustomizations.GetPartitioning() - 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: - partitionTable, err = genPartitionTableDiskCust(c, diskCust, rng) - if err != nil { - return nil, err - } - default: - 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 / -// for disk customizations. We need this because with advanced partitioning -// we never grow the rootfs to the size of the disk (unlike the tranditional -// filesystem customizations). -// -// So we need to go over the customizations and ensure the min-size for "/" -// is at least rootfsMinSize. -// -// Note that a custom "/usr" is not supported in image mode so splitting -// rootfsMinSize between / and /usr is not a concern. -func calcRequiredDirectorySizes(distCust *blueprint.DiskCustomization, rootfsMinSize uint64) (map[string]uint64, error) { - // XXX: this has *way* too much low-level knowledge about the - // inner workings of blueprint.DiskCustomizations plus when - // a new type it needs to get added here too, think about - // moving into "images" instead (at least partly) - mounts := map[string]uint64{} - for _, part := range distCust.Partitions { - switch part.Type { - case "", "plain": - mounts[part.Mountpoint] = part.MinSize - case "lvm": - for _, lv := range part.LogicalVolumes { - mounts[lv.Mountpoint] = part.MinSize - } - case "btrfs": - for _, subvol := range part.Subvolumes { - mounts[subvol.Mountpoint] = part.MinSize - } - default: - return nil, fmt.Errorf("unknown disk customization type %q", part.Type) - } - } - // ensure rootfsMinSize is respected - return map[string]uint64{ - "/": max(rootfsMinSize, mounts["/"]), - }, nil -} - -func genPartitionTableDiskCust(c *ManifestConfig, diskCust *blueprint.DiskCustomization, rng *rand.Rand) (*disk.PartitionTable, error) { - if err := diskCust.ValidateLayoutConstraints(); err != nil { - return nil, fmt.Errorf("cannot use disk customization: %w", err) - } - - diskCust.MinSize = max(diskCust.MinSize, c.RootfsMinsize) - - basept, ok := partitionTables[c.Architecture.String()] - if !ok { - return nil, fmt.Errorf("pipelines: no partition tables defined for %s", c.Architecture) - } - defaultFSType, err := disk.NewFSType(c.RootFSType) - if err != nil { - return nil, err - } - requiredMinSizes, err := calcRequiredDirectorySizes(diskCust, c.RootfsMinsize) - if err != nil { - return nil, err - } - partOptions := &disk.CustomPartitionTableOptions{ - PartitionTableType: basept.Type, - // XXX: not setting/defaults will fail to boot with btrfs/lvm - BootMode: platform.BOOT_HYBRID, - DefaultFSType: defaultFSType, - RequiredMinSizes: requiredMinSizes, - Architecture: c.Architecture, - } - return disk.NewCustomPartitionTable(diskCust, partOptions, rng) -} - -func genPartitionTableFsCust(c *ManifestConfig, fsCust []blueprint.FilesystemCustomization, rng *rand.Rand) (*disk.PartitionTable, error) { - basept, ok := partitionTables[c.Architecture.String()] - if !ok { - return nil, fmt.Errorf("pipelines: no partition tables defined for %s", c.Architecture) - } - - partitioningMode := partition.RawPartitioningMode - if c.RootFSType == "btrfs" { - partitioningMode = partition.BtrfsPartitioningMode - } - if err := checkFilesystemCustomizations(fsCust, partitioningMode); err != nil { - return nil, err - } - fsCustomizations := updateFilesystemSizes(fsCust, c.RootfsMinsize) - - pt, err := disk.NewPartitionTable(&basept, fsCustomizations, DEFAULT_SIZE, partitioningMode, c.Architecture, nil, rng) - if err != nil { - return nil, err - } - - if err := setFSTypes(pt, c.RootFSType); err != nil { - return nil, fmt.Errorf("error setting root filesystem type: %w", err) - } - return pt, nil -} - -func manifestForDiskImage(c *ManifestConfig, rng *rand.Rand) (*manifest.Manifest, error) { - if c.Imgref == "" { - return nil, fmt.Errorf("pipeline: no base image defined") - } - containerSource := container.SourceSpec{ - Source: c.Imgref, - 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, buildContainerSource) - img.OSCustomizations.Users = users.UsersFromBP(customizations.GetUsers()) - img.OSCustomizations.Groups = users.GroupsFromBP(customizations.GetGroups()) - img.OSCustomizations.SELinux = c.SourceInfo.SELinuxPolicy - img.OSCustomizations.BuildSELinux = img.OSCustomizations.SELinux - if c.BuildSourceInfo != nil { - img.OSCustomizations.BuildSELinux = c.BuildSourceInfo.SELinuxPolicy - } - - img.OSCustomizations.KernelOptionsAppend = []string{ - "rw", - // TODO: Drop this as we expect kargs to come from the container image, - // xref https://github.com/CentOS/centos-bootc-layered/blob/main/cloud/usr/lib/bootc/install/05-cloud-kargs.toml - "console=tty0", - "console=ttyS0", - } - - img.Platform = &platform.Data{ - Arch: c.Architecture, - UEFIVendor: c.SourceInfo.UEFIVendor, - QCOW2Compat: "1.1", - } - switch c.Architecture { - case arch.ARCH_X86_64: - img.Platform.(*platform.Data).BIOSPlatform = "i386-pc" - case arch.ARCH_PPC64LE: - img.Platform.(*platform.Data).BIOSPlatform = "powerpc-ieee1275" - case arch.ARCH_S390X: - img.Platform.(*platform.Data).ZiplSupport = true - } - - if kopts := customizations.GetKernel(); kopts != nil && kopts.Append != "" { - img.OSCustomizations.KernelOptionsAppend = append(img.OSCustomizations.KernelOptionsAppend, kopts.Append) - } - - pt, err := genPartitionTable(c, customizations, rng) - if err != nil { - return nil, err - } - img.PartitionTable = pt - - // Check Directory/File Customizations are valid - dc := customizations.GetDirectories() - fc := customizations.GetFiles() - if err := blueprint.ValidateDirFileCustomizations(dc, fc); err != nil { - return nil, err - } - if err := blueprint.CheckDirectoryCustomizationsPolicy(dc, policies.OstreeCustomDirectoriesPolicies); err != nil { - return nil, err - } - if err := blueprint.CheckFileCustomizationsPolicy(fc, policies.OstreeCustomFilesPolicies); err != nil { - return nil, err - } - img.OSCustomizations.Files, err = blueprint.FileCustomizationsToFsNodeFiles(fc) - if err != nil { - return nil, err - } - img.OSCustomizations.Directories, err = blueprint.DirectoryCustomizationsToFsNodeDirectories(dc) - if err != nil { - return nil, err - } - - // For the bootc-disk image, the filename is the basename and the extension - // is added automatically for each disk format - img.Filename = "disk" - - mf := manifest.New() - mf.Distro = manifest.DISTRO_FEDORA - runner := &runner.Linux{} - - if err := img.InstantiateManifestFromContainers(&mf, []container.SourceSpec{containerSource}, runner, rng); err != nil { - return nil, err - } - - return &mf, nil -} - func labelForISO(os *osinfo.OSRelease, arch *arch.Arch) string { switch os.ID { case "fedora": @@ -467,121 +70,6 @@ func needsRHELLoraxTemplates(si osinfo.OSRelease) bool { return si.ID == "rhel" || slices.Contains(si.IDLike, "rhel") || si.VersionID == "eln" } -func manifestForISO(c *ManifestConfig, rng *rand.Rand) (*manifest.Manifest, error) { - if c.Imgref == "" { - return nil, fmt.Errorf("pipeline: no base image defined") - } - - imageDef, err := distrodef.LoadImageDef(c.DistroDefPaths, c.SourceInfo.OSRelease.ID, c.SourceInfo.OSRelease.VersionID, "anaconda-iso") - if err != nil { - return nil, err - } - - containerSource := container.SourceSpec{ - Source: c.Imgref, - Name: c.Imgref, - Local: true, - } - - // The ref is not needed and will be removed from the ctor later - // in time - img := image.NewAnacondaContainerInstaller(containerSource, "") - img.ContainerRemoveSignatures = true - img.RootfsCompression = "zstd" - - img.Product = c.SourceInfo.OSRelease.Name - img.OSVersion = c.SourceInfo.OSRelease.VersionID - - img.ExtraBasePackages = rpmmd.PackageSet{ - Include: imageDef.Packages, - } - - img.ISOLabel = labelForISO(&c.SourceInfo.OSRelease, &c.Architecture) - - var customizations *blueprint.Customizations - if c.Config != nil { - customizations = c.Config.Customizations - } - img.InstallerCustomizations.FIPS = customizations.GetFIPS() - img.Kickstart, err = kickstart.New(customizations) - if err != nil { - return 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, 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, - ) - - img.Kickstart.OSTree = &kickstart.OSTree{ - OSName: "default", - } - img.InstallerCustomizations.UseRHELLoraxTemplates = needsRHELLoraxTemplates(c.SourceInfo.OSRelease) - - img.Platform = &platform.Data{ - Arch: c.Architecture, - ImageFormat: platform.FORMAT_ISO, - UEFIVendor: c.SourceInfo.UEFIVendor, - } - switch c.Architecture { - case arch.ARCH_X86_64: - img.Platform.(*platform.Data).BIOSPlatform = "i386-pc" - img.InstallerCustomizations.ISOBoot = manifest.Grub2ISOBoot - case arch.ARCH_AARCH64: - // aarch64 always uses UEFI, so let's enforce the vendor - if c.SourceInfo.UEFIVendor == "" { - return nil, fmt.Errorf("UEFI vendor must be set for aarch64 ISO") - } - case arch.ARCH_S390X: - img.Platform.(*platform.Data).ZiplSupport = true - case arch.ARCH_PPC64LE: - img.Platform.(*platform.Data).BIOSPlatform = "powerpc-ieee1275" - case arch.ARCH_RISCV64: - // nothing special needed - default: - return nil, fmt.Errorf("unsupported architecture %v", c.Architecture) - } - // see https://github.com/osbuild/bootc-image-builder/issues/733 - img.InstallerCustomizations.ISORootfsType = manifest.SquashfsRootfs - img.Filename = "install.iso" - - installRootfsType, err := disk.NewFSType(c.RootFSType) - if err != nil { - return nil, err - } - img.InstallRootfsType = installRootfsType - - mf := manifest.New() - - foundDistro, foundRunner, err := getDistroAndRunner(c.SourceInfo.OSRelease) - if err != nil { - return nil, fmt.Errorf("failed to infer distro and runner: %w", err) - } - mf.Distro = foundDistro - - _, err = img.InstantiateManifest(&mf, nil, foundRunner, rng) - return &mf, err -} - func getDistroAndRunner(osRelease osinfo.OSRelease) (manifest.Distro, runner.Runner, error) { switch osRelease.ID { case "fedora": diff --git a/bib/cmd/bootc-image-builder/image_test.go b/bib/cmd/bootc-image-builder/image_test.go index acddc18fe..a204b1811 100644 --- a/bib/cmd/bootc-image-builder/image_test.go +++ b/bib/cmd/bootc-image-builder/image_test.go @@ -7,11 +7,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/osbuild/blueprint/pkg/blueprint" - "github.com/osbuild/images/pkg/arch" - "github.com/osbuild/images/pkg/datasizes" - "github.com/osbuild/images/pkg/disk" - "github.com/osbuild/images/pkg/disk/partition" "github.com/osbuild/images/pkg/manifest" "github.com/osbuild/images/pkg/runner" @@ -61,658 +56,3 @@ func TestGetDistroAndRunner(t *testing.T) { }) } } - -func TestCheckFilesystemCustomizationsValidates(t *testing.T) { - for _, tc := range []struct { - fsCust []blueprint.FilesystemCustomization - ptmode partition.PartitioningMode - expectedErr string - }{ - // happy - { - fsCust: []blueprint.FilesystemCustomization{}, - expectedErr: "", - }, - { - fsCust: []blueprint.FilesystemCustomization{}, - ptmode: partition.BtrfsPartitioningMode, - expectedErr: "", - }, - { - fsCust: []blueprint.FilesystemCustomization{ - {Mountpoint: "/"}, {Mountpoint: "/boot"}, - }, - ptmode: partition.RawPartitioningMode, - expectedErr: "", - }, - { - fsCust: []blueprint.FilesystemCustomization{ - {Mountpoint: "/"}, {Mountpoint: "/boot"}, - }, - ptmode: partition.BtrfsPartitioningMode, - expectedErr: "", - }, - { - fsCust: []blueprint.FilesystemCustomization{ - {Mountpoint: "/"}, - {Mountpoint: "/boot"}, - {Mountpoint: "/var/log"}, - {Mountpoint: "/var/data"}, - }, - expectedErr: "", - }, - // sad - { - fsCust: []blueprint.FilesystemCustomization{ - {Mountpoint: "/"}, - {Mountpoint: "/ostree"}, - }, - ptmode: partition.RawPartitioningMode, - expectedErr: "the following errors occurred while validating custom mountpoints:\npath \"/ostree\" is not allowed", - }, - { - fsCust: []blueprint.FilesystemCustomization{ - {Mountpoint: "/"}, - {Mountpoint: "/var"}, - }, - ptmode: partition.RawPartitioningMode, - expectedErr: "the following errors occurred while validating custom mountpoints:\npath \"/var\" is not allowed", - }, - { - fsCust: []blueprint.FilesystemCustomization{ - {Mountpoint: "/"}, - {Mountpoint: "/var/data"}, - }, - ptmode: partition.BtrfsPartitioningMode, - expectedErr: "the following errors occurred while validating custom mountpoints:\npath \"/var/data\" is not allowed", - }, - { - fsCust: []blueprint.FilesystemCustomization{ - {Mountpoint: "/"}, - {Mountpoint: "/boot/"}, - }, - ptmode: partition.BtrfsPartitioningMode, - expectedErr: "the following errors occurred while validating custom mountpoints:\npath \"/boot/\" must be canonical", - }, - { - fsCust: []blueprint.FilesystemCustomization{ - {Mountpoint: "/"}, - {Mountpoint: "/boot/"}, - {Mountpoint: "/opt"}, - }, - ptmode: partition.BtrfsPartitioningMode, - expectedErr: "the following errors occurred while validating custom mountpoints:\npath \"/boot/\" must be canonical\npath \"/opt\" is not allowed", - }, - } { - if tc.expectedErr == "" { - assert.NoError(t, bib.CheckFilesystemCustomizations(tc.fsCust, tc.ptmode)) - } else { - assert.ErrorContains(t, bib.CheckFilesystemCustomizations(tc.fsCust, tc.ptmode), tc.expectedErr) - } - } -} - -func TestLocalMountpointPolicy(t *testing.T) { - // extended testing of the general mountpoint policy (non-minimal) - type testCase struct { - path string - allowed bool - } - - testCases := []testCase{ - // existing mountpoints / and /boot are fine for sizing - {"/", true}, - {"/boot", true}, - - // root mountpoints are not allowed - {"/data", false}, - {"/opt", false}, - {"/stuff", false}, - {"/usr", false}, - - // /var explicitly is not allowed - {"/var", false}, - - // subdirs of /boot are not allowed - {"/boot/stuff", false}, - {"/boot/loader", false}, - - // /var subdirectories are allowed - {"/var/data", true}, - {"/var/scratch", true}, - {"/var/log", true}, - {"/var/opt", true}, - {"/var/opt/application", true}, - - // but not these - {"/var/home", false}, - {"/var/lock", false}, // symlink to ../run/lock which is on tmpfs - {"/var/mail", false}, // symlink to spool/mail - {"/var/mnt", false}, - {"/var/roothome", false}, - {"/var/run", false}, // symlink to ../run which is on tmpfs - {"/var/srv", false}, - {"/var/usrlocal", false}, - - // nor their subdirs - {"/var/run/subrun", false}, - {"/var/srv/test", false}, - {"/var/home/user", false}, - {"/var/usrlocal/bin", false}, - } - - for _, tc := range testCases { - t.Run(tc.path, func(t *testing.T) { - err := bib.CheckFilesystemCustomizations([]blueprint.FilesystemCustomization{{Mountpoint: tc.path}}, partition.RawPartitioningMode) - if err != nil && tc.allowed { - t.Errorf("expected %s to be allowed, but got error: %v", tc.path, err) - } else if err == nil && !tc.allowed { - t.Errorf("expected %s to be denied, but got no error", tc.path) - } - }) - } -} - -func TestBasePartitionTablesHaveRoot(t *testing.T) { - // make sure that all base partition tables have at least a root partition defined - for arch, pt := range bib.PartitionTables { - rootMountable := pt.FindMountable("/") - if rootMountable == nil { - t.Errorf("partition table %q does not define a root filesystem", arch) - } - _, isFS := rootMountable.(*disk.Filesystem) - if !isFS { - t.Errorf("root mountable for %q is not an ordinary filesystem", arch) - } - } - -} - -func TestUpdateFilesystemSizes(t *testing.T) { - type testCase struct { - customizations []blueprint.FilesystemCustomization - minRootSize uint64 - expected []blueprint.FilesystemCustomization - } - - testCases := map[string]testCase{ - "simple": { - customizations: nil, - minRootSize: 999, - expected: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/", - MinSize: 999, - }, - }, - }, - "container-is-larger": { - customizations: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/", - MinSize: 10, - }, - }, - minRootSize: 999, - expected: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/", - MinSize: 999, - }, - }, - }, - "container-is-smaller": { - customizations: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/", - MinSize: 1000, - }, - }, - minRootSize: 892, - expected: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/", - MinSize: 1000, - }, - }, - }, - "customizations-noroot": { - customizations: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/var/data", - MinSize: 1_000_000, - }, - }, - minRootSize: 9000, - expected: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/var/data", - MinSize: 1_000_000, - }, - { - Mountpoint: "/", - MinSize: 9000, - }, - }, - }, - "customizations-withroot-smallcontainer": { - customizations: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/var/data", - MinSize: 1_000_000, - }, - { - Mountpoint: "/", - MinSize: 2_000_000, - }, - }, - minRootSize: 9000, - expected: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/var/data", - MinSize: 1_000_000, - }, - { - Mountpoint: "/", - MinSize: 2_000_000, - }, - }, - }, - "customizations-withroot-largecontainer": { - customizations: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/var/data", - MinSize: 1_000_000, - }, - { - Mountpoint: "/", - MinSize: 2_000_000, - }, - }, - minRootSize: 9_000_000, - expected: []blueprint.FilesystemCustomization{ - { - Mountpoint: "/var/data", - MinSize: 1_000_000, - }, - { - Mountpoint: "/", - MinSize: 9_000_000, - }, - }, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - assert.ElementsMatch(t, bib.UpdateFilesystemSizes(tc.customizations, tc.minRootSize), tc.expected) - }) - } - -} - -func findMountableSizeableFor(pt *disk.PartitionTable, needle string) (disk.Mountable, disk.Sizeable) { - var foundMnt disk.Mountable - var foundParent disk.Sizeable - err := pt.ForEachMountable(func(mnt disk.Mountable, path []disk.Entity) error { - if mnt.GetMountpoint() == needle { - foundMnt = mnt - for idx := len(path) - 1; idx >= 0; idx-- { - if sz, ok := path[idx].(disk.Sizeable); ok { - foundParent = sz - break - } - } - } - return nil - }) - if err != nil { - panic(err) - } - return foundMnt, foundParent -} - -func TestGenPartitionTableSetsRootfsForAllFilesystemsXFS(t *testing.T) { - rng := bib.CreateRand() - - cnf := &bib.ManifestConfig{ - Architecture: arch.ARCH_X86_64, - RootFSType: "xfs", - } - cus := &blueprint.Customizations{ - Filesystem: []blueprint.FilesystemCustomization{ - {Mountpoint: "/var/data", MinSize: 2_000_000}, - {Mountpoint: "/var/stuff", MinSize: 10_000_000}, - }, - } - pt, err := bib.GenPartitionTable(cnf, cus, rng) - assert.NoError(t, err) - - for _, mntPoint := range []string{"/", "/boot", "/var/data"} { - mnt, _ := findMountableSizeableFor(pt, mntPoint) - assert.Equal(t, "xfs", mnt.GetFSType()) - } - _, parent := findMountableSizeableFor(pt, "/var/data") - assert.True(t, parent.GetSize() >= 2_000_000) - - _, parent = findMountableSizeableFor(pt, "/var/stuff") - assert.True(t, parent.GetSize() >= 10_000_000) - - // ESP is always vfat - mnt, _ := findMountableSizeableFor(pt, "/boot/efi") - assert.Equal(t, "vfat", mnt.GetFSType()) -} - -func TestGenPartitionTableSetsRootfsForAllFilesystemsBtrfs(t *testing.T) { - rng := bib.CreateRand() - - cnf := &bib.ManifestConfig{ - Architecture: arch.ARCH_X86_64, - RootFSType: "btrfs", - } - cus := &blueprint.Customizations{} - pt, err := bib.GenPartitionTable(cnf, cus, rng) - assert.NoError(t, err) - - mnt, _ := findMountableSizeableFor(pt, "/") - assert.Equal(t, "btrfs", mnt.GetFSType()) - - // btrfs has a default (ext4) /boot - mnt, _ = findMountableSizeableFor(pt, "/boot") - assert.Equal(t, "ext4", mnt.GetFSType()) - - // ESP is always vfat - mnt, _ = findMountableSizeableFor(pt, "/boot/efi") - assert.Equal(t, "vfat", mnt.GetFSType()) -} - -func TestGenPartitionTableDiskCustomizationRunsValidateLayoutConstraints(t *testing.T) { - rng := bib.CreateRand() - - cnf := &bib.ManifestConfig{ - Architecture: arch.ARCH_X86_64, - RootFSType: "xfs", - } - cus := &blueprint.Customizations{ - Disk: &blueprint.DiskCustomization{ - Partitions: []blueprint.PartitionCustomization{ - { - Type: "lvm", - VGCustomization: blueprint.VGCustomization{}, - }, - { - Type: "lvm", - VGCustomization: blueprint.VGCustomization{}, - }, - }, - }, - } - _, err := bib.GenPartitionTable(cnf, cus, rng) - assert.EqualError(t, err, "cannot use disk customization: multiple LVM volume groups are not yet supported") -} - -func TestGenPartitionTableDiskCustomizationUnknownTypesError(t *testing.T) { - cus := &blueprint.Customizations{ - Disk: &blueprint.DiskCustomization{ - Partitions: []blueprint.PartitionCustomization{ - { - Type: "rando", - }, - }, - }, - } - _, err := bib.CalcRequiredDirectorySizes(cus.Disk, 5*datasizes.GiB) - assert.EqualError(t, err, `unknown disk customization type "rando"`) -} - -func TestGenPartitionTableDiskCustomizationSizes(t *testing.T) { - rng := bib.CreateRand() - - for _, tc := range []struct { - name string - rootfsMinSize uint64 - partitions []blueprint.PartitionCustomization - expectedMinRootSize uint64 - }{ - { - "empty disk customizaton, root expands to rootfsMinsize", - 2 * datasizes.GiB, - nil, - 2 * datasizes.GiB, - }, - // plain - { - "plain, no root minsize, expands to rootfsMinSize", - 5 * datasizes.GiB, - []blueprint.PartitionCustomization{ - { - MinSize: 10 * datasizes.GiB, - FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ - Mountpoint: "/var", - FSType: "xfs", - }, - }, - }, - 5 * datasizes.GiB, - }, - { - "plain, small root minsize, expands to rootfsMnSize", - 5 * datasizes.GiB, - []blueprint.PartitionCustomization{ - { - MinSize: 1 * datasizes.GiB, - FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ - Mountpoint: "/", - FSType: "xfs", - }, - }, - }, - 5 * datasizes.GiB, - }, - { - "plain, big root minsize", - 5 * datasizes.GiB, - []blueprint.PartitionCustomization{ - { - MinSize: 10 * datasizes.GiB, - FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ - Mountpoint: "/", - FSType: "xfs", - }, - }, - }, - 10 * datasizes.GiB, - }, - // btrfs - { - "btrfs, no root minsize, expands to rootfsMinSize", - 5 * datasizes.GiB, - []blueprint.PartitionCustomization{ - { - Type: "btrfs", - MinSize: 10 * datasizes.GiB, - BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ - Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ - { - Mountpoint: "/var", - Name: "varvol", - }, - }, - }, - }, - }, - 5 * datasizes.GiB, - }, - { - "btrfs, small root minsize, expands to rootfsMnSize", - 5 * datasizes.GiB, - []blueprint.PartitionCustomization{ - { - Type: "btrfs", - MinSize: 1 * datasizes.GiB, - BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ - Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ - { - Mountpoint: "/", - Name: "rootvol", - }, - }, - }, - }, - }, - 5 * datasizes.GiB, - }, - { - "btrfs, big root minsize", - 5 * datasizes.GiB, - []blueprint.PartitionCustomization{ - { - Type: "btrfs", - MinSize: 10 * datasizes.GiB, - BtrfsVolumeCustomization: blueprint.BtrfsVolumeCustomization{ - Subvolumes: []blueprint.BtrfsSubvolumeCustomization{ - { - Mountpoint: "/", - Name: "rootvol", - }, - }, - }, - }, - }, - 10 * datasizes.GiB, - }, - // lvm - { - "lvm, no root minsize, expands to rootfsMinSize", - 5 * datasizes.GiB, - []blueprint.PartitionCustomization{ - { - Type: "lvm", - MinSize: 10 * datasizes.GiB, - VGCustomization: blueprint.VGCustomization{ - LogicalVolumes: []blueprint.LVCustomization{ - { - MinSize: 10 * datasizes.GiB, - FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ - Mountpoint: "/var", - FSType: "xfs", - }, - }, - }, - }, - }, - }, - 5 * datasizes.GiB, - }, - { - "lvm, small root minsize, expands to rootfsMnSize", - 5 * datasizes.GiB, - []blueprint.PartitionCustomization{ - { - Type: "lvm", - MinSize: 1 * datasizes.GiB, - VGCustomization: blueprint.VGCustomization{ - LogicalVolumes: []blueprint.LVCustomization{ - { - MinSize: 1 * datasizes.GiB, - FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ - Mountpoint: "/", - FSType: "xfs", - }, - }, - }, - }, - }, - }, - 5 * datasizes.GiB, - }, - { - "lvm, big root minsize", - 5 * datasizes.GiB, - []blueprint.PartitionCustomization{ - { - Type: "lvm", - MinSize: 10 * datasizes.GiB, - VGCustomization: blueprint.VGCustomization{ - LogicalVolumes: []blueprint.LVCustomization{ - { - MinSize: 10 * datasizes.GiB, - FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{ - Mountpoint: "/", - FSType: "xfs", - }, - }, - }, - }, - }, - }, - 10 * datasizes.GiB, - }, - } { - t.Run(tc.name, func(t *testing.T) { - cnf := &bib.ManifestConfig{ - Architecture: arch.ARCH_X86_64, - RootFSType: "xfs", - RootfsMinsize: tc.rootfsMinSize, - } - cus := &blueprint.Customizations{ - Disk: &blueprint.DiskCustomization{ - Partitions: tc.partitions, - }, - } - pt, err := bib.GenPartitionTable(cnf, cus, rng) - assert.NoError(t, err) - - var rootSize uint64 - err = pt.ForEachMountable(func(mnt disk.Mountable, path []disk.Entity) error { - if mnt.GetMountpoint() == "/" { - for idx := len(path) - 1; idx >= 0; idx-- { - if parent, ok := path[idx].(disk.Sizeable); ok { - rootSize = parent.GetSize() - break - } - } - } - return nil - }) - assert.NoError(t, err) - // expected size is within a reasonable limit - assert.True(t, rootSize >= tc.expectedMinRootSize && rootSize < tc.expectedMinRootSize+5*datasizes.MiB) - }) - } -} - -func TestManifestFilecustomizationsSad(t *testing.T) { - config := getBaseConfig() - config.ImageTypes = []string{"qcow2"} - config.Config = &blueprint.Blueprint{ - Customizations: &blueprint.Customizations{ - Files: []blueprint.FileCustomization{ - { - Path: "/not/allowed", - Data: "some-data", - }, - }, - }, - } - - _, err := bib.Manifest(config) - assert.EqualError(t, err, `the following custom files are not allowed: ["/not/allowed"]`) -} - -func TestManifestDirCustomizationsSad(t *testing.T) { - config := getBaseConfig() - config.ImageTypes = []string{"qcow2"} - config.Config = &blueprint.Blueprint{ - Customizations: &blueprint.Customizations{ - Directories: []blueprint.DirectoryCustomization{ - { - Path: "/dir/not/allowed", - }, - }, - }, - } - - _, err := bib.Manifest(config) - assert.EqualError(t, err, `the following custom directories are not allowed: ["/dir/not/allowed"]`) -} diff --git a/bib/cmd/bootc-image-builder/main.go b/bib/cmd/bootc-image-builder/main.go index db1ee508d..8a8f59de8 100644 --- a/bib/cmd/bootc-image-builder/main.go +++ b/bib/cmd/bootc-image-builder/main.go @@ -1,10 +1,12 @@ package main import ( + "bytes" "encoding/json" "errors" "fmt" "io" + "io/fs" "log" "os" "os/exec" @@ -18,32 +20,22 @@ import ( "github.com/spf13/pflag" "golang.org/x/exp/slices" + repos "github.com/osbuild/images/data/repositories" "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/distro/bootc" "github.com/osbuild/images/pkg/experimentalflags" "github.com/osbuild/images/pkg/manifest" - "github.com/osbuild/images/pkg/osbuild" + "github.com/osbuild/images/pkg/manifestgen" + "github.com/osbuild/images/pkg/reporegistry" "github.com/osbuild/images/pkg/rpmmd" "github.com/osbuild/bootc-image-builder/bib/internal/imagetypes" - 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 @@ -96,77 +88,6 @@ func inContainerOrUnknown() bool { 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 *dnfjson.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 - depsolvedSets := make(map[string]dnfjson.DepsolveResult) - depsolvedRepos := make(map[string][]rpmmd.RepoConfig) - for name, pkgSet := range mani.GetPackageSetChains() { - res, err := solver.Depsolve(pkgSet, 0) - if err != nil { - return nil, nil, fmt.Errorf("cannot depsolve: %w", err) - } - depsolvedSets[name] = *res - depsolvedRepos[name] = res.Repos - } - - // 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 - if c.UseLibrepo { - opts.RpmDownloader = osbuild.RpmDownloaderLibrepo - } - 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 { @@ -200,11 +121,11 @@ func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.Progress imgref := args[0] userConfigFile, _ := cmd.Flags().GetString("config") imgTypes, _ := cmd.Flags().GetStringArray("type") - rpmCacheRoot, _ := cmd.Flags().GetString("rpmmd") + //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") + //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") { @@ -241,118 +162,79 @@ func manifestFromCobra(cmd *cobra.Command, args []string, pbar progress.Progress 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) - } - }() + pbar.SetPulseMsgf("Manifest generation step") + pbar.Start() - 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`) - } - } + // For now shortcut here and build ding "images" for anything + // that is not the iso - // Gather some data from the containers distro - sourceinfo, err := osinfo.Load(container.Root()) + distro, err := bootc.NewBootcDistro(imgref) 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 + if err := distro.SetBuildContainer(buildImgref); err != nil { + return nil, nil, err } - - // 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 := buildContainer.InitDNF(); err != nil { + if err := distro.SetDefaultFs(rootFs); err != nil { return nil, nil, err } - solver, err := buildContainer.NewContainerSolver(rpmCacheRoot, cntArch, sourceinfo) + // XXX: consider target-arch + archi, err := distro.GetArch(cntArch.String()) if err != nil { return nil, nil, 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, + // XXX: how to generate for all image types + imgType, err := archi.GetImageType(imgTypes[0]) + if err != nil { + return nil, nil, err } - manifest, repos, err := makeManifest(manifestConfig, solver, rpmCacheRoot) + var buf bytes.Buffer + repos, err := reporegistry.New(nil, []fs.FS{repos.FS}) if err != nil { return nil, nil, err } - - mTLS, err := extractTLSKeys(repos) + mg, err := manifestgen.New(repos, &manifestgen.Options{ + Output: &buf, + // XXX: hack to skip repo loading for the bootc image. + // We need to add a SkipRepositories or similar to + // manifestgen instead to make this clean + OverrideRepos: []rpmmd.RepoConfig{ + { + BaseURLs: []string{"https://example.com/not-used"}, + }, + }, + }) if err != nil { return nil, nil, err } + if err := mg.Generate(config, distro, imgType, archi, nil); err != nil { + return nil, nil, err + } + + return buf.Bytes(), nil, nil + // XXX: portme + /* + osbuildExtraEnv := mg.OsbuildExtraEnv + + mTLS, err := extractTLSKeys(repos) + if err != nil { + return nil, nil, err + } + + return manifest, mTLS, nil + */ - return manifest, mTLS, nil + return buf.Bytes(), nil, nil } func cmdManifest(cmd *cobra.Command, args []string) error { diff --git a/bib/cmd/bootc-image-builder/main_test.go b/bib/cmd/bootc-image-builder/main_test.go index 21bbd940c..9ee0b9333 100644 --- a/bib/cmd/bootc-image-builder/main_test.go +++ b/bib/cmd/bootc-image-builder/main_test.go @@ -1,8 +1,6 @@ package main_test import ( - "encoding/json" - "errors" "fmt" "os" "strings" @@ -14,16 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/osbuild/blueprint/pkg/blueprint" - "github.com/osbuild/images/pkg/arch" - "github.com/osbuild/images/pkg/bib/osinfo" - "github.com/osbuild/images/pkg/container" - "github.com/osbuild/images/pkg/dnfjson" - "github.com/osbuild/images/pkg/manifest" - "github.com/osbuild/images/pkg/rpmmd" - main "github.com/osbuild/bootc-image-builder/bib/cmd/bootc-image-builder" - "github.com/osbuild/bootc-image-builder/bib/internal/imagetypes" ) func TestCanChownInPathHappy(t *testing.T) { @@ -60,467 +49,6 @@ func TestCanChownInPathCannotChange(t *testing.T) { assert.Equal(t, canChown, false) } -type manifestTestCase struct { - config *main.ManifestConfig - imageTypes imagetypes.ImageTypes - depsolved map[string]dnfjson.DepsolveResult - containers map[string][]container.Spec - expStages map[string][]string - notExpectedStages map[string][]string - err interface{} -} - -func getBaseConfig() *main.ManifestConfig { - return &main.ManifestConfig{ - Architecture: arch.ARCH_X86_64, - Imgref: "testempty", - SourceInfo: &osinfo.Info{ - OSRelease: osinfo.OSRelease{ - ID: "fedora", - VersionID: "40", - Name: "Fedora Linux", - PlatformID: "platform:f40", - }, - UEFIVendor: "fedora", - }, - - // We need the real path here, because we are creating real manifests - DistroDefPaths: []string{"../../data/defs"}, - - // RootFSType is required to create a Manifest - RootFSType: "ext4", - } -} - -func getUserConfig() *main.ManifestConfig { - // add a user - pass := "super-secret-password-42" - key := "ssh-ed25519 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - return &main.ManifestConfig{ - Architecture: arch.ARCH_X86_64, - Imgref: "testuser", - Config: &blueprint.Blueprint{ - Customizations: &blueprint.Customizations{ - User: []blueprint.UserCustomization{ - { - Name: "tester", - Password: &pass, - Key: &key, - }, - }, - }, - }, - SourceInfo: &osinfo.Info{ - OSRelease: osinfo.OSRelease{ - ID: "fedora", - VersionID: "40", - Name: "Fedora Linux", - PlatformID: "platform:f40", - }, - UEFIVendor: "fedora", - }, - - // We need the real path here, because we are creating real manifests - DistroDefPaths: []string{"../../data/defs"}, - - // RootFSType is required to create a Manifest - RootFSType: "ext4", - } -} - -func TestManifestGenerationEmptyConfig(t *testing.T) { - baseConfig := getBaseConfig() - testCases := map[string]manifestTestCase{ - "ami-base": { - config: baseConfig, - imageTypes: []string{"ami"}, - }, - "raw-base": { - config: baseConfig, - imageTypes: []string{"raw"}, - }, - "qcow2-base": { - config: baseConfig, - imageTypes: []string{"qcow2"}, - }, - "iso-base": { - config: baseConfig, - imageTypes: []string{"iso"}, - }, - "empty-config": { - config: &main.ManifestConfig{}, - imageTypes: []string{"qcow2"}, - err: errors.New("pipeline: no base image defined"), - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - config := main.ManifestConfig(*tc.config) - config.ImageTypes = tc.imageTypes - _, err := main.Manifest(&config) - assert.Equal(t, err, tc.err) - }) - } -} - -func TestManifestGenerationUserConfig(t *testing.T) { - userConfig := getUserConfig() - testCases := map[string]manifestTestCase{ - "ami-user": { - config: userConfig, - imageTypes: []string{"ami"}, - }, - "raw-user": { - config: userConfig, - imageTypes: []string{"raw"}, - }, - "qcow2-user": { - config: userConfig, - imageTypes: []string{"qcow2"}, - }, - "iso-user": { - config: userConfig, - imageTypes: []string{"iso"}, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - config := main.ManifestConfig(*tc.config) - config.ImageTypes = tc.imageTypes - _, err := main.Manifest(&config) - assert.NoError(t, err) - }) - } -} - -// Disk images require a container for the build/image pipelines -var containerSpec = container.Spec{ - Source: "test-container", - Digest: "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", - ImageID: "sha256:1111111111111111111111111111111111111111111111111111111111111111", -} - -// diskContainers can be passed to Serialize() to get a minimal disk image -var diskContainers = map[string][]container.Spec{ - "build": { - containerSpec, - }, - "image": { - containerSpec, - }, - "target": { - containerSpec, - }, -} - -// TODO: this tests at this layer is not ideal, it has too much knowledge -// over the implementation details of the "images" library and how an -// image.NewBootcDiskImage() works (i.e. what the pipeline names are and -// what key piplines to expect). These details should be tested in "images" -// and here we would just check (somehow) that image.NewBootcDiskImage() -// (or image.NewAnacondaContainerInstaller()) is called and the right -// customizations are passed. The existing layout makes this hard so this -// is fine for now but would be nice to revisit this. -func TestManifestSerialization(t *testing.T) { - // Tests that the manifest is generated without error and is serialized - // with expected key stages. - - // ISOs require a container for the bootiso-tree, build packages, and packages for the anaconda-tree (with a kernel). - var isoContainers = map[string][]container.Spec{ - "bootiso-tree": { - containerSpec, - }, - } - isoPackages := map[string]dnfjson.DepsolveResult{ - "build": { - Packages: []rpmmd.PackageSpec{ - { - Name: "package", - Version: "113", - Checksum: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - }, - }, - }, - "anaconda-tree": { - Packages: []rpmmd.PackageSpec{ - { - Name: "kernel", - Version: "10.11", - Checksum: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - }, - { - Name: "package", - Version: "113", - Checksum: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - }, - }, - }, - } - - pkgsNoBuild := map[string]dnfjson.DepsolveResult{ - "anaconda-tree": { - Packages: []rpmmd.PackageSpec{ - - { - Name: "kernel", - Version: "10.11", - Checksum: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - }, - { - Name: "package", - Version: "113", - Checksum: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - }, - }, - }, - } - - baseConfig := getBaseConfig() - userConfig := getUserConfig() - testCases := map[string]manifestTestCase{ - "ami-base": { - config: baseConfig, - imageTypes: []string{"ami"}, - containers: diskContainers, - expStages: map[string][]string{ - "build": {"org.osbuild.container-deploy"}, - "image": { - "org.osbuild.bootc.install-to-filesystem", - }, - }, - notExpectedStages: map[string][]string{ - "build": {"org.osbuild.rpm"}, - "image": { - "org.osbuild.users", - }, - }, - }, - "raw-base": { - config: baseConfig, - imageTypes: []string{"raw"}, - containers: diskContainers, - expStages: map[string][]string{ - "build": {"org.osbuild.container-deploy"}, - "image": { - "org.osbuild.bootc.install-to-filesystem", - }, - }, - notExpectedStages: map[string][]string{ - "build": {"org.osbuild.rpm"}, - "image": { - "org.osbuild.users", - }, - }, - }, - "qcow2-base": { - config: baseConfig, - imageTypes: []string{"qcow2"}, - containers: diskContainers, - expStages: map[string][]string{ - "build": {"org.osbuild.container-deploy"}, - "image": { - "org.osbuild.bootc.install-to-filesystem", - }, - }, - notExpectedStages: map[string][]string{ - "build": {"org.osbuild.rpm"}, - "image": { - "org.osbuild.users", - }, - }, - }, - "ami-user": { - config: userConfig, - imageTypes: []string{"ami"}, - containers: diskContainers, - expStages: map[string][]string{ - "build": {"org.osbuild.container-deploy"}, - "image": { - "org.osbuild.users", - "org.osbuild.bootc.install-to-filesystem", - }, - }, - notExpectedStages: map[string][]string{ - "build": {"org.osbuild.rpm"}, - }, - }, - "raw-user": { - config: userConfig, - imageTypes: []string{"raw"}, - containers: diskContainers, - expStages: map[string][]string{ - "build": {"org.osbuild.container-deploy"}, - "image": { - "org.osbuild.users", // user creation stage when we add users - "org.osbuild.bootc.install-to-filesystem", - }, - }, - notExpectedStages: map[string][]string{ - "build": {"org.osbuild.rpm"}, - }, - }, - "qcow2-user": { - config: userConfig, - imageTypes: []string{"qcow2"}, - containers: diskContainers, - expStages: map[string][]string{ - "build": {"org.osbuild.container-deploy"}, - "image": { - "org.osbuild.users", // user creation stage when we add users - "org.osbuild.bootc.install-to-filesystem", - }, - }, - notExpectedStages: map[string][]string{ - "build": {"org.osbuild.rpm"}, - }, - }, - "iso-user": { - config: userConfig, - imageTypes: []string{"iso"}, - containers: isoContainers, - depsolved: isoPackages, - expStages: map[string][]string{ - "build": {"org.osbuild.rpm"}, - "bootiso-tree": {"org.osbuild.skopeo"}, // adds the container to the ISO tree - }, - }, - "iso-nobuildpkg": { - config: userConfig, - imageTypes: []string{"iso"}, - containers: isoContainers, - depsolved: pkgsNoBuild, - err: "serialization not started", - }, - "iso-nocontainer": { - config: userConfig, - imageTypes: []string{"iso"}, - depsolved: isoPackages, - err: "missing ostree, container, or ospipeline parameters in ISO tree pipeline", - }, - "ami-nocontainer": { - config: userConfig, - imageTypes: []string{"ami"}, - // errors come from BuildrootFromContainer() - // TODO: think about better error and testing here (not the ideal layer or err msg) - err: "serialization not started", - }, - "raw-nocontainer": { - config: userConfig, - imageTypes: []string{"raw"}, - // errors come from BuildrootFromContainer() - // TODO: think about better error and testing here (not the ideal layer or err msg) - err: "serialization not started", - }, - "qcow2-nocontainer": { - config: userConfig, - imageTypes: []string{"qcow2"}, - // errors come from BuildrootFromContainer() - // TODO: think about better error and testing here (not the ideal layer or err msg) - err: "serialization not started", - }, - } - - // Use an empty config: only the imgref is required - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - config := main.ManifestConfig(*tc.config) - config.ImageTypes = tc.imageTypes - mf, err := main.Manifest(&config) - assert.NoError(err) // this isn't the error we're testing for - - if tc.err != nil { - assert.PanicsWithValue(tc.err, func() { - _, err := mf.Serialize(tc.depsolved, tc.containers, nil, nil) - assert.NoError(err) - }) - } else { - manifestJson, err := mf.Serialize(tc.depsolved, tc.containers, nil, nil) - assert.NoError(err) - assert.NoError(checkStages(manifestJson, tc.expStages, tc.notExpectedStages)) - } - }) - } - - { - // this one panics with a typed error and needs to be tested separately from the above (PanicsWithError()) - t.Run("iso-nopkgs", func(t *testing.T) { - assert := assert.New(t) - config := main.ManifestConfig(*userConfig) - config.ImageTypes, _ = imagetypes.New("iso") - manifest, err := main.Manifest(&config) - assert.NoError(err) // this isn't the error we're testing for - - expError := "package \"kernel\" not found in the PackageSpec list" - assert.PanicsWithError(expError, func() { - _, err := manifest.Serialize(nil, isoContainers, nil, nil) - assert.NoError(err) - }) - }) - } -} - -// simplified representation of a manifest -type testManifest struct { - Pipelines []pipeline `json:"pipelines"` -} -type pipeline struct { - Name string `json:"name"` - Stages []stage `json:"stages"` -} -type stage struct { - Type string `json:"type"` -} - -func checkStages(serialized manifest.OSBuildManifest, pipelineStages map[string][]string, missingStages map[string][]string) error { - mf := &testManifest{} - if err := json.Unmarshal(serialized, mf); err != nil { - return err - } - pipelineMap := map[string]pipeline{} - for _, pl := range mf.Pipelines { - pipelineMap[pl.Name] = pl - } - - for plname, stages := range pipelineStages { - pl, found := pipelineMap[plname] - if !found { - return fmt.Errorf("pipeline %q not found", plname) - } - - stageMap := map[string]bool{} - for _, stage := range pl.Stages { - stageMap[stage.Type] = true - } - for _, stage := range stages { - if _, found := stageMap[stage]; !found { - return fmt.Errorf("pipeline %q - stage %q - not found", plname, stage) - } - } - } - - for plname, stages := range missingStages { - pl, found := pipelineMap[plname] - if !found { - return fmt.Errorf("pipeline %q not found", plname) - } - - stageMap := map[string]bool{} - for _, stage := range pl.Stages { - stageMap[stage.Type] = true - } - for _, stage := range stages { - if _, found := stageMap[stage]; found { - return fmt.Errorf("pipeline %q - stage %q - found (but should not be)", plname, stage) - } - } - } - - return nil -} - func mockOsArgs(new []string) (restore func()) { saved := os.Args os.Args = append([]string{"argv0"}, new...) diff --git a/bib/cmd/bootc-image-builder/partition_tables.go b/bib/cmd/bootc-image-builder/partition_tables.go deleted file mode 100644 index 9ce5468c5..000000000 --- a/bib/cmd/bootc-image-builder/partition_tables.go +++ /dev/null @@ -1,139 +0,0 @@ -package main - -import ( - "github.com/osbuild/images/pkg/arch" - "github.com/osbuild/images/pkg/disk" - "github.com/osbuild/images/pkg/distro" -) - -const ( - MebiByte = 1024 * 1024 // MiB - GibiByte = 1024 * 1024 * 1024 // GiB - // BootOptions defines the mountpoint options for /boot - // See https://github.com/containers/bootc/pull/341 for the rationale for - // using `ro` by default. Briefly it protects against corruption - // by non-ostree aware tools. - BootOptions = "ro" - // And we default to `ro` for the rootfs too, because we assume the input - // container image is using composefs. For more info, see - // https://github.com/containers/bootc/pull/417 and - // https://github.com/ostreedev/ostree/issues/3193 - RootOptions = "ro" -) - -// diskUuidOfUnknownOrigin is used by default for disk images, -// picked by someone in the past for unknown reasons. More in -// e.g. https://github.com/osbuild/bootc-image-builder/pull/568 and -// https://github.com/osbuild/images/pull/823 -const diskUuidOfUnknownOrigin = "D209C89E-EA5E-4FBD-B161-B461CCE297E0" - -// efiPartition defines the default ESP. See also -// https://en.wikipedia.org/wiki/EFI_system_partition -var efiPartition = disk.Partition{ - Size: 501 * MebiByte, - Type: disk.EFISystemPartitionGUID, - UUID: disk.EFISystemPartitionUUID, - Payload: &disk.Filesystem{ - Type: "vfat", - UUID: disk.EFIFilesystemUUID, - Mountpoint: "/boot/efi", - Label: "EFI-SYSTEM", - FSTabOptions: "umask=0077,shortname=winnt", - FSTabFreq: 0, - FSTabPassNo: 2, - }, -} - -// bootPartition defines a distinct filesystem for /boot -// which is needed for e.g. LVM or LUKS when using GRUB -// (which this project doesn't support today...) -// See also https://github.com/containers/bootc/pull/529/commits/e5548d8765079171e6ed39a3ab0479bc8681a1c9 -var bootPartition = disk.Partition{ - Size: 1 * GibiByte, - Type: disk.FilesystemDataGUID, - UUID: disk.DataPartitionUUID, - Payload: &disk.Filesystem{ - Type: "ext4", - Mountpoint: "/boot", - Label: "boot", - FSTabOptions: BootOptions, - FSTabFreq: 1, - FSTabPassNo: 2, - }, -} - -// rootPartition holds the root filesystem; however note -// that while the type here defines "ext4" because the data -// type requires something there, in practice we pull -// the rootfs type from the container image by default. -// See https://containers.github.io/bootc/bootc-install.html -var rootPartition = disk.Partition{ - Size: 2 * GibiByte, - Type: disk.FilesystemDataGUID, - UUID: disk.RootPartitionUUID, - Payload: &disk.Filesystem{ - Type: "ext4", - Label: "root", - Mountpoint: "/", - FSTabOptions: RootOptions, - FSTabFreq: 1, - FSTabPassNo: 1, - }, -} - -var partitionTables = distro.BasePartitionTableMap{ - arch.ARCH_X86_64.String(): disk.PartitionTable{ - UUID: diskUuidOfUnknownOrigin, - Type: disk.PT_GPT, - Partitions: []disk.Partition{ - { - Size: 1 * MebiByte, - Bootable: true, - Type: disk.BIOSBootPartitionGUID, - UUID: disk.BIOSBootPartitionUUID, - }, - efiPartition, - bootPartition, - rootPartition, - }, - }, - arch.ARCH_AARCH64.String(): disk.PartitionTable{ - UUID: diskUuidOfUnknownOrigin, - Type: disk.PT_GPT, - Partitions: []disk.Partition{ - efiPartition, - bootPartition, - rootPartition, - }, - }, - arch.ARCH_S390X.String(): disk.PartitionTable{ - UUID: diskUuidOfUnknownOrigin, - Type: disk.PT_GPT, - Partitions: []disk.Partition{ - bootPartition, - rootPartition, - }, - }, - arch.ARCH_PPC64LE.String(): disk.PartitionTable{ - UUID: diskUuidOfUnknownOrigin, - Type: disk.PT_GPT, - Partitions: []disk.Partition{ - { - Size: 4 * MebiByte, - Type: disk.PRePartitionGUID, - Bootable: true, - }, - bootPartition, - rootPartition, - }, - }, - arch.ARCH_RISCV64.String(): disk.PartitionTable{ - UUID: diskUuidOfUnknownOrigin, - Type: disk.PT_GPT, - Partitions: []disk.Partition{ - efiPartition, - bootPartition, - rootPartition, - }, - }, -} diff --git a/bib/go.mod b/bib/go.mod index c06261ddd..b7d7f5eb4 100644 --- a/bib/go.mod +++ b/bib/go.mod @@ -5,9 +5,8 @@ go 1.23.9 require ( github.com/cheggaaa/pb/v3 v3.1.7 github.com/hashicorp/go-version v1.7.0 - github.com/osbuild/blueprint v1.13.0 github.com/osbuild/image-builder-cli v0.0.0-20250331194259-63bb56e12db3 - github.com/osbuild/images v0.179.0 + github.com/osbuild/images v0.180.1-0.20250827153323-b3eeac43188f github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.7 @@ -23,25 +22,25 @@ require ( github.com/Microsoft/hcsshim v0.13.0 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect - github.com/aws/aws-sdk-go-v2 v1.38.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.38.1 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 // indirect - github.com/aws/aws-sdk-go-v2/config v1.31.0 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.18.4 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3 // indirect - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.18.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.3 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.3 // indirect + github.com/aws/aws-sdk-go-v2/config v1.31.2 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.18.6 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.3 // indirect - github.com/aws/aws-sdk-go-v2/service/ec2 v1.244.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ec2 v1.245.2 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.3 // indirect - github.com/aws/aws-sdk-go-v2/service/s3 v1.87.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.28.0 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.37.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.4 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.87.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.28.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.38.0 // indirect github.com/aws/smithy-go v1.22.5 // indirect github.com/containerd/cgroups/v3 v3.0.5 // indirect github.com/containerd/errdefs v1.0.0 // indirect @@ -99,6 +98,7 @@ require ( github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opencontainers/runtime-spec v1.2.1 // indirect github.com/opencontainers/selinux v1.12.0 // indirect + github.com/osbuild/blueprint v1.13.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/proglottis/gpgme v0.1.4 // indirect @@ -127,9 +127,11 @@ require ( golang.org/x/sys v0.35.0 // indirect golang.org/x/term v0.34.0 // indirect golang.org/x/text v0.28.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect google.golang.org/grpc v1.74.2 // indirect google.golang.org/protobuf v1.36.7 // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) + +replace github.com/osbuild/images => github.com/mvo5/images v0.0.0-20250828130520-bbe53a9fa6ad diff --git a/bib/go.sum b/bib/go.sum index f8e4ed121..f83a8974d 100644 --- a/bib/go.sum +++ b/bib/go.sum @@ -14,44 +14,44 @@ 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/aws/aws-sdk-go-v2 v1.38.0 h1:UCRQ5mlqcFk9HJDIqENSLR3wiG1VTWlyUfLDEvY7RxU= -github.com/aws/aws-sdk-go-v2 v1.38.0/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= +github.com/aws/aws-sdk-go-v2 v1.38.1 h1:j7sc33amE74Rz0M/PoCpsZQ6OunLqys/m5antM0J+Z8= +github.com/aws/aws-sdk-go-v2 v1.38.1/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0 h1:6GMWV6CNpA/6fbFHnoAjrv4+LGfyTqZz2LtCHnspgDg= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.0/go.mod h1:/mXlTIVG9jbxkqDnr5UQNQxW1HRYxeGklkM9vAFeabg= -github.com/aws/aws-sdk-go-v2/config v1.31.0 h1:9yH0xiY5fUnVNLRWO0AtayqwU1ndriZdN78LlhruJR4= -github.com/aws/aws-sdk-go-v2/config v1.31.0/go.mod h1:VeV3K72nXnhbe4EuxxhzsDc/ByrCSlZwUnWH52Nde/I= -github.com/aws/aws-sdk-go-v2/credentials v1.18.4 h1:IPd0Algf1b+Qy9BcDp0sCUcIWdCQPSzDoMK3a8pcbUM= -github.com/aws/aws-sdk-go-v2/credentials v1.18.4/go.mod h1:nwg78FjH2qvsRM1EVZlX9WuGUJOL5od+0qvm0adEzHk= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3 h1:GicIdnekoJsjq9wqnvyi2elW6CGMSYKhdozE7/Svh78= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.3/go.mod h1:R7BIi6WNC5mc1kfRM7XM/VHC3uRWkjc396sfabq4iOo= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.18.4 h1:0SzCLoPRSK3qSydsaFQWugP+lOBCTPwfcBOm6222+UA= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.18.4/go.mod h1:JAet9FsBHjfdI+TnMBX4ModNNaQHAd3dc/Bk+cNsxeM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.3 h1:o9RnO+YZ4X+kt5Z7Nvcishlz0nksIt2PIzDglLMP0vA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.3/go.mod h1:+6aLJzOG1fvMOyzIySYjOFjcguGvVRL68R+uoRencN4= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.3 h1:joyyUFhiTQQmVK6ImzNU9TQSNRNeD9kOklqTzyk5v6s= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.3/go.mod h1:+vNIyZQP3b3B1tSLI0lxvrU9cfM7gpdRXMFfm67ZcPc= +github.com/aws/aws-sdk-go-v2/config v1.31.2 h1:NOaSZpVGEH2Np/c1toSeW0jooNl+9ALmsUTZ8YvkJR0= +github.com/aws/aws-sdk-go-v2/config v1.31.2/go.mod h1:17ft42Yb2lF6OigqSYiDAiUcX4RIkEMY6XxEMJsrAes= +github.com/aws/aws-sdk-go-v2/credentials v1.18.6 h1:AmmvNEYrru7sYNJnp3pf57lGbiarX4T9qU/6AZ9SucU= +github.com/aws/aws-sdk-go-v2/credentials v1.18.6/go.mod h1:/jdQkh1iVPa01xndfECInp1v1Wnp70v3K4MvtlLGVEc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4 h1:lpdMwTzmuDLkgW7086jE94HweHCqG+uOJwHf3LZs7T0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.4/go.mod h1:9xzb8/SV62W6gHQGC/8rrvgNXU6ZoYM3sAIJCIrXJxY= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.0 h1:2FFgK3oFA8PTNBjprLFfcmkgg7U9YuSimBvR64RUmiA= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.19.0/go.mod h1:xdxj6nC1aU/jAO80RIlIj3fU40MOSqutEA9N2XFct04= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4 h1:IdCLsiiIj5YJ3AFevsewURCPV+YWUlOW8JiPhoAy8vg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.4/go.mod h1:l4bdfCD7XyyZA9BolKBo1eLqgaJxl0/x91PL4Yqe0ao= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4 h1:j7vjtr1YIssWQOMeOWRbh3z8g2oY/xPjnZH2gLY4sGw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.4/go.mod h1:yDmJgqOiH4EA8Hndnv4KwAo8jCGTSnM5ASG1nBI+toA= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.3 h1:ZV2XK2L3HBq9sCKQiQ/MdhZJppH/rH0vddEAamsHUIs= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.3/go.mod h1:b9F9tk2HdHpbf3xbN7rUZcfmJI26N6NcJu/8OsBFI/0= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.244.0 h1:KfETrpt7yv2nkSrjOltgmKyAl8scbzYc4TFtZeoV6uc= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.244.0/go.mod h1:EeWmteKqZjaMj45MUmPET1SisFI+HkqWIRQoyjMivcc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.4 h1:BE/MNQ86yzTINrfxPPFS86QCBNQeLKY2A0KhDh47+wI= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.4/go.mod h1:SPBBhkJxjcrzJBc+qY85e83MQ2q3qdra8fghhkkyrJg= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.245.2 h1:P94OfRObDwjklbvdJTGuRZXeGYF7Bv5NNUo+I628kKQ= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.245.2/go.mod h1:D8Wb993SJuFQ10Lp95Vod8VTpYjJz4v0LeW4rEI471c= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.3 h1:3ZKmesYBaFX33czDl6mbrcHb6jeheg6LqjJhQdefhsY= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.3/go.mod h1:7ryVb78GLCnjq7cw45N6oUb9REl7/vNUwjvIqC5UgdY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3 h1:ieRzyHXypu5ByllM7Sp4hC5f/1Fy5wqxqY0yB85hC7s= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.3/go.mod h1:O5ROz8jHiOAKAwx179v+7sHMhfobFVi6nZt8DEyiYoM= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.3 h1:SE/e52dq9a05RuxzLcjT+S5ZpQobj3ie3UTaSf2NnZc= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.3/go.mod h1:zkpvBTsR020VVr8TOrwK2TrUW9pOir28sH5ECHpnAfo= -github.com/aws/aws-sdk-go-v2/service/s3 v1.87.0 h1:egoDf+Geuuntmw79Mz6mk9gGmELCPzg5PFEABOHB+6Y= -github.com/aws/aws-sdk-go-v2/service/s3 v1.87.0/go.mod h1:t9MDi29H+HDbkolTSQtbI0HP9DemAWQzUjmWC7LGMnE= -github.com/aws/aws-sdk-go-v2/service/sso v1.28.0 h1:Mc/MKBf2m4VynyJkABoVEN+QzkfLqGj0aiJuEe7cMeM= -github.com/aws/aws-sdk-go-v2/service/sso v1.28.0/go.mod h1:iS5OmxEcN4QIPXARGhavH7S8kETNL11kym6jhoS7IUQ= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.0 h1:6csaS/aJmqZQbKhi1EyEMM7yBW653Wy/B9hnBofW+sw= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.0/go.mod h1:59qHWaY5B+Rs7HGTuVGaC32m0rdpQ68N8QCN3khYiqs= -github.com/aws/aws-sdk-go-v2/service/sts v1.37.0 h1:MG9VFW43M4A8BYeAfaJJZWrroinxeTi2r3+SnmLQfSA= -github.com/aws/aws-sdk-go-v2/service/sts v1.37.0/go.mod h1:JdeBDPgpJfuS6rU/hNglmOigKhyEZtBmbraLE4GK1J8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.4 h1:Beh9oVgtQnBgR4sKKzkUBRQpf1GnL4wt0l4s8h2VCJ0= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.4/go.mod h1:b17At0o8inygF+c6FOD3rNyYZufPw62o9XJbSfQPgbo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4 h1:ueB2Te0NacDMnaC+68za9jLwkjzxGWm0KB5HTUHjLTI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.4/go.mod h1:nLEfLnVMmLvyIG58/6gsSA03F1voKGaCfHV7+lR8S7s= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.4 h1:HVSeukL40rHclNcUqVcBwE1YoZhOkoLeBfhUqR3tjIU= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.4/go.mod h1:DnbBOv4FlIXHj2/xmrUQYtawRFC9L9ZmQPz+DBc6X5I= +github.com/aws/aws-sdk-go-v2/service/s3 v1.87.1 h1:2n6Pd67eJwAb/5KCX62/8RTU0aFAAW7V5XIGSghiHrw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.87.1/go.mod h1:w5PC+6GHLkvMJKasYGVloB3TduOtROEMqm15HSuIbw4= +github.com/aws/aws-sdk-go-v2/service/sso v1.28.2 h1:ve9dYBB8CfJGTFqcQ3ZLAAb/KXWgYlgu/2R2TZL2Ko0= +github.com/aws/aws-sdk-go-v2/service/sso v1.28.2/go.mod h1:n9bTZFZcBa9hGGqVz3i/a6+NG0zmZgtkB9qVVFDqPA8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2 h1:pd9G9HQaM6UZAZh19pYOkpKSQkyQQ9ftnl/LttQOcGI= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.33.2/go.mod h1:eknndR9rU8UpE/OmFpqU78V1EcXPKFTTm5l/buZYgvM= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.0 h1:iV1Ko4Em/lkJIsoKyGfc0nQySi+v0Udxr6Igq+y9JZc= +github.com/aws/aws-sdk-go-v2/service/sts v1.38.0/go.mod h1:bEPcjW7IbolPfK67G1nilqWyoxYMSPrDiIQ3RdIdKgo= github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw= github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -233,6 +233,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mvo5/images v0.0.0-20250828130520-bbe53a9fa6ad h1:2d5YjL5nshIBMzzMOWogXFJRYdpyykp0Kw6Y2oOTtEM= +github.com/mvo5/images v0.0.0-20250828130520-bbe53a9fa6ad/go.mod h1:qbGjthiOmiZr1xCJEYMHv5oPNXXcxkJyvj7dky4/ibw= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -245,8 +247,6 @@ github.com/osbuild/blueprint v1.13.0 h1:blo22+S2ZX5bBmjGcRveoTUrV4Ms7kLfKyb32Wyu github.com/osbuild/blueprint v1.13.0/go.mod h1:HPlJzkEl7q5g8hzaGksUk7ifFAy9QFw9LmzhuFOAVm4= 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.179.0 h1:E0CkI/UVuiVmgq0BIhzanjaOkf4auFSSDNXiy9jwDl4= -github.com/osbuild/images v0.179.0/go.mod h1:7CfDwGb8YA4erIzvMnqJysVpSu52i6l/f3h82usGPTg= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -463,10 +463,10 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc= -google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:AtEkQdl5b6zsybXcbz00j1LwNodDuH6hVifIaNqk7NQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= diff --git a/test/test_manifest.py b/test/test_manifest.py index 91b99b861..082f3f8c0 100644 --- a/test/test_manifest.py +++ b/test/test_manifest.py @@ -151,7 +151,8 @@ def test_manifest_cross_arch_check(tmp_path, build_container): "manifest", "--target-arch=aarch64", f"localhost/{container_tag}" ], check=True, capture_output=True, encoding="utf8") - assert 'image found is for unexpected architecture "x86_64"' in exc.value.stderr + assert 'cannot generate manifest: requested container architecture '\ + 'does not match resolved container: "x86_64" !=' in exc.value.stderr def find_rootfs_type_from(manifest_str):