Repository: osbuild/osbuild-composer
Pull request #2313 (2022-02-16 11:31:46)

worker: Properly log successful image builds
This will make counting successful image builds in Splunk and Cloudwatch
much simpler and robust.


This pull request includes:

- [ ] adequate testing for the new functionality or fixed issue
- [ ] adequate documentation informing people about the change such as
  - [ ] submit a PR for the [guides](https://github.com/osbuild/guides) repository if this PR changed any behavior described there: https://www.osbuild.org/guides/


Patch:
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 213d52a87..1edd443c5 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -407,6 +407,8 @@ Upgrade:
       - ci-artifacts
       - install_console.log
       - upgrade_console.log
+      - leapp-preupgrade.log
+      - leapp-report.txt
       - osbuild-upgrade8to9.log
       - osbuild-upgrade8to9.json
     when: always
diff --git a/cmd/osbuild-worker/jobimpl-koji-finalize.go b/cmd/osbuild-worker/jobimpl-koji-finalize.go
index 8d4780834..7a21a5892 100644
--- a/cmd/osbuild-worker/jobimpl-koji-finalize.go
+++ b/cmd/osbuild-worker/jobimpl-koji-finalize.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/sirupsen/logrus"
 
+	osbuild "github.com/osbuild/osbuild-composer/internal/osbuild2"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 	"github.com/osbuild/osbuild-composer/internal/upload/koji"
 	"github.com/osbuild/osbuild-composer/internal/worker"
@@ -127,7 +128,7 @@ func (impl *KojiFinalizeJobImpl) Run(job worker.Job) error {
 		// collect packages from stages in build pipelines
 		for _, plName := range buildArgs.PipelineNames.Build {
 			buildPipelineMd := buildArgs.OSBuildOutput.Metadata[plName]
-			buildRPMs = append(buildRPMs, rpmmd.OSBuildMetadataToRPMs(buildPipelineMd)...)
+			buildRPMs = append(buildRPMs, osbuild.OSBuildMetadataToRPMs(buildPipelineMd)...)
 		}
 		// this dedupe is usually not necessary since we generally only have
 		// one rpm stage in one build pipeline, but it's not invalid to have
@@ -155,7 +156,7 @@ func (impl *KojiFinalizeJobImpl) Run(job worker.Job) error {
 		imageRPMs := make([]rpmmd.RPM, 0)
 		for _, plName := range buildArgs.PipelineNames.Payload {
 			payloadPipelineMd := buildArgs.OSBuildOutput.Metadata[plName]
-			imageRPMs = append(imageRPMs, rpmmd.OSBuildMetadataToRPMs(payloadPipelineMd)...)
+			imageRPMs = append(imageRPMs, osbuild.OSBuildMetadataToRPMs(payloadPipelineMd)...)
 		}
 
 		// deduplicate
diff --git a/cmd/osbuild-worker/jobimpl-osbuild-koji.go b/cmd/osbuild-worker/jobimpl-osbuild-koji.go
index 3c60384e1..6f06addc1 100644
--- a/cmd/osbuild-worker/jobimpl-osbuild-koji.go
+++ b/cmd/osbuild-worker/jobimpl-osbuild-koji.go
@@ -61,6 +61,8 @@ func validateKojiResult(result *worker.OSBuildKojiJobResult, jobID string) {
 		reason := "osbuild job was unsuccessful"
 		logWithId.Errorf("osbuild job failed: %s", reason)
 		result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorBuildJob, reason)
+	} else {
+		logWithId.Infof("osbuild-koji job succeeded")
 	}
 }
 
diff --git a/cmd/osbuild-worker/jobimpl-osbuild.go b/cmd/osbuild-worker/jobimpl-osbuild.go
index bfaba5078..bef5ac6bb 100644
--- a/cmd/osbuild-worker/jobimpl-osbuild.go
+++ b/cmd/osbuild-worker/jobimpl-osbuild.go
@@ -62,6 +62,8 @@ func validateResult(result *worker.OSBuildJobResult, jobID string) {
 		logWithId.Errorf("osbuild job failed: %s", reason)
 		result.JobError = clienterrors.WorkerClientError(clienterrors.ErrorBuildJob, reason)
 		return
+	} else {
+		logWithId.Infof("osbuild job succeeded")
 	}
 	result.Success = true
 }
diff --git a/internal/cloudapi/v2/v2.go b/internal/cloudapi/v2/v2.go
index b3732c9ab..ddfe7d28e 100644
--- a/internal/cloudapi/v2/v2.go
+++ b/internal/cloudapi/v2/v2.go
@@ -1013,7 +1013,7 @@ func stagesToPackageMetadata(stages []osbuild.RPMStageMetadata) []PackageMetadat
 					Epoch:     rpm.Epoch,
 					Arch:      rpm.Arch,
 					Sigmd5:    rpm.SigMD5,
-					Signature: rpmmd.PackageMetadataToSignature(rpm),
+					Signature: osbuild.RPMPackageMetadataToSignature(rpm),
 				},
 			)
 		}
diff --git a/internal/distro/rhel84/distro_v2.go b/internal/distro/rhel84/distro_v2.go
index 0e27e6ff2..091d8f44b 100644
--- a/internal/distro/rhel84/distro_v2.go
+++ b/internal/distro/rhel84/distro_v2.go
@@ -275,7 +275,7 @@ func (t *imageTypeS2) buildPipeline(repos []rpmmd.RepoConfig, buildPackageSpecs
 	p := new(osbuild.Pipeline)
 	p.Name = "build"
 	p.Runner = "org.osbuild.rhel84"
-	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(repos), t.rpmStageInputs(buildPackageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(buildPackageSpecs)))
 	p.AddStage(osbuild.NewSELinuxStage(t.selinuxStageOptions()))
 	return p
 }
@@ -284,7 +284,7 @@ func (t *imageTypeS2) ostreeTreePipeline(repos []rpmmd.RepoConfig, packages []rp
 	p := new(osbuild.Pipeline)
 	p.Name = "ostree-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(repos), t.rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	language, keyboard := c.GetPrimaryLocale()
 	if language != nil {
 		p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: *language}))
@@ -381,7 +381,7 @@ func (t *imageTypeS2) containerTreePipeline(repos []rpmmd.RepoConfig, packages [
 	p := new(osbuild.Pipeline)
 	p.Name = "container-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(repos), t.rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	language, _ := c.GetPrimaryLocale()
 	if language != nil {
 		p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: *language}))
@@ -392,7 +392,7 @@ func (t *imageTypeS2) containerTreePipeline(repos []rpmmd.RepoConfig, packages [
 
 	p.AddStage(osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: "/var/www/html/repo"},
-		t.ostreePullStageInputs("org.osbuild.pipeline", "name:ostree-commit", options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.pipeline", "name:ostree-commit", options.OSTree.Ref),
 	))
 	return p
 }
@@ -424,11 +424,11 @@ func (t *imageTypeS2) anacondaTreePipeline(repos []rpmmd.RepoConfig, packages []
 	p := new(osbuild.Pipeline)
 	p.Name = "anaconda-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(repos), t.rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	p.AddStage(osbuild.NewOSTreeInitStage(&osbuild.OSTreeInitStageOptions{Path: ostreeRepoPath}))
 	p.AddStage(osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: ostreeRepoPath},
-		t.ostreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
 	))
 	p.AddStage(osbuild.NewBuildstampStage(t.buildStampStageOptions()))
 	p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: "en_US.UTF-8"}))
@@ -471,7 +471,7 @@ func (t *imageTypeS2) bootISOTreePipeline(kernelVer string) *osbuild.Pipeline {
 	p.Name = "bootiso-tree"
 	p.Build = "name:build"
 
-	p.AddStage(osbuild.NewBootISOMonoStage(t.bootISOMonoStageOptions(kernelVer), t.bootISOMonoStageInputs()))
+	p.AddStage(osbuild.NewBootISOMonoStage(t.bootISOMonoStageOptions(kernelVer), osbuild.NewBootISOMonoStagePipelineTreeInputs("anaconda-tree")))
 	p.AddStage(osbuild.NewDiscinfoStage(t.discinfoStageOptions()))
 
 	return p
@@ -482,39 +482,12 @@ func (t *imageTypeS2) bootISOPipeline() *osbuild.Pipeline {
 	p.Name = "assembler"
 	p.Build = "name:build"
 
-	p.AddStage(osbuild.NewXorrisofsStage(t.xorrisofsStageOptions(), t.xorrisofsStageInputs()))
+	p.AddStage(osbuild.NewXorrisofsStage(t.xorrisofsStageOptions(), osbuild.NewXorrisofsStagePipelineTreeInputs("bootiso-tree")))
 	p.AddStage(osbuild.NewImplantisomd5Stage(&osbuild.Implantisomd5StageOptions{Filename: t.Filename()}))
 
 	return p
 }
 
-func (t *imageTypeS2) rpmStageInputs(specs []rpmmd.PackageSpec) *osbuild.RPMStageInputs {
-	stageInput := new(osbuild.RPMStageInput)
-	stageInput.Type = "org.osbuild.files"
-	stageInput.Origin = "org.osbuild.source"
-	stageInput.References = pkgRefs(specs)
-	return &osbuild.RPMStageInputs{Packages: stageInput}
-}
-
-func pkgRefs(specs []rpmmd.PackageSpec) osbuild.RPMStageReferences {
-	refs := make([]string, len(specs))
-	for idx, pkg := range specs {
-		refs[idx] = pkg.Checksum
-	}
-	return refs
-}
-
-func (t *imageTypeS2) ostreePullStageInputs(origin, source, commitRef string) *osbuild.OSTreePullStageInputs {
-	pullStageInput := new(osbuild.OSTreePullStageInput)
-	pullStageInput.Type = "org.osbuild.ostree"
-	pullStageInput.Origin = origin
-
-	inputRefs := make(map[string]osbuild.OSTreePullStageReference)
-	inputRefs[source] = osbuild.OSTreePullStageReference{Ref: commitRef}
-	pullStageInput.References = inputRefs
-	return &osbuild.OSTreePullStageInputs{Commits: pullStageInput}
-}
-
 func (t *imageTypeS2) rpmStageOptions(repos []rpmmd.RepoConfig) *osbuild.RPMStageOptions {
 	var gpgKeys []string
 	for _, repo := range repos {
@@ -775,16 +748,6 @@ func (t *imageTypeS2) bootISOMonoStageOptions(kernelVer string) *osbuild.BootISO
 	}
 }
 
-func (t *imageTypeS2) bootISOMonoStageInputs() *osbuild.BootISOMonoStageInputs {
-	rootfsInput := new(osbuild.BootISOMonoStageInput)
-	rootfsInput.Type = "org.osbuild.tree"
-	rootfsInput.Origin = "org.osbuild.pipeline"
-	rootfsInput.References = osbuild.BootISOMonoStageReferences{"name:anaconda-tree"}
-	return &osbuild.BootISOMonoStageInputs{
-		RootFS: rootfsInput,
-	}
-}
-
 func (t *imageTypeS2) discinfoStageOptions() *osbuild.DiscinfoStageOptions {
 	return &osbuild.DiscinfoStageOptions{
 		BaseArch: t.Arch().Name(),
@@ -805,11 +768,3 @@ func (t *imageTypeS2) xorrisofsStageOptions() *osbuild.XorrisofsStageOptions {
 		IsohybridMBR: "/usr/share/syslinux/isohdpfx.bin",
 	}
 }
-
-func (t *imageTypeS2) xorrisofsStageInputs() *osbuild.XorrisofsStageInputs {
-	input := new(osbuild.XorrisofsStageInput)
-	input.Type = "org.osbuild.tree"
-	input.Origin = "org.osbuild.pipeline"
-	input.References = osbuild.XorrisofsStageReferences{"name:bootiso-tree"}
-	return &osbuild.XorrisofsStageInputs{Tree: input}
-}
diff --git a/internal/distro/rhel85/pipelines.go b/internal/distro/rhel85/pipelines.go
index 75a74d0ff..05c778827 100644
--- a/internal/distro/rhel85/pipelines.go
+++ b/internal/distro/rhel85/pipelines.go
@@ -47,7 +47,7 @@ func qcow2Pipelines(t *imageType, customizations *blueprint.Customizations, opti
 	}
 
 	treePipeline.AddStage(osbuild.NewFSTabStage(partitionTable.FSTabStageOptionsV2()))
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	treePipeline.AddStage(bootloaderConfigStage(t, partitionTable, customizations.GetKernel(), kernelVer, false, false))
 	treePipeline.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
 	pipelines = append(pipelines, *treePipeline)
@@ -68,7 +68,7 @@ func prependKernelCmdlineStage(pipeline *osbuild.Pipeline, t *imageType, pt *dis
 		if rootPartition == nil {
 			panic("s390x image must have a root partition, this is a programming error")
 		}
-		kernelStage := osbuild.NewKernelCmdlineStage(kernelCmdlineStageOptions(rootPartition.Filesystem.UUID, t.kernelOptions))
+		kernelStage := osbuild.NewKernelCmdlineStage(osbuild.NewKernelCmdlineStageOptions(rootPartition.Filesystem.UUID, t.kernelOptions))
 		pipeline.Stages = append([]*osbuild.Stage{kernelStage}, pipeline.Stages...)
 	}
 	return pipeline
@@ -88,7 +88,7 @@ func vhdPipelines(t *imageType, customizations *blueprint.Customizations, option
 	}
 
 	treePipeline.AddStage(osbuild.NewFSTabStage(partitionTable.FSTabStageOptionsV2()))
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	treePipeline.AddStage(bootloaderConfigStage(t, partitionTable, customizations.GetKernel(), kernelVer, false, false))
 	treePipeline.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
 	pipelines = append(pipelines, *treePipeline)
@@ -119,7 +119,7 @@ func vmdkPipelines(t *imageType, customizations *blueprint.Customizations, optio
 	}
 
 	treePipeline.AddStage(osbuild.NewFSTabStage(partitionTable.FSTabStageOptionsV2()))
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	treePipeline.AddStage(bootloaderConfigStage(t, partitionTable, customizations.GetKernel(), kernelVer, false, false))
 	treePipeline.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
 	pipelines = append(pipelines, *treePipeline)
@@ -150,7 +150,7 @@ func openstackPipelines(t *imageType, customizations *blueprint.Customizations,
 	}
 
 	treePipeline.AddStage(osbuild.NewFSTabStage(partitionTable.FSTabStageOptionsV2()))
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	treePipeline.AddStage(bootloaderConfigStage(t, partitionTable, customizations.GetKernel(), kernelVer, false, false))
 	treePipeline.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
 	pipelines = append(pipelines, *treePipeline)
@@ -184,7 +184,7 @@ func ec2BaseTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec,
 	p.Name = "os"
 	p.Build = "name:build"
 	packages = append(packages, bpPackages...)
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 
 	// If the /boot is on a separate partition, the prefix for the BLS stage must be ""
 	if pt.BootPartition() == nil {
@@ -432,7 +432,7 @@ func ec2CommonPipelines(t *imageType, customizations *blueprint.Customizations,
 	}
 
 	treePipeline.AddStage(osbuild.NewFSTabStage(partitionTable.FSTabStageOptionsV2()))
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	treePipeline.AddStage(bootloaderConfigStage(t, partitionTable, customizations.GetKernel(), kernelVer, false, false))
 	// The last stage must be the SELinux stage
 	treePipeline.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
@@ -493,7 +493,7 @@ func edgeInstallerPipelines(t *imageType, customizations *blueprint.Customizatio
 	pipelines := make([]osbuild.Pipeline, 0)
 	pipelines = append(pipelines, *buildPipeline(repos, packageSetSpecs[buildPkgsKey]))
 	installerPackages := packageSetSpecs[installerPkgsKey]
-	kernelVer := kernelVerStr(installerPackages, "kernel", t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(installerPackages, "kernel")
 	ostreeRepoPath := "/ostree/repo"
 	payloadStages := ostreePayloadStages(options, ostreeRepoPath)
 	kickstartOptions := ostreeKickstartStageOptions(makeISORootPath(ostreeRepoPath), options.OSTree.Ref)
@@ -626,7 +626,7 @@ func buildPipeline(repos []rpmmd.RepoConfig, buildPackageSpecs []rpmmd.PackageSp
 	p := new(osbuild.Pipeline)
 	p.Name = "build"
 	p.Runner = "org.osbuild.rhel85"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(buildPackageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(buildPackageSpecs)))
 	p.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(true)))
 	return p
 }
@@ -636,7 +636,7 @@ func osPipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec, bpPackag
 	p.Name = "os"
 	p.Build = "name:build"
 	packages = append(packages, bpPackages...)
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	p.AddStage(osbuild.NewFixBLSStage(&osbuild.FixBLSStageOptions{}))
 	language, keyboard := c.GetPrimaryLocale()
 	if language != nil {
@@ -722,7 +722,7 @@ func ostreeTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec,
 		p.AddStage(osbuild2.NewOSTreePasswdStage("org.osbuild.source", options.OSTree.Parent))
 	}
 
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	p.AddStage(osbuild.NewFixBLSStage(&osbuild.FixBLSStageOptions{}))
 	language, keyboard := c.GetPrimaryLocale()
 	if language != nil {
@@ -839,7 +839,7 @@ func containerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpe
 	p := new(osbuild.Pipeline)
 	p.Name = "container-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	language, _ := c.GetPrimaryLocale()
 	if language != nil {
 		p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: *language}))
@@ -853,7 +853,7 @@ func containerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpe
 
 	p.AddStage(osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: repoPath},
-		ostreePullStageInputs("org.osbuild.pipeline", "name:ostree-commit", options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.pipeline", "name:ostree-commit", options.OSTree.Ref),
 	))
 
 	// make nginx log directory world writeable, otherwise nginx can't start in
@@ -892,7 +892,7 @@ func ostreePayloadStages(options distro.ImageOptions, ostreeRepoPath string) []*
 	stages = append(stages, osbuild.NewOSTreeInitStage(&osbuild.OSTreeInitStageOptions{Path: ostreeRepoPath}))
 	stages = append(stages, osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: ostreeRepoPath},
-		ostreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
 	))
 
 	return stages
@@ -902,7 +902,7 @@ func edgeSimplifiedInstallerPipelines(t *imageType, customizations *blueprint.Cu
 	pipelines := make([]osbuild.Pipeline, 0)
 	pipelines = append(pipelines, *buildPipeline(repos, packageSetSpecs[buildPkgsKey]))
 	installerPackages := packageSetSpecs[installerPkgsKey]
-	kernelVer := kernelVerStr(installerPackages, "kernel", t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(installerPackages, "kernel")
 	imgName := "disk.img.xz"
 	installDevice := customizations.GetInstallationDevice()
 
@@ -979,12 +979,12 @@ func simplifiedInstallerBootISOTreePipeline(archivePipelineName, kver string) *o
 	}
 
 	inputName := "root-tree"
-	copyInputs := copyPipelineTreeInputs(inputName, "efiboot-tree")
+	copyInputs := osbuild.NewCopyStagePipelineTreeInputs(inputName, "efiboot-tree")
 	copyOptions, copyDevices, copyMounts := copyFSTreeOptions(inputName, "efiboot-tree", &pt, loopback)
 	p.AddStage(osbuild.NewCopyStage(copyOptions, copyInputs, copyDevices, copyMounts))
 
 	inputName = "coi"
-	copyInputs = copyPipelineTreeInputs(inputName, "coi-tree")
+	copyInputs = osbuild.NewCopyStagePipelineTreeInputs(inputName, "coi-tree")
 	p.AddStage(osbuild.NewCopyStageSimple(
 		&osbuild.CopyStageOptions{
 			Paths: []osbuild.CopyStagePath{
@@ -1002,7 +1002,7 @@ func simplifiedInstallerBootISOTreePipeline(archivePipelineName, kver string) *o
 	))
 
 	inputName = "efi-tree"
-	copyInputs = copyPipelineTreeInputs(inputName, "efiboot-tree")
+	copyInputs = osbuild.NewCopyStagePipelineTreeInputs(inputName, "efiboot-tree")
 	p.AddStage(osbuild.NewCopyStageSimple(
 		&osbuild.CopyStageOptions{
 			Paths: []osbuild.CopyStagePath{
@@ -1066,7 +1066,7 @@ func simplifiedInstallerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.
 	p := new(osbuild.Pipeline)
 	p.Name = "coi-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	p.AddStage(osbuild.NewBuildstampStage(buildStampStageOptions(arch)))
 	p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: "en_US.UTF-8"}))
 	p.AddStage(osbuild.NewSystemdStage(systemdStageOptions([]string{"coreos-installer"}, nil, nil, "")))
@@ -1095,7 +1095,7 @@ func ostreeDeployPipeline(
 	p.AddStage(osbuild.OSTreeInitFsStage())
 	p.AddStage(osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: repoPath},
-		ostreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
 	))
 	p.AddStage(osbuild.NewOSTreeOsInitStage(
 		&osbuild.OSTreeOsInitStageOptions{
@@ -1155,7 +1155,7 @@ func anacondaTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec
 	p := new(osbuild.Pipeline)
 	p.Name = "anaconda-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	p.AddStage(osbuild.NewBuildstampStage(buildStampStageOptions(arch)))
 	p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: "en_US.UTF-8"}))
 
@@ -1198,7 +1198,7 @@ func bootISOTreePipeline(kernelVer string, arch string, ksOptions *osbuild.Kicks
 	p.Name = "bootiso-tree"
 	p.Build = "name:build"
 
-	p.AddStage(osbuild.NewBootISOMonoStage(bootISOMonoStageOptions(kernelVer, arch), bootISOMonoStageInputs()))
+	p.AddStage(osbuild.NewBootISOMonoStage(bootISOMonoStageOptions(kernelVer, arch), osbuild.NewBootISOMonoStagePipelineTreeInputs("anaconda-tree")))
 	p.AddStage(osbuild.NewKickstartStage(ksOptions))
 	p.AddStage(osbuild.NewDiscinfoStage(discinfoStageOptions(arch)))
 
@@ -1213,7 +1213,7 @@ func bootISOPipeline(filename string, arch string, isolinux bool) *osbuild.Pipel
 	p.Name = "bootiso"
 	p.Build = "name:build"
 
-	p.AddStage(osbuild.NewXorrisofsStage(xorrisofsStageOptions(filename, arch, isolinux), xorrisofsStageInputs("bootiso-tree")))
+	p.AddStage(osbuild.NewXorrisofsStage(xorrisofsStageOptions(filename, arch, isolinux), osbuild.NewXorrisofsStagePipelineTreeInputs("bootiso-tree")))
 	p.AddStage(osbuild.NewImplantisomd5Stage(&osbuild.Implantisomd5StageOptions{Filename: filename}))
 
 	return p
@@ -1235,7 +1235,7 @@ func liveImagePipeline(inputPipelineName string, outputFilename string, pt *disk
 
 	inputName := "root-tree"
 	copyOptions, copyDevices, copyMounts := copyFSTreeOptions(inputName, inputPipelineName, pt, loopback)
-	copyInputs := copyPipelineTreeInputs(inputName, inputPipelineName)
+	copyInputs := osbuild.NewCopyStagePipelineTreeInputs(inputName, inputPipelineName)
 	p.AddStage(osbuild.NewCopyStage(copyOptions, copyInputs, copyDevices, copyMounts))
 	p.AddStage(bootloaderInstStage(outputFilename, pt, arch, kernelVer, copyDevices, copyMounts, loopback))
 	return p
@@ -1316,7 +1316,7 @@ func qemuPipeline(inputPipelineName, inputFilename, outputFilename, format, qcow
 	p.Name = format
 	p.Build = "name:build"
 
-	qemuStage := osbuild.NewQEMUStage(qemuStageOptions(outputFilename, format, qcow2Compat), qemuStageInputs(inputPipelineName, inputFilename))
+	qemuStage := osbuild.NewQEMUStage(qemuStageOptions(outputFilename, format, qcow2Compat), osbuild.NewQemuStagePipelineFilesInputs(inputPipelineName, inputFilename))
 	p.AddStage(qemuStage)
 	return p
 }
@@ -1348,19 +1348,3 @@ func bootloaderInstStage(filename string, pt *disk.PartitionTable, arch *archite
 
 	return nil
 }
-
-func kernelVerStr(pkgs []rpmmd.PackageSpec, kernelName, arch string) string {
-	kernelPkg := new(rpmmd.PackageSpec)
-	for _, pkg := range pkgs {
-		if pkg.Name == kernelName {
-			// Implicit memory alasing doesn't couse any bug in this case
-			/* #nosec G601 */
-			kernelPkg = &pkg
-			break
-		}
-	}
-	if kernelPkg == nil {
-		panic(fmt.Sprintf("kernel package %q not found", kernelName))
-	}
-	return fmt.Sprintf("%s-%s.%s", kernelPkg.Version, kernelPkg.Release, kernelPkg.Arch)
-}
diff --git a/internal/distro/rhel85/stage_inputs.go b/internal/distro/rhel85/stage_inputs.go
deleted file mode 100644
index 80bc2690a..000000000
--- a/internal/distro/rhel85/stage_inputs.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package rhel85
-
-import (
-	osbuild "github.com/osbuild/osbuild-composer/internal/osbuild2"
-	"github.com/osbuild/osbuild-composer/internal/rpmmd"
-)
-
-func bootISOMonoStageInputs() *osbuild.BootISOMonoStageInputs {
-	rootfsInput := new(osbuild.BootISOMonoStageInput)
-	rootfsInput.Type = "org.osbuild.tree"
-	rootfsInput.Origin = "org.osbuild.pipeline"
-	rootfsInput.References = osbuild.BootISOMonoStageReferences{"name:anaconda-tree"}
-	return &osbuild.BootISOMonoStageInputs{
-		RootFS: rootfsInput,
-	}
-}
-
-func rpmStageInputs(specs []rpmmd.PackageSpec) *osbuild.RPMStageInputs {
-	stageInput := new(osbuild.RPMStageInput)
-	stageInput.Type = "org.osbuild.files"
-	stageInput.Origin = "org.osbuild.source"
-	stageInput.References = pkgRefs(specs)
-	return &osbuild.RPMStageInputs{Packages: stageInput}
-}
-
-func pkgRefs(specs []rpmmd.PackageSpec) osbuild.RPMStageReferences {
-	refs := make([]string, len(specs))
-	for idx, pkg := range specs {
-		refs[idx] = pkg.Checksum
-	}
-	return refs
-}
-
-func ostreePullStageInputs(origin, source, commitRef string) *osbuild.OSTreePullStageInputs {
-	pullStageInput := new(osbuild.OSTreePullStageInput)
-	pullStageInput.Type = "org.osbuild.ostree"
-	pullStageInput.Origin = origin
-
-	inputRefs := make(map[string]osbuild.OSTreePullStageReference)
-	inputRefs[source] = osbuild.OSTreePullStageReference{Ref: commitRef}
-	pullStageInput.References = inputRefs
-	return &osbuild.OSTreePullStageInputs{Commits: pullStageInput}
-}
-
-func xorrisofsStageInputs(pipeline string) *osbuild.XorrisofsStageInputs {
-	input := new(osbuild.XorrisofsStageInput)
-	input.Type = "org.osbuild.tree"
-	input.Origin = "org.osbuild.pipeline"
-	input.References = osbuild.XorrisofsStageReferences{"name:" + pipeline}
-	return &osbuild.XorrisofsStageInputs{Tree: input}
-}
-
-func copyPipelineTreeInputs(name, inputPipeline string) *osbuild.CopyStageInputs {
-	inputName := name
-	treeInput := osbuild.CopyStageInput{}
-	treeInput.Type = "org.osbuild.tree"
-	treeInput.Origin = "org.osbuild.pipeline"
-	treeInput.References = []string{"name:" + inputPipeline}
-	return &osbuild.CopyStageInputs{inputName: treeInput}
-}
-
-func qemuStageInputs(stage, file string) *osbuild.QEMUStageInputs {
-	stageKey := "name:" + stage
-	ref := map[string]osbuild.QEMUFile{
-		stageKey: {
-			File: file,
-		},
-	}
-	input := new(osbuild.QEMUStageInput)
-	input.Type = "org.osbuild.files"
-	input.Origin = "org.osbuild.pipeline"
-	input.References = ref
-	return &osbuild.QEMUStageInputs{Image: input}
-}
diff --git a/internal/distro/rhel85/stage_options.go b/internal/distro/rhel85/stage_options.go
index 9ae5b64a9..90d876f27 100644
--- a/internal/distro/rhel85/stage_options.go
+++ b/internal/distro/rhel85/stage_options.go
@@ -532,13 +532,6 @@ func qemuStageOptions(filename, format, compat string) *osbuild.QEMUStageOptions
 	}
 }
 
-func kernelCmdlineStageOptions(rootUUID string, kernelOptions string) *osbuild.KernelCmdlineStageOptions {
-	return &osbuild.KernelCmdlineStageOptions{
-		RootFsUUID: rootUUID,
-		KernelOpts: kernelOptions,
-	}
-}
-
 func nginxConfigStageOptions(path, htmlRoot, listen string) *osbuild.NginxConfigStageOptions {
 	// configure nginx to work in an unprivileged container
 	cfg := &osbuild.NginxConfig{
diff --git a/internal/distro/rhel86/pipelines.go b/internal/distro/rhel86/pipelines.go
index 4e1d87908..02521de8a 100644
--- a/internal/distro/rhel86/pipelines.go
+++ b/internal/distro/rhel86/pipelines.go
@@ -31,7 +31,7 @@ func qcow2Pipelines(t *imageType, customizations *blueprint.Customizations, opti
 	pipelines = append(pipelines, *treePipeline)
 
 	diskfile := "disk.img"
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, &partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 
@@ -47,7 +47,7 @@ func prependKernelCmdlineStage(pipeline *osbuild.Pipeline, t *imageType, pt *dis
 		if rootPartition == nil {
 			panic("s390x image must have a root partition, this is a programming error")
 		}
-		kernelStage := osbuild.NewKernelCmdlineStage(kernelCmdlineStageOptions(rootPartition.Filesystem.UUID, t.kernelOptions))
+		kernelStage := osbuild.NewKernelCmdlineStage(osbuild.NewKernelCmdlineStageOptions(rootPartition.Filesystem.UUID, t.kernelOptions))
 		pipeline.Stages = append([]*osbuild.Stage{kernelStage}, pipeline.Stages...)
 	}
 	return pipeline
@@ -69,7 +69,7 @@ func vhdPipelines(t *imageType, customizations *blueprint.Customizations, option
 	pipelines = append(pipelines, *treePipeline)
 
 	diskfile := "disk.img"
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, &partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 	if err != nil {
@@ -97,7 +97,7 @@ func vmdkPipelines(t *imageType, customizations *blueprint.Customizations, optio
 	pipelines = append(pipelines, *treePipeline)
 
 	diskfile := "disk.img"
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, &partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 	if err != nil {
@@ -125,7 +125,7 @@ func openstackPipelines(t *imageType, customizations *blueprint.Customizations,
 	pipelines = append(pipelines, *treePipeline)
 
 	diskfile := "disk.img"
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, &partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 	if err != nil {
@@ -153,8 +153,7 @@ func ec2CommonPipelines(t *imageType, customizations *blueprint.Customizations,
 		return nil, err
 	}
 	pipelines = append(pipelines, *treePipeline)
-
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, &partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 	return pipelines, nil
@@ -211,7 +210,7 @@ func edgeInstallerPipelines(t *imageType, customizations *blueprint.Customizatio
 	installerPackages := packageSetSpecs[installerPkgsKey]
 	d := t.arch.distro
 	archName := t.arch.name
-	kernelVer := kernelVerStr(installerPackages, "kernel", archName)
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(installerPackages, "kernel")
 	ostreeRepoPath := "/ostree/repo"
 	payloadStages := ostreePayloadStages(options, ostreeRepoPath)
 	kickstartOptions := ostreeKickstartStageOptions(makeISORootPath(ostreeRepoPath), options.OSTree.Ref)
@@ -347,7 +346,7 @@ func buildPipeline(repos []rpmmd.RepoConfig, buildPackageSpecs []rpmmd.PackageSp
 	p := new(osbuild.Pipeline)
 	p.Name = "build"
 	p.Runner = runner
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(buildPackageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(buildPackageSpecs)))
 	p.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(true)))
 	return p
 }
@@ -373,7 +372,7 @@ func osPipeline(t *imageType,
 		p.AddStage(osbuild.NewOSTreePasswdStage("org.osbuild.source", options.OSTree.Parent))
 	}
 
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 
 	// If the /boot is on a separate partition, the prefix for the BLS stage must be ""
 	if pt == nil || pt.BootPartition() == nil {
@@ -522,7 +521,7 @@ func osPipeline(t *imageType,
 	if pt != nil {
 		p = prependKernelCmdlineStage(p, t, pt)
 		p.AddStage(osbuild.NewFSTabStage(pt.FSTabStageOptionsV2()))
-		kernelVer := kernelVerStr(bpPackages, c.GetKernel().Name, t.Arch().Name())
+		kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(bpPackages, c.GetKernel().Name)
 		p.AddStage(bootloaderConfigStage(t, *pt, c.GetKernel(), kernelVer, false, false))
 	}
 
@@ -574,7 +573,7 @@ func containerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpe
 	p := new(osbuild.Pipeline)
 	p.Name = "container-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	language, _ := c.GetPrimaryLocale()
 	if language != nil {
 		p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: *language}))
@@ -588,7 +587,7 @@ func containerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpe
 
 	p.AddStage(osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: repoPath},
-		ostreePullStageInputs("org.osbuild.pipeline", "name:ostree-commit", options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.pipeline", "name:ostree-commit", options.OSTree.Ref),
 	))
 
 	// make nginx log directory world writeable, otherwise nginx can't start in
@@ -627,7 +626,7 @@ func ostreePayloadStages(options distro.ImageOptions, ostreeRepoPath string) []*
 	stages = append(stages, osbuild.NewOSTreeInitStage(&osbuild.OSTreeInitStageOptions{Path: ostreeRepoPath}))
 	stages = append(stages, osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: ostreeRepoPath},
-		ostreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
 	))
 
 	return stages
@@ -637,7 +636,7 @@ func edgeSimplifiedInstallerPipelines(t *imageType, customizations *blueprint.Cu
 	pipelines := make([]osbuild.Pipeline, 0)
 	pipelines = append(pipelines, *buildPipeline(repos, packageSetSpecs[buildPkgsKey], t.arch.distro.runner))
 	installerPackages := packageSetSpecs[installerPkgsKey]
-	kernelVer := kernelVerStr(installerPackages, "kernel", t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(installerPackages, "kernel")
 	imgName := "disk.img.xz"
 	installDevice := customizations.GetInstallationDevice()
 
@@ -717,12 +716,12 @@ func simplifiedInstallerBootISOTreePipeline(archivePipelineName, kver string) *o
 	}
 
 	inputName := "root-tree"
-	copyInputs := copyPipelineTreeInputs(inputName, "efiboot-tree")
+	copyInputs := osbuild.NewCopyStagePipelineTreeInputs(inputName, "efiboot-tree")
 	copyOptions, copyDevices, copyMounts := copyFSTreeOptions(inputName, "efiboot-tree", &pt, loopback)
 	p.AddStage(osbuild.NewCopyStage(copyOptions, copyInputs, copyDevices, copyMounts))
 
 	inputName = "coi"
-	copyInputs = copyPipelineTreeInputs(inputName, "coi-tree")
+	copyInputs = osbuild.NewCopyStagePipelineTreeInputs(inputName, "coi-tree")
 	p.AddStage(osbuild.NewCopyStageSimple(
 		&osbuild.CopyStageOptions{
 			Paths: []osbuild.CopyStagePath{
@@ -740,7 +739,7 @@ func simplifiedInstallerBootISOTreePipeline(archivePipelineName, kver string) *o
 	))
 
 	inputName = "efi-tree"
-	copyInputs = copyPipelineTreeInputs(inputName, "efiboot-tree")
+	copyInputs = osbuild.NewCopyStagePipelineTreeInputs(inputName, "efiboot-tree")
 	p.AddStage(osbuild.NewCopyStageSimple(
 		&osbuild.CopyStageOptions{
 			Paths: []osbuild.CopyStagePath{
@@ -768,7 +767,7 @@ func simplifiedInstallerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.
 	p := new(osbuild.Pipeline)
 	p.Name = "coi-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	p.AddStage(osbuild.NewBuildstampStage(buildStampStageOptions(arch, product, osVersion, variant)))
 	p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: "en_US.UTF-8"}))
 	p.AddStage(osbuild.NewDracutStage(dracutStageOptions(kernelVer, arch, []string{"coreos-installer"})))
@@ -795,7 +794,7 @@ func ostreeDeployPipeline(
 	p.AddStage(osbuild.OSTreeInitFsStage())
 	p.AddStage(osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: repoPath, Remote: remote},
-		ostreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
 	))
 	p.AddStage(osbuild.NewOSTreeOsInitStage(
 		&osbuild.OSTreeOsInitStageOptions{
@@ -871,7 +870,7 @@ func anacondaTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec
 	p := new(osbuild.Pipeline)
 	p.Name = "anaconda-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	p.AddStage(osbuild.NewBuildstampStage(buildStampStageOptions(arch, product, osVersion, variant)))
 	p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: "en_US.UTF-8"}))
 
@@ -922,7 +921,7 @@ func bootISOTreePipeline(kernelVer, arch, vendor, product, osVersion, isolabel s
 	p.Name = "bootiso-tree"
 	p.Build = "name:build"
 
-	p.AddStage(osbuild.NewBootISOMonoStage(bootISOMonoStageOptions(kernelVer, arch, vendor, product, osVersion, isolabel), bootISOMonoStageInputs()))
+	p.AddStage(osbuild.NewBootISOMonoStage(bootISOMonoStageOptions(kernelVer, arch, vendor, product, osVersion, isolabel), osbuild.NewBootISOMonoStagePipelineTreeInputs("anaconda-tree")))
 	p.AddStage(osbuild.NewKickstartStage(ksOptions))
 	p.AddStage(osbuild.NewDiscinfoStage(discinfoStageOptions(arch)))
 
@@ -937,7 +936,7 @@ func bootISOPipeline(filename, isolabel, arch string, isolinux bool) *osbuild.Pi
 	p.Name = "bootiso"
 	p.Build = "name:build"
 
-	p.AddStage(osbuild.NewXorrisofsStage(xorrisofsStageOptions(filename, isolabel, arch, isolinux), xorrisofsStageInputs("bootiso-tree")))
+	p.AddStage(osbuild.NewXorrisofsStage(xorrisofsStageOptions(filename, isolabel, arch, isolinux), osbuild.NewXorrisofsStagePipelineTreeInputs("bootiso-tree")))
 	p.AddStage(osbuild.NewImplantisomd5Stage(&osbuild.Implantisomd5StageOptions{Filename: filename}))
 
 	return p
@@ -959,7 +958,7 @@ func liveImagePipeline(inputPipelineName string, outputFilename string, pt *disk
 
 	inputName := "root-tree"
 	copyOptions, copyDevices, copyMounts := copyFSTreeOptions(inputName, inputPipelineName, pt, loopback)
-	copyInputs := copyPipelineTreeInputs(inputName, inputPipelineName)
+	copyInputs := osbuild.NewCopyStagePipelineTreeInputs(inputName, inputPipelineName)
 	p.AddStage(osbuild.NewCopyStage(copyOptions, copyInputs, copyDevices, copyMounts))
 	p.AddStage(bootloaderInstStage(outputFilename, pt, arch, kernelVer, copyDevices, copyMounts, loopback))
 	return p
@@ -1040,7 +1039,7 @@ func qemuPipeline(inputPipelineName, inputFilename, outputFilename, format, qcow
 	p.Name = format
 	p.Build = "name:build"
 
-	qemuStage := osbuild.NewQEMUStage(qemuStageOptions(outputFilename, format, qcow2Compat), qemuStageInputs(inputPipelineName, inputFilename))
+	qemuStage := osbuild.NewQEMUStage(qemuStageOptions(outputFilename, format, qcow2Compat), osbuild.NewQemuStagePipelineFilesInputs(inputPipelineName, inputFilename))
 	p.AddStage(qemuStage)
 	return p
 }
@@ -1072,19 +1071,3 @@ func bootloaderInstStage(filename string, pt *disk.PartitionTable, arch *archite
 
 	return nil
 }
-
-func kernelVerStr(pkgs []rpmmd.PackageSpec, kernelName, arch string) string {
-	kernelPkg := new(rpmmd.PackageSpec)
-	for _, pkg := range pkgs {
-		if pkg.Name == kernelName {
-			// Implicit memory alasing doesn't couse any bug in this case
-			/* #nosec G601 */
-			kernelPkg = &pkg
-			break
-		}
-	}
-	if kernelPkg == nil {
-		panic(fmt.Sprintf("kernel package %q not found", kernelName))
-	}
-	return fmt.Sprintf("%s-%s.%s", kernelPkg.Version, kernelPkg.Release, kernelPkg.Arch)
-}
diff --git a/internal/distro/rhel86/stage_inputs.go b/internal/distro/rhel86/stage_inputs.go
deleted file mode 100644
index 206c6bc7e..000000000
--- a/internal/distro/rhel86/stage_inputs.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package rhel86
-
-import (
-	osbuild "github.com/osbuild/osbuild-composer/internal/osbuild2"
-	"github.com/osbuild/osbuild-composer/internal/rpmmd"
-)
-
-func bootISOMonoStageInputs() *osbuild.BootISOMonoStageInputs {
-	rootfsInput := new(osbuild.BootISOMonoStageInput)
-	rootfsInput.Type = "org.osbuild.tree"
-	rootfsInput.Origin = "org.osbuild.pipeline"
-	rootfsInput.References = osbuild.BootISOMonoStageReferences{"name:anaconda-tree"}
-	return &osbuild.BootISOMonoStageInputs{
-		RootFS: rootfsInput,
-	}
-}
-
-func rpmStageInputs(specs []rpmmd.PackageSpec) *osbuild.RPMStageInputs {
-	stageInput := new(osbuild.RPMStageInput)
-	stageInput.Type = "org.osbuild.files"
-	stageInput.Origin = "org.osbuild.source"
-	stageInput.References = pkgRefs(specs)
-	return &osbuild.RPMStageInputs{Packages: stageInput}
-}
-
-func pkgRefs(specs []rpmmd.PackageSpec) osbuild.RPMStageReferences {
-	refs := make([]string, len(specs))
-	for idx, pkg := range specs {
-		refs[idx] = pkg.Checksum
-	}
-	return refs
-}
-
-func ostreePullStageInputs(origin, source, commitRef string) *osbuild.OSTreePullStageInputs {
-	pullStageInput := new(osbuild.OSTreePullStageInput)
-	pullStageInput.Type = "org.osbuild.ostree"
-	pullStageInput.Origin = origin
-
-	inputRefs := make(map[string]osbuild.OSTreePullStageReference)
-	inputRefs[source] = osbuild.OSTreePullStageReference{Ref: commitRef}
-	pullStageInput.References = inputRefs
-	return &osbuild.OSTreePullStageInputs{Commits: pullStageInput}
-}
-
-func xorrisofsStageInputs(pipeline string) *osbuild.XorrisofsStageInputs {
-	input := new(osbuild.XorrisofsStageInput)
-	input.Type = "org.osbuild.tree"
-	input.Origin = "org.osbuild.pipeline"
-	input.References = osbuild.XorrisofsStageReferences{"name:" + pipeline}
-	return &osbuild.XorrisofsStageInputs{Tree: input}
-}
-
-func copyPipelineTreeInputs(name, inputPipeline string) *osbuild.CopyStageInputs {
-	inputName := name
-	treeInput := osbuild.CopyStageInput{}
-	treeInput.Type = "org.osbuild.tree"
-	treeInput.Origin = "org.osbuild.pipeline"
-	treeInput.References = []string{"name:" + inputPipeline}
-	return &osbuild.CopyStageInputs{inputName: treeInput}
-}
-
-func qemuStageInputs(stage, file string) *osbuild.QEMUStageInputs {
-	stageKey := "name:" + stage
-	ref := map[string]osbuild.QEMUFile{
-		stageKey: {
-			File: file,
-		},
-	}
-	input := new(osbuild.QEMUStageInput)
-	input.Type = "org.osbuild.files"
-	input.Origin = "org.osbuild.pipeline"
-	input.References = ref
-	return &osbuild.QEMUStageInputs{Image: input}
-}
diff --git a/internal/distro/rhel86/stage_options.go b/internal/distro/rhel86/stage_options.go
index 7d242f9f4..aa2d8c9c0 100644
--- a/internal/distro/rhel86/stage_options.go
+++ b/internal/distro/rhel86/stage_options.go
@@ -573,13 +573,6 @@ func qemuStageOptions(filename, format, compat string) *osbuild.QEMUStageOptions
 	}
 }
 
-func kernelCmdlineStageOptions(rootUUID string, kernelOptions string) *osbuild.KernelCmdlineStageOptions {
-	return &osbuild.KernelCmdlineStageOptions{
-		RootFsUUID: rootUUID,
-		KernelOpts: kernelOptions,
-	}
-}
-
 func nginxConfigStageOptions(path, htmlRoot, listen string) *osbuild.NginxConfigStageOptions {
 	// configure nginx to work in an unprivileged container
 	cfg := &osbuild.NginxConfig{
diff --git a/internal/distro/rhel90/pipelines.go b/internal/distro/rhel90/pipelines.go
index e9dddb138..bc46e9a56 100644
--- a/internal/distro/rhel90/pipelines.go
+++ b/internal/distro/rhel90/pipelines.go
@@ -31,7 +31,7 @@ func qcow2Pipelines(t *imageType, customizations *blueprint.Customizations, opti
 	pipelines = append(pipelines, *treePipeline)
 
 	diskfile := "disk.img"
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, &partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 
@@ -43,7 +43,7 @@ func qcow2Pipelines(t *imageType, customizations *blueprint.Customizations, opti
 
 func prependKernelCmdlineStage(pipeline *osbuild.Pipeline, t *imageType, pt *disk.PartitionTable) *osbuild.Pipeline {
 	rootFsUUID := pt.RootPartition().Filesystem.UUID
-	kernelStage := osbuild.NewKernelCmdlineStage(kernelCmdlineStageOptions(rootFsUUID, t.kernelOptions))
+	kernelStage := osbuild.NewKernelCmdlineStage(osbuild.NewKernelCmdlineStageOptions(rootFsUUID, t.kernelOptions))
 	pipeline.Stages = append([]*osbuild.Stage{kernelStage}, pipeline.Stages...)
 	return pipeline
 }
@@ -64,7 +64,7 @@ func vhdPipelines(t *imageType, customizations *blueprint.Customizations, option
 	pipelines = append(pipelines, *treePipeline)
 
 	diskfile := "disk.img"
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, &partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 
@@ -89,7 +89,7 @@ func vmdkPipelines(t *imageType, customizations *blueprint.Customizations, optio
 	pipelines = append(pipelines, *treePipeline)
 
 	diskfile := "disk.img"
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, &partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 
@@ -114,7 +114,7 @@ func openstackPipelines(t *imageType, customizations *blueprint.Customizations,
 	pipelines = append(pipelines, *treePipeline)
 
 	diskfile := "disk.img"
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, &partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 
@@ -140,7 +140,7 @@ func ec2CommonPipelines(t *imageType, customizations *blueprint.Customizations,
 	}
 	pipelines = append(pipelines, *treePipeline)
 
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	imagePipeline := liveImagePipeline(treePipeline.Name, diskfile, &partitionTable, t.arch, kernelVer)
 	pipelines = append(pipelines, *imagePipeline)
 	return pipelines, nil
@@ -197,7 +197,7 @@ func edgeInstallerPipelines(t *imageType, customizations *blueprint.Customizatio
 	installerPackages := packageSetSpecs[installerPkgsKey]
 	d := t.arch.distro
 	archName := t.arch.name
-	kernelVer := kernelVerStr(installerPackages, "kernel", archName)
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(installerPackages, "kernel")
 	ostreeRepoPath := "/ostree/repo"
 	payloadStages := ostreePayloadStages(options, ostreeRepoPath)
 	kickstartOptions := ostreeKickstartStageOptions(makeISORootPath(ostreeRepoPath), options.OSTree.Ref)
@@ -333,7 +333,7 @@ func buildPipeline(repos []rpmmd.RepoConfig, buildPackageSpecs []rpmmd.PackageSp
 	p := new(osbuild.Pipeline)
 	p.Name = "build"
 	p.Runner = runner
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(buildPackageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(buildPackageSpecs)))
 	p.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(true)))
 	return p
 }
@@ -359,7 +359,7 @@ func osPipeline(t *imageType,
 		p.AddStage(osbuild.NewOSTreePasswdStage("org.osbuild.source", options.OSTree.Parent))
 	}
 
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 
 	// If the /boot is on a separate partition, the prefix for the BLS stage must be ""
 	if pt == nil || pt.BootPartition() == nil {
@@ -508,7 +508,7 @@ func osPipeline(t *imageType,
 	if pt != nil {
 		p = prependKernelCmdlineStage(p, t, pt)
 		p.AddStage(osbuild.NewFSTabStage(pt.FSTabStageOptionsV2()))
-		kernelVer := kernelVerStr(bpPackages, c.GetKernel().Name, t.Arch().Name())
+		kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(bpPackages, c.GetKernel().Name)
 		p.AddStage(bootloaderConfigStage(t, *pt, c.GetKernel(), kernelVer, false, false))
 	}
 
@@ -560,7 +560,7 @@ func containerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpe
 	p := new(osbuild.Pipeline)
 	p.Name = "container-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	language, _ := c.GetPrimaryLocale()
 	if language != nil {
 		p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: *language}))
@@ -574,7 +574,7 @@ func containerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpe
 
 	p.AddStage(osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: repoPath},
-		ostreePullStageInputs("org.osbuild.pipeline", "name:ostree-commit", options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.pipeline", "name:ostree-commit", options.OSTree.Ref),
 	))
 
 	// make nginx log and lib directories world writeable, otherwise nginx can't start in
@@ -614,7 +614,7 @@ func ostreePayloadStages(options distro.ImageOptions, ostreeRepoPath string) []*
 	stages = append(stages, osbuild.NewOSTreeInitStage(&osbuild.OSTreeInitStageOptions{Path: ostreeRepoPath}))
 	stages = append(stages, osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: ostreeRepoPath},
-		ostreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
 	))
 
 	return stages
@@ -624,7 +624,7 @@ func edgeSimplifiedInstallerPipelines(t *imageType, customizations *blueprint.Cu
 	pipelines := make([]osbuild.Pipeline, 0)
 	pipelines = append(pipelines, *buildPipeline(repos, packageSetSpecs[buildPkgsKey], t.arch.distro.runner))
 	installerPackages := packageSetSpecs[installerPkgsKey]
-	kernelVer := kernelVerStr(installerPackages, "kernel", t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(installerPackages, "kernel")
 	imgName := "disk.img.xz"
 	installDevice := customizations.GetInstallationDevice()
 
@@ -713,12 +713,12 @@ func simplifiedInstallerBootISOTreePipeline(archivePipelineName, kver string, rn
 	}
 
 	inputName := "root-tree"
-	copyInputs := copyPipelineTreeInputs(inputName, "efiboot-tree")
+	copyInputs := osbuild.NewCopyStagePipelineTreeInputs(inputName, "efiboot-tree")
 	copyOptions, copyDevices, copyMounts := copyFSTreeOptions(inputName, "efiboot-tree", &pt, loopback)
 	p.AddStage(osbuild.NewCopyStage(copyOptions, copyInputs, copyDevices, copyMounts))
 
 	inputName = "coi"
-	copyInputs = copyPipelineTreeInputs(inputName, "coi-tree")
+	copyInputs = osbuild.NewCopyStagePipelineTreeInputs(inputName, "coi-tree")
 	p.AddStage(osbuild.NewCopyStageSimple(
 		&osbuild.CopyStageOptions{
 			Paths: []osbuild.CopyStagePath{
@@ -736,7 +736,7 @@ func simplifiedInstallerBootISOTreePipeline(archivePipelineName, kver string, rn
 	))
 
 	inputName = "efi-tree"
-	copyInputs = copyPipelineTreeInputs(inputName, "efiboot-tree")
+	copyInputs = osbuild.NewCopyStagePipelineTreeInputs(inputName, "efiboot-tree")
 	p.AddStage(osbuild.NewCopyStageSimple(
 		&osbuild.CopyStageOptions{
 			Paths: []osbuild.CopyStagePath{
@@ -764,7 +764,7 @@ func simplifiedInstallerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.
 	p := new(osbuild.Pipeline)
 	p.Name = "coi-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	p.AddStage(osbuild.NewBuildstampStage(buildStampStageOptions(arch, product, osVersion, variant)))
 	p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: "en_US.UTF-8"}))
 	p.AddStage(osbuild.NewDracutStage(dracutStageOptions(kernelVer, arch, []string{"coreos-installer"})))
@@ -791,7 +791,7 @@ func ostreeDeployPipeline(
 	p.AddStage(osbuild.OSTreeInitFsStage())
 	p.AddStage(osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: repoPath, Remote: remote},
-		ostreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
 	))
 	p.AddStage(osbuild.NewOSTreeOsInitStage(
 		&osbuild.OSTreeOsInitStageOptions{
@@ -867,7 +867,7 @@ func anacondaTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec
 	p := new(osbuild.Pipeline)
 	p.Name = "anaconda-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	p.AddStage(osbuild.NewBuildstampStage(buildStampStageOptions(arch, product, osVersion, variant)))
 	p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: "en_US.UTF-8"}))
 
@@ -918,7 +918,7 @@ func bootISOTreePipeline(kernelVer, arch, vendor, product, osVersion, isolabel s
 	p.Name = "bootiso-tree"
 	p.Build = "name:build"
 
-	p.AddStage(osbuild.NewBootISOMonoStage(bootISOMonoStageOptions(kernelVer, arch, vendor, product, osVersion, isolabel), bootISOMonoStageInputs()))
+	p.AddStage(osbuild.NewBootISOMonoStage(bootISOMonoStageOptions(kernelVer, arch, vendor, product, osVersion, isolabel), osbuild.NewBootISOMonoStagePipelineTreeInputs("anaconda-tree")))
 	p.AddStage(osbuild.NewKickstartStage(ksOptions))
 	p.AddStage(osbuild.NewDiscinfoStage(discinfoStageOptions(arch)))
 
@@ -933,7 +933,7 @@ func bootISOPipeline(filename, isolabel, arch string, isolinux bool) *osbuild.Pi
 	p.Name = "bootiso"
 	p.Build = "name:build"
 
-	p.AddStage(osbuild.NewXorrisofsStage(xorrisofsStageOptions(filename, isolabel, arch, isolinux), xorrisofsStageInputs("bootiso-tree")))
+	p.AddStage(osbuild.NewXorrisofsStage(xorrisofsStageOptions(filename, isolabel, arch, isolinux), osbuild.NewXorrisofsStagePipelineTreeInputs("bootiso-tree")))
 	p.AddStage(osbuild.NewImplantisomd5Stage(&osbuild.Implantisomd5StageOptions{Filename: filename}))
 
 	return p
@@ -955,7 +955,7 @@ func liveImagePipeline(inputPipelineName string, outputFilename string, pt *disk
 
 	inputName := "root-tree"
 	copyOptions, copyDevices, copyMounts := copyFSTreeOptions(inputName, inputPipelineName, pt, loopback)
-	copyInputs := copyPipelineTreeInputs(inputName, inputPipelineName)
+	copyInputs := osbuild.NewCopyStagePipelineTreeInputs(inputName, inputPipelineName)
 	p.AddStage(osbuild.NewCopyStage(copyOptions, copyInputs, copyDevices, copyMounts))
 	p.AddStage(bootloaderInstStage(outputFilename, pt, arch, kernelVer, copyDevices, copyMounts, loopback))
 	return p
@@ -1036,7 +1036,7 @@ func qemuPipeline(inputPipelineName, inputFilename, outputFilename, format, qcow
 	p.Name = format
 	p.Build = "name:build"
 
-	qemuStage := osbuild.NewQEMUStage(qemuStageOptions(outputFilename, format, qcow2Compat), qemuStageInputs(inputPipelineName, inputFilename))
+	qemuStage := osbuild.NewQEMUStage(qemuStageOptions(outputFilename, format, qcow2Compat), osbuild.NewQemuStagePipelineFilesInputs(inputPipelineName, inputFilename))
 	p.AddStage(qemuStage)
 	return p
 }
@@ -1068,19 +1068,3 @@ func bootloaderInstStage(filename string, pt *disk.PartitionTable, arch *archite
 
 	return nil
 }
-
-func kernelVerStr(pkgs []rpmmd.PackageSpec, kernelName, arch string) string {
-	kernelPkg := new(rpmmd.PackageSpec)
-	for _, pkg := range pkgs {
-		if pkg.Name == kernelName {
-			// Implicit memory alasing doesn't couse any bug in this case
-			/* #nosec G601 */
-			kernelPkg = &pkg
-			break
-		}
-	}
-	if kernelPkg == nil {
-		panic(fmt.Sprintf("kernel package %q not found", kernelName))
-	}
-	return fmt.Sprintf("%s-%s.%s", kernelPkg.Version, kernelPkg.Release, kernelPkg.Arch)
-}
diff --git a/internal/distro/rhel90/stage_inputs.go b/internal/distro/rhel90/stage_inputs.go
deleted file mode 100644
index c1b27e311..000000000
--- a/internal/distro/rhel90/stage_inputs.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package rhel90
-
-import (
-	osbuild "github.com/osbuild/osbuild-composer/internal/osbuild2"
-	"github.com/osbuild/osbuild-composer/internal/rpmmd"
-)
-
-func bootISOMonoStageInputs() *osbuild.BootISOMonoStageInputs {
-	rootfsInput := new(osbuild.BootISOMonoStageInput)
-	rootfsInput.Type = "org.osbuild.tree"
-	rootfsInput.Origin = "org.osbuild.pipeline"
-	rootfsInput.References = osbuild.BootISOMonoStageReferences{"name:anaconda-tree"}
-	return &osbuild.BootISOMonoStageInputs{
-		RootFS: rootfsInput,
-	}
-}
-
-func rpmStageInputs(specs []rpmmd.PackageSpec) *osbuild.RPMStageInputs {
-	stageInput := new(osbuild.RPMStageInput)
-	stageInput.Type = "org.osbuild.files"
-	stageInput.Origin = "org.osbuild.source"
-	stageInput.References = pkgRefs(specs)
-	return &osbuild.RPMStageInputs{Packages: stageInput}
-}
-
-func pkgRefs(specs []rpmmd.PackageSpec) osbuild.RPMStageReferences {
-	refs := make([]string, len(specs))
-	for idx, pkg := range specs {
-		refs[idx] = pkg.Checksum
-	}
-	return refs
-}
-
-func ostreePullStageInputs(origin, source, commitRef string) *osbuild.OSTreePullStageInputs {
-	pullStageInput := new(osbuild.OSTreePullStageInput)
-	pullStageInput.Type = "org.osbuild.ostree"
-	pullStageInput.Origin = origin
-
-	inputRefs := make(map[string]osbuild.OSTreePullStageReference)
-	inputRefs[source] = osbuild.OSTreePullStageReference{Ref: commitRef}
-	pullStageInput.References = inputRefs
-	return &osbuild.OSTreePullStageInputs{Commits: pullStageInput}
-}
-
-func xorrisofsStageInputs(pipeline string) *osbuild.XorrisofsStageInputs {
-	input := new(osbuild.XorrisofsStageInput)
-	input.Type = "org.osbuild.tree"
-	input.Origin = "org.osbuild.pipeline"
-	input.References = osbuild.XorrisofsStageReferences{"name:" + pipeline}
-	return &osbuild.XorrisofsStageInputs{Tree: input}
-}
-
-func copyPipelineTreeInputs(name, inputPipeline string) *osbuild.CopyStageInputs {
-	inputName := name
-	treeInput := osbuild.CopyStageInput{}
-	treeInput.Type = "org.osbuild.tree"
-	treeInput.Origin = "org.osbuild.pipeline"
-	treeInput.References = []string{"name:" + inputPipeline}
-	return &osbuild.CopyStageInputs{inputName: treeInput}
-}
-
-func qemuStageInputs(stage, file string) *osbuild.QEMUStageInputs {
-	stageKey := "name:" + stage
-	ref := map[string]osbuild.QEMUFile{
-		stageKey: {
-			File: file,
-		},
-	}
-	input := new(osbuild.QEMUStageInput)
-	input.Type = "org.osbuild.files"
-	input.Origin = "org.osbuild.pipeline"
-	input.References = ref
-	return &osbuild.QEMUStageInputs{Image: input}
-}
diff --git a/internal/distro/rhel90/stage_options.go b/internal/distro/rhel90/stage_options.go
index 3774b15d4..9095b4bec 100644
--- a/internal/distro/rhel90/stage_options.go
+++ b/internal/distro/rhel90/stage_options.go
@@ -574,13 +574,6 @@ func qemuStageOptions(filename, format, compat string) *osbuild.QEMUStageOptions
 	}
 }
 
-func kernelCmdlineStageOptions(rootUUID string, kernelOptions string) *osbuild.KernelCmdlineStageOptions {
-	return &osbuild.KernelCmdlineStageOptions{
-		RootFsUUID: rootUUID,
-		KernelOpts: kernelOptions,
-	}
-}
-
 func nginxConfigStageOptions(path, htmlRoot, listen string) *osbuild.NginxConfigStageOptions {
 	// configure nginx to work in an unprivileged container
 	cfg := &osbuild.NginxConfig{
diff --git a/internal/distro/rhel90beta/pipelines.go b/internal/distro/rhel90beta/pipelines.go
index 17b5a6d88..f9d95338f 100644
--- a/internal/distro/rhel90beta/pipelines.go
+++ b/internal/distro/rhel90beta/pipelines.go
@@ -9,7 +9,6 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/common"
 	"github.com/osbuild/osbuild-composer/internal/disk"
 	"github.com/osbuild/osbuild-composer/internal/distro"
-	"github.com/osbuild/osbuild-composer/internal/osbuild2"
 	osbuild "github.com/osbuild/osbuild-composer/internal/osbuild2"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
@@ -44,7 +43,7 @@ func qcow2Pipelines(t *imageType, customizations *blueprint.Customizations, opti
 		}))
 	}
 	treePipeline.AddStage(osbuild.NewFSTabStage(partitionTable.FSTabStageOptionsV2()))
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	treePipeline.AddStage(bootloaderConfigStage(t, partitionTable, customizations.GetKernel(), kernelVer))
 	treePipeline.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
 	pipelines = append(pipelines, *treePipeline)
@@ -61,7 +60,7 @@ func qcow2Pipelines(t *imageType, customizations *blueprint.Customizations, opti
 
 func prependKernelCmdlineStage(pipeline *osbuild.Pipeline, t *imageType, pt *disk.PartitionTable) *osbuild.Pipeline {
 	rootFsUUID := pt.RootPartition().Filesystem.UUID
-	kernelStage := osbuild.NewKernelCmdlineStage(kernelCmdlineStageOptions(rootFsUUID, t.kernelOptions))
+	kernelStage := osbuild.NewKernelCmdlineStage(osbuild.NewKernelCmdlineStageOptions(rootFsUUID, t.kernelOptions))
 	pipeline.Stages = append([]*osbuild.Stage{kernelStage}, pipeline.Stages...)
 	return pipeline
 }
@@ -81,7 +80,7 @@ func vhdPipelines(t *imageType, customizations *blueprint.Customizations, option
 
 	treePipeline = prependKernelCmdlineStage(treePipeline, t, &partitionTable)
 	treePipeline.AddStage(osbuild.NewFSTabStage(partitionTable.FSTabStageOptionsV2()))
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	treePipeline.AddStage(bootloaderConfigStage(t, partitionTable, customizations.GetKernel(), kernelVer))
 	treePipeline.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
 	pipelines = append(pipelines, *treePipeline)
@@ -110,7 +109,7 @@ func vmdkPipelines(t *imageType, customizations *blueprint.Customizations, optio
 
 	treePipeline = prependKernelCmdlineStage(treePipeline, t, &partitionTable)
 	treePipeline.AddStage(osbuild.NewFSTabStage(partitionTable.FSTabStageOptionsV2()))
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	treePipeline.AddStage(bootloaderConfigStage(t, partitionTable, customizations.GetKernel(), kernelVer))
 	treePipeline.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
 	pipelines = append(pipelines, *treePipeline)
@@ -138,7 +137,7 @@ func openstackPipelines(t *imageType, customizations *blueprint.Customizations,
 	}
 	treePipeline = prependKernelCmdlineStage(treePipeline, t, &partitionTable)
 	treePipeline.AddStage(osbuild.NewFSTabStage(partitionTable.FSTabStageOptionsV2()))
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	treePipeline.AddStage(bootloaderConfigStage(t, partitionTable, customizations.GetKernel(), kernelVer))
 	treePipeline.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
 	pipelines = append(pipelines, *treePipeline)
@@ -169,7 +168,7 @@ func ec2BaseTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec,
 	p.Name = "os"
 	p.Build = "name:build"
 	packages = append(packages, bpPackages...)
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 
 	// If the /boot is on a separate partition, the prefix for the BLS stage must be ""
 	if pt.BootPartition() == nil {
@@ -412,7 +411,7 @@ func ec2CommonPipelines(t *imageType, customizations *blueprint.Customizations,
 
 	treePipeline = prependKernelCmdlineStage(treePipeline, t, &partitionTable)
 	treePipeline.AddStage(osbuild.NewFSTabStage(partitionTable.FSTabStageOptionsV2()))
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	treePipeline.AddStage(bootloaderConfigStage(t, partitionTable, customizations.GetKernel(), kernelVer))
 	// The last stage must be the SELinux stage
 	treePipeline.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
@@ -553,7 +552,7 @@ func ec2SapPipelines(t *imageType, customizations *blueprint.Customizations, opt
 
 	treePipeline = prependKernelCmdlineStage(treePipeline, t, &partitionTable)
 	treePipeline.AddStage(osbuild.NewFSTabStage(partitionTable.FSTabStageOptionsV2()))
-	kernelVer := kernelVerStr(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name, t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(packageSetSpecs[blueprintPkgsKey], customizations.GetKernel().Name)
 	treePipeline.AddStage(bootloaderConfigStage(t, partitionTable, customizations.GetKernel(), kernelVer))
 	// The last stage must be the SELinux stage
 	treePipeline.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(false)))
@@ -622,7 +621,7 @@ func edgeInstallerPipelines(t *imageType, customizations *blueprint.Customizatio
 	pipelines := make([]osbuild.Pipeline, 0)
 	pipelines = append(pipelines, *buildPipeline(repos, packageSetSpecs[buildPkgsKey]))
 	installerPackages := packageSetSpecs[installerPkgsKey]
-	kernelVer := kernelVerStr(installerPackages, "kernel", t.Arch().Name())
+	kernelVer := rpmmd.GetVerStrFromPackageSpecListPanic(installerPackages, "kernel")
 	ostreeRepoPath := "/ostree/repo"
 	pipelines = append(pipelines, *anacondaTreePipeline(repos, installerPackages, kernelVer, t.Arch().Name(), ostreePayloadStages(options, ostreeRepoPath)))
 	pipelines = append(pipelines, *bootISOTreePipeline(kernelVer, t.Arch().Name(), ostreeKickstartStageOptions(fmt.Sprintf("file://%s", ostreeRepoPath), options.OSTree.Ref)))
@@ -707,7 +706,7 @@ func buildPipeline(repos []rpmmd.RepoConfig, buildPackageSpecs []rpmmd.PackageSp
 	p := new(osbuild.Pipeline)
 	p.Name = "build"
 	p.Runner = "org.osbuild.rhel90"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(buildPackageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(buildPackageSpecs)))
 	p.AddStage(osbuild.NewSELinuxStage(selinuxStageOptions(true)))
 	return p
 }
@@ -717,7 +716,7 @@ func osPipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec, bpPackag
 	p.Name = "os"
 	p.Build = "name:build"
 	packages = append(packages, bpPackages...)
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	p.AddStage(osbuild.NewFixBLSStage(&osbuild.FixBLSStageOptions{}))
 	language, keyboard := c.GetPrimaryLocale()
 	if language != nil {
@@ -798,7 +797,7 @@ func ostreeTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec,
 	p.Build = "name:build"
 
 	packages = append(packages, bpPackages...)
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	p.AddStage(osbuild.NewFixBLSStage(&osbuild.FixBLSStageOptions{}))
 	language, keyboard := c.GetPrimaryLocale()
 	if language != nil {
@@ -915,7 +914,7 @@ func containerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpe
 	p := new(osbuild.Pipeline)
 	p.Name = "container-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	language, _ := c.GetPrimaryLocale()
 	if language != nil {
 		p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{Language: *language}))
@@ -926,7 +925,7 @@ func containerTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpe
 
 	p.AddStage(osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: "/var/www/html/repo"},
-		ostreePullStageInputs("org.osbuild.pipeline", "name:ostree-commit", options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.pipeline", "name:ostree-commit", options.OSTree.Ref),
 	))
 	return p
 }
@@ -959,7 +958,7 @@ func ostreePayloadStages(options distro.ImageOptions, ostreeRepoPath string) []*
 	stages = append(stages, osbuild.NewOSTreeInitStage(&osbuild.OSTreeInitStageOptions{Path: ostreeRepoPath}))
 	stages = append(stages, osbuild.NewOSTreePullStage(
 		&osbuild.OSTreePullStageOptions{Repo: ostreeRepoPath},
-		ostreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
+		osbuild.NewOstreePullStageInputs("org.osbuild.source", options.OSTree.Parent, options.OSTree.Ref),
 	))
 
 	return stages
@@ -969,7 +968,7 @@ func anacondaTreePipeline(repos []rpmmd.RepoConfig, packages []rpmmd.PackageSpec
 	p := new(osbuild.Pipeline)
 	p.Name = "anaconda-tree"
 	p.Build = "name:build"
-	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), rpmStageInputs(packages)))
+	p.AddStage(osbuild.NewRPMStage(rpmStageOptions(repos), osbuild.NewRpmStageSourceFilesInputs(packages)))
 	for _, stage := range payloadStages {
 		p.AddStage(stage)
 	}
@@ -1013,7 +1012,7 @@ func bootISOTreePipeline(kernelVer string, arch string, ksOptions *osbuild.Kicks
 	p.Name = "bootiso-tree"
 	p.Build = "name:build"
 
-	p.AddStage(osbuild.NewBootISOMonoStage(bootISOMonoStageOptions(kernelVer, arch), bootISOMonoStageInputs()))
+	p.AddStage(osbuild.NewBootISOMonoStage(bootISOMonoStageOptions(kernelVer, arch), osbuild.NewBootISOMonoStagePipelineTreeInputs("anaconda-tree")))
 	p.AddStage(osbuild.NewKickstartStage(ksOptions))
 	p.AddStage(osbuild.NewDiscinfoStage(discinfoStageOptions(arch)))
 
@@ -1024,7 +1023,7 @@ func bootISOPipeline(filename string, arch string) *osbuild.Pipeline {
 	p.Name = "bootiso"
 	p.Build = "name:build"
 
-	p.AddStage(osbuild.NewXorrisofsStage(xorrisofsStageOptions(filename, arch), xorrisofsStageInputs()))
+	p.AddStage(osbuild.NewXorrisofsStage(xorrisofsStageOptions(filename, arch), osbuild.NewXorrisofsStagePipelineTreeInputs("bootiso-tree")))
 	p.AddStage(osbuild.NewImplantisomd5Stage(&osbuild.Implantisomd5StageOptions{Filename: filename}))
 
 	return p
@@ -1046,7 +1045,7 @@ func liveImagePipeline(inputPipelineName string, outputFilename string, pt *disk
 
 	inputName := "root-tree"
 	copyOptions, copyDevices, copyMounts := copyFSTreeOptions(inputName, inputPipelineName, pt, loopback)
-	copyInputs := copyPipelineTreeInputs(inputName, inputPipelineName)
+	copyInputs := osbuild.NewCopyStagePipelineTreeInputs(inputName, inputPipelineName)
 	p.AddStage(osbuild.NewCopyStage(copyOptions, copyInputs, copyDevices, copyMounts))
 	p.AddStage(bootloaderInstStage(outputFilename, pt, arch, kernelVer, copyDevices, copyMounts, loopback))
 	return p
@@ -1067,8 +1066,8 @@ func xzArchivePipeline(inputPipelineName, inputFilename, outputFilename string)
 
 // mkfsStages generates a list of org.osbuild.mkfs.* stages based on a
 // partition table description for a single device node
-func mkfsStages(pt *disk.PartitionTable, device *osbuild.Device) []*osbuild2.Stage {
-	stages := make([]*osbuild2.Stage, 0, len(pt.Partitions))
+func mkfsStages(pt *disk.PartitionTable, device *osbuild.Device) []*osbuild.Stage {
+	stages := make([]*osbuild.Stage, 0, len(pt.Partitions))
 
 	// assume loopback device for simplicity since it's the only one currently supported
 	// panic if the conversion fails
@@ -1127,7 +1126,7 @@ func qemuPipeline(inputPipelineName, inputFilename, outputFilename, format, qcow
 	p.Name = format
 	p.Build = "name:build"
 
-	qemuStage := osbuild.NewQEMUStage(qemuStageOptions(outputFilename, format, qcow2Compat), qemuStageInputs(inputPipelineName, inputFilename))
+	qemuStage := osbuild.NewQEMUStage(qemuStageOptions(outputFilename, format, qcow2Compat), osbuild.NewQemuStagePipelineFilesInputs(inputPipelineName, inputFilename))
 	p.AddStage(qemuStage)
 	return p
 }
@@ -1155,19 +1154,3 @@ func bootloaderInstStage(filename string, pt *disk.PartitionTable, arch *archite
 
 	return nil
 }
-
-func kernelVerStr(pkgs []rpmmd.PackageSpec, kernelName, arch string) string {
-	kernelPkg := new(rpmmd.PackageSpec)
-	for _, pkg := range pkgs {
-		if pkg.Name == kernelName {
-			// Implicit memory alasing doesn't couse any bug in this case
-			/* #nosec G601 */
-			kernelPkg = &pkg
-			break
-		}
-	}
-	if kernelPkg == nil {
-		panic(fmt.Sprintf("kernel package %q not found", kernelName))
-	}
-	return fmt.Sprintf("%s-%s.%s", kernelPkg.Version, kernelPkg.Release, kernelPkg.Arch)
-}
diff --git a/internal/distro/rhel90beta/stage_inputs.go b/internal/distro/rhel90beta/stage_inputs.go
deleted file mode 100644
index 19ca6e8d9..000000000
--- a/internal/distro/rhel90beta/stage_inputs.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package rhel90beta
-
-import (
-	osbuild "github.com/osbuild/osbuild-composer/internal/osbuild2"
-	"github.com/osbuild/osbuild-composer/internal/rpmmd"
-)
-
-func bootISOMonoStageInputs() *osbuild.BootISOMonoStageInputs {
-	rootfsInput := new(osbuild.BootISOMonoStageInput)
-	rootfsInput.Type = "org.osbuild.tree"
-	rootfsInput.Origin = "org.osbuild.pipeline"
-	rootfsInput.References = osbuild.BootISOMonoStageReferences{"name:anaconda-tree"}
-	return &osbuild.BootISOMonoStageInputs{
-		RootFS: rootfsInput,
-	}
-}
-
-func rpmStageInputs(specs []rpmmd.PackageSpec) *osbuild.RPMStageInputs {
-	stageInput := new(osbuild.RPMStageInput)
-	stageInput.Type = "org.osbuild.files"
-	stageInput.Origin = "org.osbuild.source"
-	stageInput.References = pkgRefs(specs)
-	return &osbuild.RPMStageInputs{Packages: stageInput}
-}
-
-func pkgRefs(specs []rpmmd.PackageSpec) osbuild.RPMStageReferences {
-	refs := make([]string, len(specs))
-	for idx, pkg := range specs {
-		refs[idx] = pkg.Checksum
-	}
-	return refs
-}
-
-func ostreePullStageInputs(origin, source, commitRef string) *osbuild.OSTreePullStageInputs {
-	pullStageInput := new(osbuild.OSTreePullStageInput)
-	pullStageInput.Type = "org.osbuild.ostree"
-	pullStageInput.Origin = origin
-
-	inputRefs := make(map[string]osbuild.OSTreePullStageReference)
-	inputRefs[source] = osbuild.OSTreePullStageReference{Ref: commitRef}
-	pullStageInput.References = inputRefs
-	return &osbuild.OSTreePullStageInputs{Commits: pullStageInput}
-}
-
-func xorrisofsStageInputs() *osbuild.XorrisofsStageInputs {
-	input := new(osbuild.XorrisofsStageInput)
-	input.Type = "org.osbuild.tree"
-	input.Origin = "org.osbuild.pipeline"
-	input.References = osbuild.XorrisofsStageReferences{"name:bootiso-tree"}
-	return &osbuild.XorrisofsStageInputs{Tree: input}
-}
-
-func copyPipelineTreeInputs(name, inputPipeline string) *osbuild.CopyStageInputs {
-	inputName := "root-tree"
-	treeInput := osbuild.CopyStageInput{}
-	treeInput.Type = "org.osbuild.tree"
-	treeInput.Origin = "org.osbuild.pipeline"
-	treeInput.References = []string{"name:" + inputPipeline}
-	return &osbuild.CopyStageInputs{inputName: treeInput}
-}
-
-func qemuStageInputs(stage, file string) *osbuild.QEMUStageInputs {
-	stageKey := "name:" + stage
-	ref := map[string]osbuild.QEMUFile{
-		stageKey: {
-			File: file,
-		},
-	}
-	input := new(osbuild.QEMUStageInput)
-	input.Type = "org.osbuild.files"
-	input.Origin = "org.osbuild.pipeline"
-	input.References = ref
-	return &osbuild.QEMUStageInputs{Image: input}
-}
diff --git a/internal/distro/rhel90beta/stage_options.go b/internal/distro/rhel90beta/stage_options.go
index 46758f1bb..9da2960c6 100644
--- a/internal/distro/rhel90beta/stage_options.go
+++ b/internal/distro/rhel90beta/stage_options.go
@@ -505,10 +505,3 @@ func qemuStageOptions(filename, format, compat string) *osbuild.QEMUStageOptions
 		Format:   options,
 	}
 }
-
-func kernelCmdlineStageOptions(rootUUID string, kernelOptions string) *osbuild.KernelCmdlineStageOptions {
-	return &osbuild.KernelCmdlineStageOptions{
-		RootFsUUID: rootUUID,
-		KernelOpts: kernelOptions,
-	}
-}
diff --git a/internal/osbuild2/bootiso_stage.go b/internal/osbuild2/bootiso_stage.go
index 6420d8bdf..7c4f5f6d6 100644
--- a/internal/osbuild2/bootiso_stage.go
+++ b/internal/osbuild2/bootiso_stage.go
@@ -87,3 +87,13 @@ func NewBootISOMonoStage(options *BootISOMonoStageOptions, inputs *BootISOMonoSt
 		Inputs:  inputs,
 	}
 }
+
+func NewBootISOMonoStagePipelineTreeInputs(pipeline string) *BootISOMonoStageInputs {
+	rootfsInput := new(BootISOMonoStageInput)
+	rootfsInput.Type = "org.osbuild.tree"
+	rootfsInput.Origin = "org.osbuild.pipeline"
+	rootfsInput.References = BootISOMonoStageReferences{"name:" + pipeline}
+	return &BootISOMonoStageInputs{
+		RootFS: rootfsInput,
+	}
+}
diff --git a/internal/osbuild2/copy_stage.go b/internal/osbuild2/copy_stage.go
index 510ea3a67..8b8917f98 100644
--- a/internal/osbuild2/copy_stage.go
+++ b/internal/osbuild2/copy_stage.go
@@ -58,3 +58,11 @@ func NewCopyStageSimple(options *CopyStageOptions, inputs CopyStageInputsNew) *S
 		Inputs:  stageInputs,
 	}
 }
+
+func NewCopyStagePipelineTreeInputs(inputName, inputPipeline string) *CopyStageInputs {
+	treeInput := CopyStageInput{}
+	treeInput.Type = "org.osbuild.tree"
+	treeInput.Origin = "org.osbuild.pipeline"
+	treeInput.References = []string{"name:" + inputPipeline}
+	return &CopyStageInputs{inputName: treeInput}
+}
diff --git a/internal/osbuild2/kernel_cmdline_stage.go b/internal/osbuild2/kernel_cmdline_stage.go
index c75624486..45e5d791d 100644
--- a/internal/osbuild2/kernel_cmdline_stage.go
+++ b/internal/osbuild2/kernel_cmdline_stage.go
@@ -17,3 +17,10 @@ func NewKernelCmdlineStage(options *KernelCmdlineStageOptions) *Stage {
 		Options: options,
 	}
 }
+
+func NewKernelCmdlineStageOptions(rootUUID string, kernelOptions string) *KernelCmdlineStageOptions {
+	return &KernelCmdlineStageOptions{
+		RootFsUUID: rootUUID,
+		KernelOpts: kernelOptions,
+	}
+}
diff --git a/internal/osbuild2/ostree_pull_stage.go b/internal/osbuild2/ostree_pull_stage.go
index 048a4fa01..a2a762ae5 100644
--- a/internal/osbuild2/ostree_pull_stage.go
+++ b/internal/osbuild2/ostree_pull_stage.go
@@ -39,3 +39,14 @@ func NewOSTreePullStage(options *OSTreePullStageOptions, inputs *OSTreePullStage
 		Options: options,
 	}
 }
+
+func NewOstreePullStageInputs(origin, source, commitRef string) *OSTreePullStageInputs {
+	pullStageInput := new(OSTreePullStageInput)
+	pullStageInput.Type = "org.osbuild.ostree"
+	pullStageInput.Origin = origin
+
+	inputRefs := make(map[string]OSTreePullStageReference)
+	inputRefs[source] = OSTreePullStageReference{Ref: commitRef}
+	pullStageInput.References = inputRefs
+	return &OSTreePullStageInputs{Commits: pullStageInput}
+}
diff --git a/internal/osbuild2/qemu_stage.go b/internal/osbuild2/qemu_stage.go
index be863cdbe..8093babe5 100644
--- a/internal/osbuild2/qemu_stage.go
+++ b/internal/osbuild2/qemu_stage.go
@@ -105,3 +105,17 @@ func (options QEMUStageOptions) MarshalJSON() ([]byte, error) {
 
 	return json.Marshal(qemuStageOptions(options))
 }
+
+func NewQemuStagePipelineFilesInputs(stage, file string) *QEMUStageInputs {
+	stageKey := "name:" + stage
+	ref := map[string]QEMUFile{
+		stageKey: {
+			File: file,
+		},
+	}
+	input := new(QEMUStageInput)
+	input.Type = "org.osbuild.files"
+	input.Origin = "org.osbuild.pipeline"
+	input.References = ref
+	return &QEMUStageInputs{Image: input}
+}
diff --git a/internal/osbuild2/rpm_stage.go b/internal/osbuild2/rpm_stage.go
index ee2ef2d00..59b9bea40 100644
--- a/internal/osbuild2/rpm_stage.go
+++ b/internal/osbuild2/rpm_stage.go
@@ -1,5 +1,9 @@
 package osbuild2
 
+import (
+	"github.com/osbuild/osbuild-composer/internal/rpmmd"
+)
+
 type RPMStageOptions struct {
 	// Array of GPG key contents to import
 	GPGKeys []string `json:"gpgkeys,omitempty"`
@@ -70,3 +74,52 @@ type RPMPackageMetadata struct {
 }
 
 func (RPMStageMetadata) isStageMetadata() {}
+
+func OSBuildMetadataToRPMs(stagesMetadata map[string]StageMetadata) []rpmmd.RPM {
+	rpms := make([]rpmmd.RPM, 0)
+	for _, md := range stagesMetadata {
+		switch metadata := md.(type) {
+		case *RPMStageMetadata:
+			for _, pkg := range metadata.Packages {
+				rpms = append(rpms, rpmmd.RPM{
+					Type:      "rpm",
+					Name:      pkg.Name,
+					Epoch:     pkg.Epoch,
+					Version:   pkg.Version,
+					Release:   pkg.Release,
+					Arch:      pkg.Arch,
+					Sigmd5:    pkg.SigMD5,
+					Signature: RPMPackageMetadataToSignature(pkg),
+				})
+			}
+		default:
+			continue
+		}
+	}
+	return rpms
+}
+
+func RPMPackageMetadataToSignature(pkg RPMPackageMetadata) *string {
+	if pkg.SigGPG != "" {
+		return &pkg.SigGPG
+	} else if pkg.SigPGP != "" {
+		return &pkg.SigPGP
+	}
+	return nil
+}
+
+func NewRpmStageSourceFilesInputs(specs []rpmmd.PackageSpec) *RPMStageInputs {
+	stageInput := new(RPMStageInput)
+	stageInput.Type = "org.osbuild.files"
+	stageInput.Origin = "org.osbuild.source"
+	stageInput.References = pkgRefs(specs)
+	return &RPMStageInputs{Packages: stageInput}
+}
+
+func pkgRefs(specs []rpmmd.PackageSpec) RPMStageReferences {
+	refs := make([]string, len(specs))
+	for idx, pkg := range specs {
+		refs[idx] = pkg.Checksum
+	}
+	return refs
+}
diff --git a/internal/osbuild2/xorrisofs_stage.go b/internal/osbuild2/xorrisofs_stage.go
index 99724061f..275168a28 100644
--- a/internal/osbuild2/xorrisofs_stage.go
+++ b/internal/osbuild2/xorrisofs_stage.go
@@ -55,3 +55,11 @@ func NewXorrisofsStage(options *XorrisofsStageOptions, inputs *XorrisofsStageInp
 		Inputs:  inputs,
 	}
 }
+
+func NewXorrisofsStagePipelineTreeInputs(pipeline string) *XorrisofsStageInputs {
+	input := new(XorrisofsStageInput)
+	input.Type = "org.osbuild.tree"
+	input.Origin = "org.osbuild.pipeline"
+	input.References = XorrisofsStageReferences{"name:" + pipeline}
+	return &XorrisofsStageInputs{Tree: input}
+}
diff --git a/internal/rpmmd/metadata.go b/internal/rpmmd/metadata.go
index e0bfd9820..5ea531be3 100644
--- a/internal/rpmmd/metadata.go
+++ b/internal/rpmmd/metadata.go
@@ -2,8 +2,6 @@ package rpmmd
 
 import (
 	"fmt"
-
-	osbuild "github.com/osbuild/osbuild-composer/internal/osbuild2"
 )
 
 type RPM struct {
@@ -26,39 +24,6 @@ func (r RPM) String() string {
 	return fmt.Sprintf("%s-%s%s-%s.%s", r.Name, epoch, r.Version, r.Release, r.Arch)
 }
 
-func OSBuildMetadataToRPMs(stagesMetadata map[string]osbuild.StageMetadata) []RPM {
-	rpms := make([]RPM, 0)
-	for _, md := range stagesMetadata {
-		switch metadata := md.(type) {
-		case *osbuild.RPMStageMetadata:
-			for _, pkg := range metadata.Packages {
-				rpms = append(rpms, RPM{
-					Type:      "rpm",
-					Name:      pkg.Name,
-					Epoch:     pkg.Epoch,
-					Version:   pkg.Version,
-					Release:   pkg.Release,
-					Arch:      pkg.Arch,
-					Sigmd5:    pkg.SigMD5,
-					Signature: PackageMetadataToSignature(pkg),
-				})
-			}
-		default:
-			continue
-		}
-	}
-	return rpms
-}
-
-func PackageMetadataToSignature(pkg osbuild.RPMPackageMetadata) *string {
-	if pkg.SigGPG != "" {
-		return &pkg.SigGPG
-	} else if pkg.SigPGP != "" {
-		return &pkg.SigPGP
-	}
-	return nil
-}
-
 // Deduplicate a list of RPMs based on NEVRA string
 func DeduplicateRPMs(rpms []RPM) []RPM {
 	rpmMap := make(map[string]struct{}, len(rpms))
diff --git a/internal/rpmmd/repository.go b/internal/rpmmd/repository.go
index 1f87ac431..ff0acb841 100644
--- a/internal/rpmmd/repository.go
+++ b/internal/rpmmd/repository.go
@@ -203,6 +203,23 @@ func (re *RepositoryError) Error() string {
 	return re.msg
 }
 
+func GetVerStrFromPackageSpecList(pkgs []PackageSpec, packageName string) (string, error) {
+	for _, pkg := range pkgs {
+		if pkg.Name == packageName {
+			return fmt.Sprintf("%s-%s.%s", pkg.Version, pkg.Release, pkg.Arch), nil
+		}
+	}
+	return "", fmt.Errorf("package %q not found in the PackageSpec list", packageName)
+}
+
+func GetVerStrFromPackageSpecListPanic(pkgs []PackageSpec, packageName string) string {
+	pkgVerStr, err := GetVerStrFromPackageSpecList(pkgs, packageName)
+	if err != nil {
+		panic(err)
+	}
+	return pkgVerStr
+}
+
 func loadRepositoriesFromFile(filename string) (map[string][]RepoConfig, error) {
 	f, err := os.Open(filename)
 	if err != nil {
diff --git a/templates/packer/composer.pkr.hcl b/templates/packer/composer.pkr.hcl
index a37af61e6..850cf6e65 100644
--- a/templates/packer/composer.pkr.hcl
+++ b/templates/packer/composer.pkr.hcl
@@ -16,7 +16,7 @@ source "amazon-ebs" "image_builder" {
   launch_block_device_mappings {
     delete_on_termination = "true"
     device_name           = "/dev/sda1"
-    volume_size           = 25
+    volume_size           = 10
     volume_type           = "gp2"
   }
 


Test patch:
diff --git a/internal/distro/distro_test_common/distro_test_common.go b/internal/distro/distro_test_common/distro_test_common.go
index 457b0aac9..009ff10f7 100644
--- a/internal/distro/distro_test_common/distro_test_common.go
+++ b/internal/distro/distro_test_common/distro_test_common.go
@@ -190,3 +190,32 @@ func TestDistro_KernelOption(t *testing.T, d distro.Distro) {
 		}
 	}
 }
+
+// GetTestingPackageSpecSets returns PackageSpecSets useful for unit testing.
+//
+// A dummy PackageSpec for the provided packageName is added
+// to all PackageSpecSets provided in pkgSetNames.
+//
+// E.g. `kernel` package is a hard requirement of some payload pipelines
+// and they panic if it is not found in the packageSpecSets passed to
+// Manifest().
+func GetTestingPackageSpecSets(packageName, arch string, pkgSetNames []string) map[string][]rpmmd.PackageSpec {
+	pkgTestingSpec := []rpmmd.PackageSpec{
+		{
+			Name:           packageName,
+			Epoch:          0,
+			Version:        "1.2.3",
+			Release:        "2.el123",
+			Arch:           arch,
+			RemoteLocation: "http://example.org",
+			Checksum:       "lorenipsum",
+			Secrets:        "lorenipsum",
+			CheckGPG:       false,
+		},
+	}
+	testPackageSpecSets := map[string][]rpmmd.PackageSpec{}
+	for _, pkgSetName := range pkgSetNames {
+		testPackageSpecSets[pkgSetName] = pkgTestingSpec
+	}
+	return testPackageSpecSets
+}
diff --git a/internal/distro/rhel85/distro_test.go b/internal/distro/rhel85/distro_test.go
index b9fde81e9..ab1cecd2a 100644
--- a/internal/distro/rhel85/distro_test.go
+++ b/internal/distro/rhel85/distro_test.go
@@ -415,7 +415,8 @@ func TestDistro_ManifestError(t *testing.T) {
 			imgOpts := distro.ImageOptions{
 				Size: imgType.Size(0),
 			}
-			_, err := imgType.Manifest(bp.Customizations, imgOpts, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, imgOpts, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "kernel boot parameter customizations are not supported for ostree types")
 			} else if imgTypeName == "edge-raw-image" {
@@ -580,7 +581,8 @@ func TestDistro_CustomFileSystemManifestError(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
@@ -608,7 +610,8 @@ func TestDistro_TestRootMountPoint(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
@@ -640,7 +643,8 @@ func TestDistro_CustomFileSystemSubDirectories(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if strings.HasPrefix(imgTypeName, "edge-") {
 				continue
 			} else {
@@ -678,7 +682,8 @@ func TestDistro_MountpointsWithArbitraryDepthAllowed(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if strings.HasPrefix(imgTypeName, "edge-") {
 				continue
 			} else {
@@ -712,7 +717,8 @@ func TestDistro_DirtyMountpointsNotAllowed(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if strings.HasPrefix(imgTypeName, "edge-") {
 				continue
 			} else {
@@ -742,7 +748,8 @@ func TestDistro_CustomFileSystemPatternMatching(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
@@ -770,7 +777,8 @@ func TestDistro_CustomUsrPartitionNotLargeEnough(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
diff --git a/internal/distro/rhel86/distro_test.go b/internal/distro/rhel86/distro_test.go
index 0dd25fe9a..69f4ccb9d 100644
--- a/internal/distro/rhel86/distro_test.go
+++ b/internal/distro/rhel86/distro_test.go
@@ -424,7 +424,8 @@ func TestDistro_ManifestError(t *testing.T) {
 			imgOpts := distro.ImageOptions{
 				Size: imgType.Size(0),
 			}
-			_, err := imgType.Manifest(bp.Customizations, imgOpts, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, imgOpts, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "kernel boot parameter customizations are not supported for ostree types")
 			} else if imgTypeName == "edge-raw-image" {
@@ -618,7 +619,8 @@ func TestDistro_TestRootMountPoint(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
@@ -650,7 +652,8 @@ func TestDistro_CustomFileSystemSubDirectories(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if strings.HasPrefix(imgTypeName, "edge-") {
 				continue
 			} else {
@@ -688,7 +691,8 @@ func TestDistro_MountpointsWithArbitraryDepthAllowed(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if strings.HasPrefix(imgTypeName, "edge-") {
 				continue
 			} else {
@@ -780,7 +784,8 @@ func TestDistro_CustomUsrPartitionNotLargeEnough(t *testing.T) {
 		arch, _ := r8distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
diff --git a/internal/distro/rhel90/distro_test.go b/internal/distro/rhel90/distro_test.go
index c91c707ea..e13b9d06c 100644
--- a/internal/distro/rhel90/distro_test.go
+++ b/internal/distro/rhel90/distro_test.go
@@ -424,7 +424,8 @@ func TestDistro_ManifestError(t *testing.T) {
 			imgOpts := distro.ImageOptions{
 				Size: imgType.Size(0),
 			}
-			_, err := imgType.Manifest(bp.Customizations, imgOpts, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, imgOpts, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "kernel boot parameter customizations are not supported for ostree types")
 			} else if imgTypeName == "edge-raw-image" {
@@ -618,7 +619,8 @@ func TestDistro_TestRootMountPoint(t *testing.T) {
 		arch, _ := r9distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
@@ -650,7 +652,8 @@ func TestDistro_CustomFileSystemSubDirectories(t *testing.T) {
 		arch, _ := r9distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if strings.HasPrefix(imgTypeName, "edge-") {
 				continue
 			} else {
@@ -688,7 +691,8 @@ func TestDistro_MountpointsWithArbitraryDepthAllowed(t *testing.T) {
 		arch, _ := r9distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if strings.HasPrefix(imgTypeName, "edge-") {
 				continue
 			} else {
@@ -780,7 +784,8 @@ func TestDistro_CustomUsrPartitionNotLargeEnough(t *testing.T) {
 		arch, _ := r9distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" || imgTypeName == "edge-simplified-installer" || imgTypeName == "edge-raw-image" {
diff --git a/internal/distro/rhel90beta/distro_test.go b/internal/distro/rhel90beta/distro_test.go
index 0350c9445..ac40a7fda 100644
--- a/internal/distro/rhel90beta/distro_test.go
+++ b/internal/distro/rhel90beta/distro_test.go
@@ -424,7 +424,8 @@ func TestDistro_ManifestError(t *testing.T) {
 			imgOpts := distro.ImageOptions{
 				Size: imgType.Size(0),
 			}
-			_, err := imgType.Manifest(bp.Customizations, imgOpts, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, imgOpts, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "kernel boot parameter customizations are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" {
@@ -611,7 +612,8 @@ func TestDistro_TestRootMountPoint(t *testing.T) {
 		arch, _ := r9distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" {
@@ -643,7 +645,8 @@ func TestDistro_CustomFileSystemSubDirectories(t *testing.T) {
 		arch, _ := r9distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if strings.HasPrefix(imgTypeName, "edge-") {
 				continue
 			} else {
@@ -681,7 +684,8 @@ func TestDistro_MountpointsWithArbitraryDepthAllowed(t *testing.T) {
 		arch, _ := r9distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if strings.HasPrefix(imgTypeName, "edge-") {
 				continue
 			} else {
@@ -773,7 +777,8 @@ func TestDistro_CustomUsrPartitionNotLargeEnough(t *testing.T) {
 		arch, _ := r9distro.GetArch(archName)
 		for _, imgTypeName := range arch.ListImageTypes() {
 			imgType, _ := arch.GetImageType(imgTypeName)
-			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, nil, 0)
+			testPackageSpecSets := distro_test_common.GetTestingPackageSpecSets("kernel", arch.Name(), imgType.PayloadPackageSets())
+			_, err := imgType.Manifest(bp.Customizations, distro.ImageOptions{}, nil, testPackageSpecSets, 0)
 			if imgTypeName == "edge-commit" || imgTypeName == "edge-container" {
 				assert.EqualError(t, err, "Custom mountpoints are not supported for ostree types")
 			} else if imgTypeName == "edge-installer" {
diff --git a/internal/osbuild2/rpm_stage_test.go b/internal/osbuild2/rpm_stage_test.go
index fbc73f1e2..a23e55eb2 100644
--- a/internal/osbuild2/rpm_stage_test.go
+++ b/internal/osbuild2/rpm_stage_test.go
@@ -1,9 +1,13 @@
 package osbuild2
 
 import (
+	"encoding/json"
+	"fmt"
 	"testing"
 
+	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestNewRPMStage(t *testing.T) {
@@ -15,3 +19,71 @@ func TestNewRPMStage(t *testing.T) {
 	actualStage := NewRPMStage(&RPMStageOptions{}, &RPMStageInputs{})
 	assert.Equal(t, expectedStage, actualStage)
 }
+
+func Test_OSBuildMetadataToRPMs(t *testing.T) {
+	raw := `
+{
+  "org.osbuild.rpm": {
+    "packages": [
+      {
+        "name": "python3-pyserial",
+        "version": "3.4",
+        "release": "7.fc32",
+        "epoch": null,
+        "arch": "noarch",
+        "sigmd5": "378cb32f9f850b275ac4e04d21e8144b",
+        "sigpgp": "89023304000108001d162104963a2beb02009608fe67ea4249fd77499570ff3105025f5a272b000a091049fd77499570ff31ccdb0ffe38b95a55ebf3c021526b3cd4f2358c7e23f7767d1f5ce4b7cccef7b33653c6a96a23022313a818fbaf7abeb41837910f0d3ac15664e02838d5939d38ff459aa0076e248728a032d3ae09ddfaec955f941601081a2e3f9bbd49586fd65c1bc1b31685aeb0405687d1791471eab7359ccf00d5584ddef680e99ebc8a4846316391b9baa68ac8ed8ad696ee16fd625d847f8edd92517df3ea6920a46b77b4f119715a0f619f38835d25e0bd0eb5cfad08cd9c796eace6a2b28f4d3dee552e6068255d9748dc2a1906c951e0ba8aed9922ab24e1f659413a06083f8a0bfea56cfff14bddef23bced449f36bcd369da72f90ddf0512e7b0801ba5a0c8eaa8eb0582c630815e992192042cfb0a7c7239f76219197c2fdf18b6553260c105280806d4f037d7b04bdf3da9fd7e9a207db5c71f7e548f4288928f047c989c4cb9cbb8088eec7bd2fa5c252e693f51a3cfc660f666af6a255a5ca0fd2216d5ccd66cbd9c11afa61067d7f615ec8d0dc0c879b5fe633d8c9443f97285da597e4da8a3993af36f0be06acfa9b8058ec70bbc78b876e4c6c5d2108fb05c15a74ba48a3d7ded697cbc1748c228d77d1e0794a41fd5240fa67c3ed745fe47555a47c3d6163d8ce95fd6c2d0d6fa48f8e5b411e571e442109b1cb200d9a8117ee08bfe645f96aca34f7b7559622bbab75143dcad59f126ae0d319e6668ebba417e725638c4febf2e",
+        "siggpg": "883f0305005f2310139ec3e4c0f7e257e611023e11009f639c5fe64abaa76224dab3a9f70c2714a84c63bd009d1cc184fb4b428dfcd7c3556f4a5f860cc0187740"
+      },
+      {
+        "name": "libgcc",
+        "version": "10.0.1",
+        "release": "0.11.fc32",
+        "epoch": null,
+        "arch": "x86_64",
+        "sigmd5": "84fc907a5047aeebaf8da1642925a417",
+        "sigpgp": "89023304000108001d162104963a2beb02009608fe67ea4249fd77499570ff3105025f5a272b000a091049fd77499570ff31ccdb0ffe38b95a55ebf3c021526b3cd4f2358c7e23f7767d1f5ce4b7cccef7b33653c6a96a23022313a818fbaf7abeb41837910f0d3ac15664e02838d5939d38ff459aa0076e248728a032d3ae09ddfaec955f941601081a2e3f9bbd49586fd65c1bc1b31685aeb0405687d1791471eab7359ccf00d5584ddef680e99ebc8a4846316391b9baa68ac8ed8ad696ee16fd625d847f8edd92517df3ea6920a46b77b4f119715a0f619f38835d25e0bd0eb5cfad08cd9c796eace6a2b28f4d3dee552e6068255d9748dc2a1906c951e0ba8aed9922ab24e1f659413a06083f8a0bfea56cfff14bddef23bced449f36bcd369da72f90ddf0512e7b0801ba5a0c8eaa8eb0582c630815e992192042cfb0a7c7239f76219197c2fdf18b6553260c105280806d4f037d7b04bdf3da9fd7e9a207db5c71f7e548f4288928f047c989c4cb9cbb8088eec7bd2fa5c252e693f51a3cfc660f666af6a255a5ca0fd2216d5ccd66cbd9c11afa61067d7f615ec8d0dc0c879b5fe633d8c9443f97285da597e4da8a3993af36f0be06acfa9b8058ec70bbc78b876e4c6c5d2108fb05c15a74ba48a3d7ded697cbc1748c228d77d1e0794a41fd5240fa67c3ed745fe47555a47c3d6163d8ce95fd6c2d0d6fa48f8e5b411e571e442109b1cb200d9a8117ee08bfe645f96aca34f7b7559622bbab75143dcad59f126ae0d319e6668ebba417e725638c4febf2e",
+        "siggpg": null
+      },
+      {
+        "name": "libgcc-madeup",
+        "version": "10.0.1",
+        "release": "0.11.fc32",
+        "epoch": null,
+        "arch": "x86_64",
+        "sigmd5": "84fc907a5047aeebaf8da1642925a418",
+        "sigpgp": null,
+        "siggpg": null
+      }
+    ]
+  }
+}
+`
+	metadata := new(PipelineMetadata)
+	err := json.Unmarshal([]byte(raw), metadata)
+	require.NoError(t, err)
+
+	fmt.Printf("Result: %#v", metadata)
+	rpms := OSBuildMetadataToRPMs(*metadata)
+
+	require.Len(t, rpms, 3)
+
+	signature1 := "89023304000108001d162104963a2beb02009608fe67ea4249fd77499570ff3105025f5a272b000a091049fd77499570ff31ccdb0ffe38b95a55ebf3c021526b3cd4f2358c7e23f7767d1f5ce4b7cccef7b33653c6a96a23022313a818fbaf7abeb41837910f0d3ac15664e02838d5939d38ff459aa0076e248728a032d3ae09ddfaec955f941601081a2e3f9bbd49586fd65c1bc1b31685aeb0405687d1791471eab7359ccf00d5584ddef680e99ebc8a4846316391b9baa68ac8ed8ad696ee16fd625d847f8edd92517df3ea6920a46b77b4f119715a0f619f38835d25e0bd0eb5cfad08cd9c796eace6a2b28f4d3dee552e6068255d9748dc2a1906c951e0ba8aed9922ab24e1f659413a06083f8a0bfea56cfff14bddef23bced449f36bcd369da72f90ddf0512e7b0801ba5a0c8eaa8eb0582c630815e992192042cfb0a7c7239f76219197c2fdf18b6553260c105280806d4f037d7b04bdf3da9fd7e9a207db5c71f7e548f4288928f047c989c4cb9cbb8088eec7bd2fa5c252e693f51a3cfc660f666af6a255a5ca0fd2216d5ccd66cbd9c11afa61067d7f615ec8d0dc0c879b5fe633d8c9443f97285da597e4da8a3993af36f0be06acfa9b8058ec70bbc78b876e4c6c5d2108fb05c15a74ba48a3d7ded697cbc1748c228d77d1e0794a41fd5240fa67c3ed745fe47555a47c3d6163d8ce95fd6c2d0d6fa48f8e5b411e571e442109b1cb200d9a8117ee08bfe645f96aca34f7b7559622bbab75143dcad59f126ae0d319e6668ebba417e725638c4febf2e"
+	require.Equal(t, rpmmd.RPM{
+		Type:      "rpm",
+		Name:      "libgcc",
+		Version:   "10.0.1",
+		Release:   "0.11.fc32",
+		Epoch:     nil,
+		Arch:      "x86_64",
+		Sigmd5:    "84fc907a5047aeebaf8da1642925a417",
+		Signature: &signature1,
+	}, rpms[1])
+
+	// GPG has a priority over PGP
+	signature0 := "883f0305005f2310139ec3e4c0f7e257e611023e11009f639c5fe64abaa76224dab3a9f70c2714a84c63bd009d1cc184fb4b428dfcd7c3556f4a5f860cc0187740"
+	require.Equal(t, signature0, *rpms[0].Signature)
+
+	// if neither GPG nor PGP is set, the signature is nil
+	require.Nil(t, rpms[2].Signature)
+}
diff --git a/internal/rpmmd/metadata_test.go b/internal/rpmmd/metadata_test.go
index b504825c6..7f5786305 100644
--- a/internal/rpmmd/metadata_test.go
+++ b/internal/rpmmd/metadata_test.go
@@ -1,85 +1,16 @@
-package rpmmd
+// a different package is used to prevent import cycles between `rpmmd` and `osbuild2`
+package rpmmd_test
 
 import (
-	"encoding/json"
-	"fmt"
 	"sort"
 	"testing"
 
 	"github.com/osbuild/osbuild-composer/internal/common"
 	osbuild "github.com/osbuild/osbuild-composer/internal/osbuild2"
+	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
 )
 
-func Test_osbuildStagesToRPMs(t *testing.T) {
-	raw := `
-{
-  "org.osbuild.rpm": {
-    "packages": [
-      {
-        "name": "python3-pyserial",
-        "version": "3.4",
-        "release": "7.fc32",
-        "epoch": null,
-        "arch": "noarch",
-        "sigmd5": "378cb32f9f850b275ac4e04d21e8144b",
-        "sigpgp": "89023304000108001d162104963a2beb02009608fe67ea4249fd77499570ff3105025f5a272b000a091049fd77499570ff31ccdb0ffe38b95a55ebf3c021526b3cd4f2358c7e23f7767d1f5ce4b7cccef7b33653c6a96a23022313a818fbaf7abeb41837910f0d3ac15664e02838d5939d38ff459aa0076e248728a032d3ae09ddfaec955f941601081a2e3f9bbd49586fd65c1bc1b31685aeb0405687d1791471eab7359ccf00d5584ddef680e99ebc8a4846316391b9baa68ac8ed8ad696ee16fd625d847f8edd92517df3ea6920a46b77b4f119715a0f619f38835d25e0bd0eb5cfad08cd9c796eace6a2b28f4d3dee552e6068255d9748dc2a1906c951e0ba8aed9922ab24e1f659413a06083f8a0bfea56cfff14bddef23bced449f36bcd369da72f90ddf0512e7b0801ba5a0c8eaa8eb0582c630815e992192042cfb0a7c7239f76219197c2fdf18b6553260c105280806d4f037d7b04bdf3da9fd7e9a207db5c71f7e548f4288928f047c989c4cb9cbb8088eec7bd2fa5c252e693f51a3cfc660f666af6a255a5ca0fd2216d5ccd66cbd9c11afa61067d7f615ec8d0dc0c879b5fe633d8c9443f97285da597e4da8a3993af36f0be06acfa9b8058ec70bbc78b876e4c6c5d2108fb05c15a74ba48a3d7ded697cbc1748c228d77d1e0794a41fd5240fa67c3ed745fe47555a47c3d6163d8ce95fd6c2d0d6fa48f8e5b411e571e442109b1cb200d9a8117ee08bfe645f96aca34f7b7559622bbab75143dcad59f126ae0d319e6668ebba417e725638c4febf2e",
-        "siggpg": "883f0305005f2310139ec3e4c0f7e257e611023e11009f639c5fe64abaa76224dab3a9f70c2714a84c63bd009d1cc184fb4b428dfcd7c3556f4a5f860cc0187740"
-      },
-      {
-        "name": "libgcc",
-        "version": "10.0.1",
-        "release": "0.11.fc32",
-        "epoch": null,
-        "arch": "x86_64",
-        "sigmd5": "84fc907a5047aeebaf8da1642925a417",
-        "sigpgp": "89023304000108001d162104963a2beb02009608fe67ea4249fd77499570ff3105025f5a272b000a091049fd77499570ff31ccdb0ffe38b95a55ebf3c021526b3cd4f2358c7e23f7767d1f5ce4b7cccef7b33653c6a96a23022313a818fbaf7abeb41837910f0d3ac15664e02838d5939d38ff459aa0076e248728a032d3ae09ddfaec955f941601081a2e3f9bbd49586fd65c1bc1b31685aeb0405687d1791471eab7359ccf00d5584ddef680e99ebc8a4846316391b9baa68ac8ed8ad696ee16fd625d847f8edd92517df3ea6920a46b77b4f119715a0f619f38835d25e0bd0eb5cfad08cd9c796eace6a2b28f4d3dee552e6068255d9748dc2a1906c951e0ba8aed9922ab24e1f659413a06083f8a0bfea56cfff14bddef23bced449f36bcd369da72f90ddf0512e7b0801ba5a0c8eaa8eb0582c630815e992192042cfb0a7c7239f76219197c2fdf18b6553260c105280806d4f037d7b04bdf3da9fd7e9a207db5c71f7e548f4288928f047c989c4cb9cbb8088eec7bd2fa5c252e693f51a3cfc660f666af6a255a5ca0fd2216d5ccd66cbd9c11afa61067d7f615ec8d0dc0c879b5fe633d8c9443f97285da597e4da8a3993af36f0be06acfa9b8058ec70bbc78b876e4c6c5d2108fb05c15a74ba48a3d7ded697cbc1748c228d77d1e0794a41fd5240fa67c3ed745fe47555a47c3d6163d8ce95fd6c2d0d6fa48f8e5b411e571e442109b1cb200d9a8117ee08bfe645f96aca34f7b7559622bbab75143dcad59f126ae0d319e6668ebba417e725638c4febf2e",
-        "siggpg": null
-      },
-      {
-        "name": "libgcc-madeup",
-        "version": "10.0.1",
-        "release": "0.11.fc32",
-        "epoch": null,
-        "arch": "x86_64",
-        "sigmd5": "84fc907a5047aeebaf8da1642925a418",
-        "sigpgp": null,
-        "siggpg": null
-      }
-    ]
-  }
-}
-`
-	metadata := new(osbuild.PipelineMetadata)
-	err := json.Unmarshal([]byte(raw), metadata)
-	require.NoError(t, err)
-
-	fmt.Printf("Result: %#v", metadata)
-	rpms := OSBuildMetadataToRPMs(*metadata)
-
-	require.Len(t, rpms, 3)
-
-	signature1 := "89023304000108001d162104963a2beb02009608fe67ea4249fd77499570ff3105025f5a272b000a091049fd77499570ff31ccdb0ffe38b95a55ebf3c021526b3cd4f2358c7e23f7767d1f5ce4b7cccef7b33653c6a96a23022313a818fbaf7abeb41837910f0d3ac15664e02838d5939d38ff459aa0076e248728a032d3ae09ddfaec955f941601081a2e3f9bbd49586fd65c1bc1b31685aeb0405687d1791471eab7359ccf00d5584ddef680e99ebc8a4846316391b9baa68ac8ed8ad696ee16fd625d847f8edd92517df3ea6920a46b77b4f119715a0f619f38835d25e0bd0eb5cfad08cd9c796eace6a2b28f4d3dee552e6068255d9748dc2a1906c951e0ba8aed9922ab24e1f659413a06083f8a0bfea56cfff14bddef23bced449f36bcd369da72f90ddf0512e7b0801ba5a0c8eaa8eb0582c630815e992192042cfb0a7c7239f76219197c2fdf18b6553260c105280806d4f037d7b04bdf3da9fd7e9a207db5c71f7e548f4288928f047c989c4cb9cbb8088eec7bd2fa5c252e693f51a3cfc660f666af6a255a5ca0fd2216d5ccd66cbd9c11afa61067d7f615ec8d0dc0c879b5fe633d8c9443f97285da597e4da8a3993af36f0be06acfa9b8058ec70bbc78b876e4c6c5d2108fb05c15a74ba48a3d7ded697cbc1748c228d77d1e0794a41fd5240fa67c3ed745fe47555a47c3d6163d8ce95fd6c2d0d6fa48f8e5b411e571e442109b1cb200d9a8117ee08bfe645f96aca34f7b7559622bbab75143dcad59f126ae0d319e6668ebba417e725638c4febf2e"
-	require.Equal(t, RPM{
-		Type:      "rpm",
-		Name:      "libgcc",
-		Version:   "10.0.1",
-		Release:   "0.11.fc32",
-		Epoch:     nil,
-		Arch:      "x86_64",
-		Sigmd5:    "84fc907a5047aeebaf8da1642925a417",
-		Signature: &signature1,
-	}, rpms[1])
-
-	// GPG has a priority over PGP
-	signature0 := "883f0305005f2310139ec3e4c0f7e257e611023e11009f639c5fe64abaa76224dab3a9f70c2714a84c63bd009d1cc184fb4b428dfcd7c3556f4a5f860cc0187740"
-	require.Equal(t, signature0, *rpms[0].Signature)
-
-	// if neither GPG nor PGP is set, the signature is nil
-	require.Nil(t, rpms[2].Signature)
-}
-
 func TestRPMDeduplication(t *testing.T) {
 	assert := assert.New(t)
 	// start with metadata, that includes duplicates, convert, then deduplicate
@@ -183,12 +114,12 @@ func TestRPMDeduplication(t *testing.T) {
 	testNames := []string{"dupename", "dupename", "package-with-epoch", "python38", "python38", "unique", "vim-minimal", "vim-minimal"}
 	testNamesDeduped := []string{"dupename", "dupename", "package-with-epoch", "python38", "unique", "vim-minimal"}
 
-	rpms := OSBuildMetadataToRPMs(metadata)
+	rpms := osbuild.OSBuildMetadataToRPMs(metadata)
 
 	// basic sanity checks
 	assert.Len(rpms, 8)
 
-	sortedNames := func(rpms []RPM) []string {
+	sortedNames := func(rpms []rpmmd.RPM) []string {
 		names := make([]string, len(rpms))
 		for idx, rpm := range rpms {
 			names[idx] = rpm.Name
@@ -201,7 +132,7 @@ func TestRPMDeduplication(t *testing.T) {
 	names := sortedNames(rpms)
 	assert.Equal(names, testNames)
 
-	deduped := DeduplicateRPMs(rpms)
+	deduped := rpmmd.DeduplicateRPMs(rpms)
 	assert.Len(deduped, 6)
 	dedupedNames := sortedNames(deduped)
 	assert.Equal(dedupedNames, testNamesDeduped)
diff --git a/test/cases/upgrade8to9.sh b/test/cases/upgrade8to9.sh
index b8e6a2231..485c67d88 100755
--- a/test/cases/upgrade8to9.sh
+++ b/test/cases/upgrade8to9.sh
@@ -121,6 +121,8 @@ sudo pkill -P "$CONSOLE_PID"
 sudo scp "${SSH_OPTIONS[@]}" -q -i "${SSH_KEY}" /usr/share/tests/osbuild-composer/upgrade8to9/*.sh root@"$INSTANCE_ADDRESS":
 set +e
 sudo ssh "${SSH_OPTIONS[@]}" -q -i "${SSH_KEY}" root@"$INSTANCE_ADDRESS" 'source /root/upgrade_prepare.sh'
+sudo scp "${SSH_OPTIONS[@]}" -q -i "${SSH_KEY}" root@"$INSTANCE_ADDRESS":/var/log/leapp/leapp-preupgrade.log .
+sudo scp "${SSH_OPTIONS[@]}" -q -i "${SSH_KEY}" root@"$INSTANCE_ADDRESS":/var/log/leapp/leapp-report.txt .
 set -e
 
 # watch and log the console during upgrade
diff --git a/test/data/upgrade8to9/upgrade_prepare.sh b/test/data/upgrade8to9/upgrade_prepare.sh
index b3673fc23..74ed81011 100755
--- a/test/data/upgrade8to9/upgrade_prepare.sh
+++ b/test/data/upgrade8to9/upgrade_prepare.sh
@@ -32,6 +32,9 @@ enabled=0
 gpgcheck=0
 EOF
 
+# AllowZoneDrifting is disabled in RHEL-9, see rhbz#2054271 for more details
+sed -i "s/^AllowZoneDrifting=.*/AllowZoneDrifting=no/" /etc/firewalld/firewalld.conf
+
 # check upgrade
 leapp preupgrade --debug --no-rhsm