Repository: osbuild/osbuild-composer
Pull request #1627 (2021-08-06 15:35:07)

osbuild: Simplify mounts and devices
Simplifying devices and mounts for stages in the osbuild2 package.  This represents the osbuild schema much more closely.  Device and Mount have their own types that aren't tied directly to a Stage type, so stage-specific mounts and devices weren't a good representation.

I also took the opportunity to abstract the creation of some stages that include mounts and devices from their final representation.  This makes the Stage creation a bit more restrictive, to match the schema, while also allowing the use of the Mount and Device interfaces.  For example, `mkfs` stages require only one device labelled by the key `device`, so it makes more sense for the creation function to accept only one device and have that function add the key itself, instead of relying on the caller to assign the key or relying on struct types for all use cases (which is what we were doing before).
I think the usefulness of this approach is especially apparent in the zipl.inst stage, where the stage accepts a number of devices and mounts, with arbitrary keys, but also requires one device with the key `disk`.  The new creation function for this stage takes a general Devices map, for the arbitrarily named devices, and a separate disk Device that is added to the stage by the creation function with the appropriate key.

I intended to make the same changes for the inputs, which have the same issues, but that requires bigger changes that should be done in a separate PR.

This pull request includes:

- [x] adequate testing for the new functionality or fixed issue
- [ ] adequate documentation informing people about the change such as
  - [ ] create a file in [news/unreleased](https://github.com/osbuild/osbuild-composer/tree/main/docs/news/unreleased) directory if this change should be mentioned in the release news
  - [ ] 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/

<!--
Thanks for proposing a change to osbuild-composer!

Please don't remove the above check list. These are things that each pull
request must have before it is merged. It helps maintainers to not forget
anything.

If the reason for ticking any of the boxes is ambiguous, please add a short
note explaining why.

For user-visible changes, "adequate documentation" is an entry describing the
change for users in docs/news. Please refer to docs/news/README.md for details.

In addition, if this pull request fixes a downstream issue, please refer to
test/README.md and add these additional items:

- [ ] 1st commit of any `rhbz#` related PR contains bug reproducer; CI reports FAIL or
- [ ] PR contains automated tests for new functionality and
- [ ] QE has approved reproducer/new tests and
- [ ] Subsequent commits provide bug fixes without modifying the reproducer; CI reports PASS and
- [ ] QE approves this PR; RHBZ status is set to `MODIFIED + Verified=Tested`
-->


gicmo: Rebased this onto `main`.
teg: Yikes, I caused a conflict. Needs rebase.
gicmo: > Yikes, I caused a conflict. Needs rebase.

done.

Patch:
diff --git a/internal/distro/rhel85/pipelines.go b/internal/distro/rhel85/pipelines.go
index 70df03cb4..702870f68 100644
--- a/internal/distro/rhel85/pipelines.go
+++ b/internal/distro/rhel85/pipelines.go
@@ -894,10 +894,10 @@ func liveImagePipeline(inputPipelineName string, outputFilename string, pt *disk
 	p.Name = "image"
 	p.Build = "name:build"
 
-	loopback := osbuild.NewLoopbackDevice(&osbuild.LoopbackDeviceOptions{Filename: outputFilename})
 	p.AddStage(osbuild.NewTruncateStage(&osbuild.TruncateStageOptions{Filename: outputFilename, Size: fmt.Sprintf("%d", pt.Size)}))
-	sfOptions, sfDevices := sfdiskStageOptions(pt, loopback)
-	p.AddStage(osbuild.NewSfdiskStage(sfOptions, sfDevices))
+	sfOptions := sfdiskStageOptions(pt)
+	loopback := osbuild.NewLoopbackDevice(&osbuild.LoopbackDeviceOptions{Filename: outputFilename})
+	p.AddStage(osbuild.NewSfdiskStage(sfOptions, loopback))
 
 	for _, stage := range mkfsStages(pt, loopback) {
 		p.AddStage(stage)
@@ -955,28 +955,24 @@ func mkfsStages(pt *disk.PartitionTable, device *osbuild.Device) []*osbuild2.Sta
 				UUID:  p.Filesystem.UUID,
 				Label: p.Filesystem.Label,
 			}
-			devices := &osbuild.MkfsXfsStageDevices{Device: *stageDevice}
-			stage = osbuild.NewMkfsXfsStage(options, devices)
+			stage = osbuild.NewMkfsXfsStage(options, stageDevice)
 		case "vfat":
 			options := &osbuild.MkfsFATStageOptions{
 				VolID: strings.Replace(p.Filesystem.UUID, "-", "", -1),
 			}
-			devices := &osbuild.MkfsFATStageDevices{Device: *stageDevice}
-			stage = osbuild.NewMkfsFATStage(options, devices)
+			stage = osbuild.NewMkfsFATStage(options, stageDevice)
 		case "btrfs":
 			options := &osbuild.MkfsBtrfsStageOptions{
 				UUID:  p.Filesystem.UUID,
 				Label: p.Filesystem.Label,
 			}
-			devices := &osbuild.MkfsBtrfsStageDevices{Device: *stageDevice}
-			stage = osbuild.NewMkfsBtrfsStage(options, devices)
+			stage = osbuild.NewMkfsBtrfsStage(options, stageDevice)
 		case "ext4":
 			options := &osbuild.MkfsExt4StageOptions{
 				UUID:  p.Filesystem.UUID,
 				Label: p.Filesystem.Label,
 			}
-			devices := &osbuild.MkfsExt4StageDevices{Device: *stageDevice}
-			stage = osbuild.NewMkfsExt4Stage(options, devices)
+			stage = osbuild.NewMkfsExt4Stage(options, stageDevice)
 		default:
 			panic("unknown fs type " + p.Type)
 		}
@@ -1006,17 +1002,14 @@ func bootloaderConfigStage(t *imageType, partitionTable disk.PartitionTable, ker
 	return osbuild.NewGRUB2Stage(grub2StageOptions(partitionTable.RootPartition(), partitionTable.BootPartition(), kernelOptions, kernel, kernelVer, uefi, legacy))
 }
 
-func bootloaderInstStage(filename string, pt *disk.PartitionTable, arch *architecture, kernelVer string, devices *osbuild.CopyStageDevices, mounts *osbuild.CopyStageMounts, disk *osbuild.Device) *osbuild.Stage {
+func bootloaderInstStage(filename string, pt *disk.PartitionTable, arch *architecture, kernelVer string, devices *osbuild.Devices, mounts *osbuild.Mounts, disk *osbuild.Device) *osbuild.Stage {
 	platform := arch.legacy
 	if platform != "" {
 		return osbuild.NewGrub2InstStage(grub2InstStageOptions(filename, pt, platform))
 	}
 
 	if arch.name == distro.S390xArchName {
-		devmap := map[string]osbuild.Device(*devices)
-		devmap["disk"] = *disk
-		ziplDevices := osbuild.CopyStageDevices(devmap)
-		return osbuild.NewZiplInstStage(ziplInstStageOptions(kernelVer, pt), &ziplDevices, mounts)
+		return osbuild.NewZiplInstStage(ziplInstStageOptions(kernelVer, pt), disk, devices, mounts)
 	}
 
 	return nil
diff --git a/internal/distro/rhel85/stage_options.go b/internal/distro/rhel85/stage_options.go
index e6d626a90..1b70b0891 100644
--- a/internal/distro/rhel85/stage_options.go
+++ b/internal/distro/rhel85/stage_options.go
@@ -352,11 +352,7 @@ func grub2StageOptions(rootPartition *disk.Partition, bootPartition *disk.Partit
 
 // sfdiskStageOptions creates the options and devices properties for an
 // org.osbuild.sfdisk stage based on a partition table description
-func sfdiskStageOptions(pt *disk.PartitionTable, device *osbuild.Device) (*osbuild.SfdiskStageOptions, *osbuild.SfdiskStageDevices) {
-	stageDevices := &osbuild.SfdiskStageDevices{
-		Device: *device,
-	}
-
+func sfdiskStageOptions(pt *disk.PartitionTable) *osbuild.SfdiskStageOptions {
 	partitions := make([]osbuild.Partition, len(pt.Partitions))
 	for idx, p := range pt.Partitions {
 		partitions[idx] = osbuild.Partition{
@@ -373,7 +369,7 @@ func sfdiskStageOptions(pt *disk.PartitionTable, device *osbuild.Device) (*osbui
 		Partitions: partitions,
 	}
 
-	return stageOptions, stageDevices
+	return stageOptions
 }
 
 // copyFSTreeOptions creates the options, inputs, devices, and mounts properties
@@ -381,8 +377,8 @@ func sfdiskStageOptions(pt *disk.PartitionTable, device *osbuild.Device) (*osbui
 // table description to define the mounts
 func copyFSTreeOptions(inputName, inputPipeline string, pt *disk.PartitionTable, device *osbuild.Device) (
 	*osbuild.CopyStageOptions,
-	*osbuild.CopyStageDevices,
-	*osbuild.CopyStageMounts,
+	*osbuild.Devices,
+	*osbuild.Mounts,
 ) {
 	// assume loopback device for simplicity since it's the only one currently supported
 	// panic if the conversion fails
@@ -433,8 +429,8 @@ func copyFSTreeOptions(inputName, inputPipeline string, pt *disk.PartitionTable,
 		return mounts[i].Target < mounts[j].Target
 	})
 
-	stageMounts := osbuild.CopyStageMounts(mounts)
-	stageDevices := osbuild.CopyStageDevices(devices)
+	stageMounts := osbuild.Mounts(mounts)
+	stageDevices := osbuild.Devices(devices)
 
 	options := osbuild.CopyStageOptions{
 		Paths: []osbuild.CopyStagePath{
diff --git a/internal/distro/rhel90/pipelines.go b/internal/distro/rhel90/pipelines.go
index 734787bba..bd5ade2e7 100644
--- a/internal/distro/rhel90/pipelines.go
+++ b/internal/distro/rhel90/pipelines.go
@@ -873,10 +873,10 @@ func liveImagePipeline(inputPipelineName string, outputFilename string, pt *disk
 	p.Name = "image"
 	p.Build = "name:build"
 
-	loopback := osbuild.NewLoopbackDevice(&osbuild.LoopbackDeviceOptions{Filename: outputFilename})
 	p.AddStage(osbuild.NewTruncateStage(&osbuild.TruncateStageOptions{Filename: outputFilename, Size: fmt.Sprintf("%d", pt.Size)}))
-	sfOptions, sfDevices := sfdiskStageOptions(pt, loopback)
-	p.AddStage(osbuild.NewSfdiskStage(sfOptions, sfDevices))
+	sfOptions := sfdiskStageOptions(pt)
+	loopback := osbuild.NewLoopbackDevice(&osbuild.LoopbackDeviceOptions{Filename: outputFilename})
+	p.AddStage(osbuild.NewSfdiskStage(sfOptions, loopback))
 
 	for _, stage := range mkfsStages(pt, loopback) {
 		p.AddStage(stage)
@@ -934,28 +934,24 @@ func mkfsStages(pt *disk.PartitionTable, device *osbuild.Device) []*osbuild2.Sta
 				UUID:  p.Filesystem.UUID,
 				Label: p.Filesystem.Label,
 			}
-			devices := &osbuild.MkfsXfsStageDevices{Device: *stageDevice}
-			stage = osbuild.NewMkfsXfsStage(options, devices)
+			stage = osbuild.NewMkfsXfsStage(options, stageDevice)
 		case "vfat":
 			options := &osbuild.MkfsFATStageOptions{
 				VolID: strings.Replace(p.Filesystem.UUID, "-", "", -1),
 			}
-			devices := &osbuild.MkfsFATStageDevices{Device: *stageDevice}
-			stage = osbuild.NewMkfsFATStage(options, devices)
+			stage = osbuild.NewMkfsFATStage(options, stageDevice)
 		case "btrfs":
 			options := &osbuild.MkfsBtrfsStageOptions{
 				UUID:  p.Filesystem.UUID,
 				Label: p.Filesystem.Label,
 			}
-			devices := &osbuild.MkfsBtrfsStageDevices{Device: *stageDevice}
-			stage = osbuild.NewMkfsBtrfsStage(options, devices)
+			stage = osbuild.NewMkfsBtrfsStage(options, stageDevice)
 		case "ext4":
 			options := &osbuild.MkfsExt4StageOptions{
 				UUID:  p.Filesystem.UUID,
 				Label: p.Filesystem.Label,
 			}
-			devices := &osbuild.MkfsExt4StageDevices{Device: *stageDevice}
-			stage = osbuild.NewMkfsExt4Stage(options, devices)
+			stage = osbuild.NewMkfsExt4Stage(options, stageDevice)
 		default:
 			panic("unknown fs type " + p.Type)
 		}
@@ -985,17 +981,14 @@ func bootloaderConfigStage(t *imageType, partitionTable disk.PartitionTable, ker
 	return osbuild.NewGRUB2Stage(grub2StageOptions(partitionTable.RootPartition(), partitionTable.BootPartition(), kernelOptions, kernel, kernelVer, uefi, legacy))
 }
 
-func bootloaderInstStage(filename string, pt *disk.PartitionTable, arch *architecture, kernelVer string, devices *osbuild.CopyStageDevices, mounts *osbuild.CopyStageMounts, disk *osbuild.Device) *osbuild.Stage {
+func bootloaderInstStage(filename string, pt *disk.PartitionTable, arch *architecture, kernelVer string, devices *osbuild.Devices, mounts *osbuild.Mounts, disk *osbuild.Device) *osbuild.Stage {
 	platform := arch.legacy
 	if platform != "" {
 		return osbuild.NewGrub2InstStage(grub2InstStageOptions(filename, pt, platform))
 	}
 
 	if arch.name == s390xArchName {
-		devmap := map[string]osbuild.Device(*devices)
-		devmap["disk"] = *disk
-		ziplDevices := osbuild.CopyStageDevices(devmap)
-		return osbuild.NewZiplInstStage(ziplInstStageOptions(kernelVer, pt), &ziplDevices, mounts)
+		return osbuild.NewZiplInstStage(ziplInstStageOptions(kernelVer, pt), disk, devices, mounts)
 	}
 
 	return nil
diff --git a/internal/distro/rhel90/stage_options.go b/internal/distro/rhel90/stage_options.go
index 5ec931173..95a20c6b3 100644
--- a/internal/distro/rhel90/stage_options.go
+++ b/internal/distro/rhel90/stage_options.go
@@ -352,11 +352,7 @@ func grub2StageOptions(rootPartition *disk.Partition, bootPartition *disk.Partit
 
 // sfdiskStageOptions creates the options and devices properties for an
 // org.osbuild.sfdisk stage based on a partition table description
-func sfdiskStageOptions(pt *disk.PartitionTable, device *osbuild.Device) (*osbuild.SfdiskStageOptions, *osbuild.SfdiskStageDevices) {
-	stageDevices := &osbuild.SfdiskStageDevices{
-		Device: *device,
-	}
-
+func sfdiskStageOptions(pt *disk.PartitionTable) *osbuild.SfdiskStageOptions {
 	partitions := make([]osbuild.Partition, len(pt.Partitions))
 	for idx, p := range pt.Partitions {
 		partitions[idx] = osbuild.Partition{
@@ -373,7 +369,7 @@ func sfdiskStageOptions(pt *disk.PartitionTable, device *osbuild.Device) (*osbui
 		Partitions: partitions,
 	}
 
-	return stageOptions, stageDevices
+	return stageOptions
 }
 
 // copyFSTreeOptions creates the options, inputs, devices, and mounts properties
@@ -381,8 +377,8 @@ func sfdiskStageOptions(pt *disk.PartitionTable, device *osbuild.Device) (*osbui
 // table description to define the mounts
 func copyFSTreeOptions(inputName, inputPipeline string, pt *disk.PartitionTable, device *osbuild.Device) (
 	*osbuild.CopyStageOptions,
-	*osbuild.CopyStageDevices,
-	*osbuild.CopyStageMounts,
+	*osbuild.Devices,
+	*osbuild.Mounts,
 ) {
 	// assume loopback device for simplicity since it's the only one currently supported
 	// panic if the conversion fails
@@ -433,8 +429,8 @@ func copyFSTreeOptions(inputName, inputPipeline string, pt *disk.PartitionTable,
 		return mounts[i].Target < mounts[j].Target
 	})
 
-	stageMounts := osbuild.CopyStageMounts(mounts)
-	stageDevices := osbuild.CopyStageDevices(devices)
+	stageMounts := osbuild.Mounts(mounts)
+	stageDevices := osbuild.Devices(devices)
 
 	options := osbuild.CopyStageOptions{
 		Paths: []osbuild.CopyStagePath{
diff --git a/internal/osbuild2/copy_stage.go b/internal/osbuild2/copy_stage.go
index 546665d59..43a26ca10 100644
--- a/internal/osbuild2/copy_stage.go
+++ b/internal/osbuild2/copy_stage.go
@@ -27,20 +27,12 @@ type CopyStageReferences []string
 
 func (CopyStageReferences) isReferences() {}
 
-type CopyStageDevices map[string]Device
-
-func (CopyStageDevices) isStageDevices() {}
-
-type CopyStageMounts []Mount
-
-func (CopyStageMounts) isStageMounts() {}
-
-func NewCopyStage(options *CopyStageOptions, inputs *CopyStageInputs, devices *CopyStageDevices, mounts *CopyStageMounts) *Stage {
+func NewCopyStage(options *CopyStageOptions, inputs *CopyStageInputs, devices *Devices, mounts *Mounts) *Stage {
 	return &Stage{
 		Type:    "org.osbuild.copy",
 		Options: options,
 		Inputs:  inputs,
-		Devices: devices,
-		Mounts:  mounts,
+		Devices: *devices,
+		Mounts:  *mounts,
 	}
 }
diff --git a/internal/osbuild2/device.go b/internal/osbuild2/device.go
index 2aa8378ad..b6f870b8e 100644
--- a/internal/osbuild2/device.go
+++ b/internal/osbuild2/device.go
@@ -1,8 +1,6 @@
 package osbuild2
 
-type Devices interface {
-	isStageDevices()
-}
+type Devices map[string]Device
 
 type Device struct {
 	Type    string        `json:"type"`
diff --git a/internal/osbuild2/mkfs_btrfs_stage.go b/internal/osbuild2/mkfs_btrfs_stage.go
index 38f1002f6..6f116c31c 100644
--- a/internal/osbuild2/mkfs_btrfs_stage.go
+++ b/internal/osbuild2/mkfs_btrfs_stage.go
@@ -7,16 +7,10 @@ type MkfsBtrfsStageOptions struct {
 
 func (MkfsBtrfsStageOptions) isStageOptions() {}
 
-type MkfsBtrfsStageDevices struct {
-	Device Device `json:"device"`
-}
-
-func (MkfsBtrfsStageDevices) isStageDevices() {}
-
-func NewMkfsBtrfsStage(options *MkfsBtrfsStageOptions, devices *MkfsBtrfsStageDevices) *Stage {
+func NewMkfsBtrfsStage(options *MkfsBtrfsStageOptions, device *Device) *Stage {
 	return &Stage{
 		Type:    "org.osbuild.mkfs.btrfs",
 		Options: options,
-		Devices: devices,
+		Devices: Devices{"device": *device},
 	}
 }
diff --git a/internal/osbuild2/mkfs_ext4_stage.go b/internal/osbuild2/mkfs_ext4_stage.go
index 0472cf113..2ee483664 100644
--- a/internal/osbuild2/mkfs_ext4_stage.go
+++ b/internal/osbuild2/mkfs_ext4_stage.go
@@ -7,16 +7,10 @@ type MkfsExt4StageOptions struct {
 
 func (MkfsExt4StageOptions) isStageOptions() {}
 
-type MkfsExt4StageDevices struct {
-	Device Device `json:"device"`
-}
-
-func (MkfsExt4StageDevices) isStageDevices() {}
-
-func NewMkfsExt4Stage(options *MkfsExt4StageOptions, devices *MkfsExt4StageDevices) *Stage {
+func NewMkfsExt4Stage(options *MkfsExt4StageOptions, device *Device) *Stage {
 	return &Stage{
 		Type:    "org.osbuild.mkfs.ext4",
 		Options: options,
-		Devices: devices,
+		Devices: Devices{"device": *device},
 	}
 }
diff --git a/internal/osbuild2/mkfs_fat_stage.go b/internal/osbuild2/mkfs_fat_stage.go
index b7556ba67..6100656cb 100644
--- a/internal/osbuild2/mkfs_fat_stage.go
+++ b/internal/osbuild2/mkfs_fat_stage.go
@@ -8,16 +8,10 @@ type MkfsFATStageOptions struct {
 
 func (MkfsFATStageOptions) isStageOptions() {}
 
-type MkfsFATStageDevices struct {
-	Device Device `json:"device"`
-}
-
-func (MkfsFATStageDevices) isStageDevices() {}
-
-func NewMkfsFATStage(options *MkfsFATStageOptions, devices *MkfsFATStageDevices) *Stage {
+func NewMkfsFATStage(options *MkfsFATStageOptions, device *Device) *Stage {
 	return &Stage{
 		Type:    "org.osbuild.mkfs.fat",
 		Options: options,
-		Devices: devices,
+		Devices: Devices{"device": *device},
 	}
 }
diff --git a/internal/osbuild2/mkfs_xfs_stage.go b/internal/osbuild2/mkfs_xfs_stage.go
index 81f5c6a5a..6f2b3aec7 100644
--- a/internal/osbuild2/mkfs_xfs_stage.go
+++ b/internal/osbuild2/mkfs_xfs_stage.go
@@ -7,16 +7,10 @@ type MkfsXfsStageOptions struct {
 
 func (MkfsXfsStageOptions) isStageOptions() {}
 
-type MkfsXfsStageDevices struct {
-	Device Device `json:"device"`
-}
-
-func (MkfsXfsStageDevices) isStageDevices() {}
-
-func NewMkfsXfsStage(options *MkfsXfsStageOptions, devices *MkfsXfsStageDevices) *Stage {
+func NewMkfsXfsStage(options *MkfsXfsStageOptions, device *Device) *Stage {
 	return &Stage{
 		Type:    "org.osbuild.mkfs.xfs",
 		Options: options,
-		Devices: devices,
+		Devices: Devices{"device": *device},
 	}
 }
diff --git a/internal/osbuild2/mount.go b/internal/osbuild2/mount.go
index 054c063eb..af629d555 100644
--- a/internal/osbuild2/mount.go
+++ b/internal/osbuild2/mount.go
@@ -1,8 +1,6 @@
 package osbuild2
 
-type Mounts interface {
-	isStageMounts()
-}
+type Mounts []Mount
 
 type Mount struct {
 	Name    string        `json:"name"`
diff --git a/internal/osbuild2/sfdisk_stage.go b/internal/osbuild2/sfdisk_stage.go
index c27995f82..e370a34d7 100644
--- a/internal/osbuild2/sfdisk_stage.go
+++ b/internal/osbuild2/sfdisk_stage.go
@@ -36,16 +36,10 @@ type Partition struct {
 	UUID string `json:"uuid,omitempty"`
 }
 
-type SfdiskStageDevices struct {
-	Device Device `json:"device"`
-}
-
-func (SfdiskStageDevices) isStageDevices() {}
-
-func NewSfdiskStage(options *SfdiskStageOptions, devices *SfdiskStageDevices) *Stage {
+func NewSfdiskStage(options *SfdiskStageOptions, device *Device) *Stage {
 	return &Stage{
 		Type:    "org.osbuild.sfdisk",
 		Options: options,
-		Devices: devices,
+		Devices: Devices{"device": *device},
 	}
 }
diff --git a/internal/osbuild2/stage.go b/internal/osbuild2/stage.go
index 8b47531f6..82052816a 100644
--- a/internal/osbuild2/stage.go
+++ b/internal/osbuild2/stage.go
@@ -111,24 +111,17 @@ func (stage *Stage) UnmarshalJSON(data []byte) error {
 		options = new(TruncateStageOptions)
 	case "org.osbuild.sfdisk":
 		options = new(SfdiskStageOptions)
-		devices = new(SfdiskStageDevices)
 	case "org.osbuild.copy":
 		options = new(CopyStageOptions)
 		inputs = new(CopyStageInputs)
-		devices = new(CopyStageDevices)
-		mounts = new(CopyStageMounts)
 	case "org.osbuild.mkfs.btrfs":
 		options = new(MkfsBtrfsStageOptions)
-		devices = new(MkfsBtrfsStageDevices)
 	case "org.osbuild.mkfs.ext4":
 		options = new(MkfsExt4StageOptions)
-		devices = new(MkfsExt4StageDevices)
 	case "org.osbuild.mkfs.fat":
 		options = new(MkfsFATStageOptions)
-		devices = new(MkfsFATStageDevices)
 	case "org.osbuild.mkfs.xfs":
 		options = new(MkfsXfsStageOptions)
-		devices = new(MkfsXfsStageDevices)
 	case "org.osbuild.qemu":
 		options = new(QEMUStageOptions)
 		inputs = new(QEMUStageInputs)
diff --git a/internal/osbuild2/zipl_inst_stage.go b/internal/osbuild2/zipl_inst_stage.go
index 546570dd4..8f2eb4ebc 100644
--- a/internal/osbuild2/zipl_inst_stage.go
+++ b/internal/osbuild2/zipl_inst_stage.go
@@ -13,20 +13,17 @@ type ZiplInstStageOptions struct {
 
 func (ZiplInstStageOptions) isStageOptions() {}
 
-type ZiplInstStageDevices map[string]Device
-
-func (ZiplInstStageDevices) isStageDevices() {}
-
-type ZiplInstStageMounts []Mount
-
-func (ZiplInstStageMounts) isStageMounts() {}
-
-// Return a new zipl.inst stage. A device needs to be specified as 'disk' and root mountpoint must be provided
-func NewZiplInstStage(options *ZiplInstStageOptions, devices *CopyStageDevices, mounts *CopyStageMounts) *Stage {
+// Return a new zipl.inst stage. The 'disk' parameter must represent the
+// (entire) device that contains the /boot partition.
+func NewZiplInstStage(options *ZiplInstStageOptions, disk *Device, devices *Devices, mounts *Mounts) *Stage {
+	// create a new devices map and add the disk to it
+	devmap := map[string]Device(*devices)
+	devmap["disk"] = *disk
+	ziplDevices := Devices(devmap)
 	return &Stage{
 		Type:    "org.osbuild.zipl.inst",
 		Options: options,
-		Devices: devices,
-		Mounts:  mounts,
+		Devices: ziplDevices,
+		Mounts:  *mounts,
 	}
 }


Test patch:
diff --git a/internal/osbuild2/copy_stage_test.go b/internal/osbuild2/copy_stage_test.go
index d2594cdc4..7c9c9e9a9 100644
--- a/internal/osbuild2/copy_stage_test.go
+++ b/internal/osbuild2/copy_stage_test.go
@@ -14,8 +14,8 @@ func TestNewCopyStage(t *testing.T) {
 			To:   "mount://root/",
 		},
 	}
+
 	devices := make(map[string]Device)
-	var mounts []Mount
 	devices["root"] = Device{
 		Type: "org.osbuild.loopback",
 		Options: LoopbackDeviceOptions{
@@ -24,19 +24,25 @@ func TestNewCopyStage(t *testing.T) {
 			Size:     1073741824,
 		},
 	}
+
+	mounts := []Mount{
+		*NewBtrfsMount("root", "root", "/"),
+	}
+
 	treeInput := CopyStageInput{}
 	treeInput.Type = "org.osbuild.tree"
 	treeInput.Origin = "org.osbuild.pipeline"
 	treeInput.References = []string{"name:input-pipeline"}
-	copyStageMounts := CopyStageMounts(mounts)
-	copyStageDevices := CopyStageDevices(devices)
 	expectedStage := &Stage{
 		Type:    "org.osbuild.copy",
 		Options: &CopyStageOptions{paths},
 		Inputs:  &CopyStageInputs{"tree-input": treeInput},
-		Devices: &copyStageDevices,
-		Mounts:  &copyStageMounts,
+		Devices: devices,
+		Mounts:  mounts,
 	}
-	actualStage := NewCopyStage(&CopyStageOptions{paths}, &CopyStageInputs{"tree-input": treeInput}, &copyStageDevices, &copyStageMounts)
+	// convert to alias types
+	stageMounts := Mounts(mounts)
+	stageDevices := Devices(devices)
+	actualStage := NewCopyStage(&CopyStageOptions{paths}, &CopyStageInputs{"tree-input": treeInput}, &stageDevices, &stageMounts)
 	assert.Equal(t, expectedStage, actualStage)
 }
diff --git a/internal/osbuild2/mkfs_stages_test.go b/internal/osbuild2/mkfs_stages_test.go
index b0c78c92b..b47bdf564 100644
--- a/internal/osbuild2/mkfs_stages_test.go
+++ b/internal/osbuild2/mkfs_stages_test.go
@@ -21,12 +21,11 @@ func TestNewMkfsStage(t *testing.T) {
 		UUID:  uuid.New().String(),
 		Label: "test",
 	}
-	btrfsDevices := &MkfsBtrfsStageDevices{Device: *device}
-	mkbtrfs := NewMkfsBtrfsStage(btrfsOptions, btrfsDevices)
+	mkbtrfs := NewMkfsBtrfsStage(btrfsOptions, device)
 	mkbtrfsExpected := &Stage{
 		Type:    "org.osbuild.mkfs.btrfs",
 		Options: btrfsOptions,
-		Devices: btrfsDevices,
+		Devices: Devices{"device": *device},
 	}
 	assert.Equal(t, mkbtrfsExpected, mkbtrfs)
 
@@ -34,12 +33,11 @@ func TestNewMkfsStage(t *testing.T) {
 		UUID:  uuid.New().String(),
 		Label: "test",
 	}
-	ext4Devices := &MkfsExt4StageDevices{Device: *device}
-	mkext4 := NewMkfsExt4Stage(ext4Options, ext4Devices)
+	mkext4 := NewMkfsExt4Stage(ext4Options, device)
 	mkext4Expected := &Stage{
 		Type:    "org.osbuild.mkfs.ext4",
 		Options: ext4Options,
-		Devices: ext4Devices,
+		Devices: Devices{"device": *device},
 	}
 	assert.Equal(t, mkext4Expected, mkext4)
 
@@ -48,12 +46,11 @@ func TestNewMkfsStage(t *testing.T) {
 		Label:   "test",
 		FATSize: common.IntToPtr(12),
 	}
-	fatDevices := &MkfsFATStageDevices{Device: *device}
-	mkfat := NewMkfsFATStage(fatOptions, fatDevices)
+	mkfat := NewMkfsFATStage(fatOptions, device)
 	mkfatExpected := &Stage{
 		Type:    "org.osbuild.mkfs.fat",
 		Options: fatOptions,
-		Devices: fatDevices,
+		Devices: Devices{"device": *device},
 	}
 	assert.Equal(t, mkfatExpected, mkfat)
 
@@ -61,12 +58,11 @@ func TestNewMkfsStage(t *testing.T) {
 		UUID:  uuid.New().String(),
 		Label: "test",
 	}
-	xfsDevices := &MkfsXfsStageDevices{Device: *device}
-	mkxfs := NewMkfsXfsStage(xfsOptions, xfsDevices)
+	mkxfs := NewMkfsXfsStage(xfsOptions, device)
 	mkxfsExpected := &Stage{
 		Type:    "org.osbuild.mkfs.xfs",
 		Options: xfsOptions,
-		Devices: xfsDevices,
+		Devices: Devices{"device": *device},
 	}
 	assert.Equal(t, mkxfsExpected, mkxfs)
 }
diff --git a/internal/osbuild2/sfdisk_stage_test.go b/internal/osbuild2/sfdisk_stage_test.go
index 4a0a3c0d7..131058d03 100644
--- a/internal/osbuild2/sfdisk_stage_test.go
+++ b/internal/osbuild2/sfdisk_stage_test.go
@@ -24,14 +24,14 @@ func TestNewSfdiskStage(t *testing.T) {
 	}
 
 	device := NewLoopbackDevice(&LoopbackDeviceOptions{Filename: "disk.raw"})
-	devices := SfdiskStageDevices{*device}
+	devices := Devices{"device": *device}
 
 	expectedStage := &Stage{
 		Type:    "org.osbuild.sfdisk",
 		Options: &options,
-		Devices: &devices,
+		Devices: devices,
 	}
 
-	actualStage := NewSfdiskStage(&options, &devices)
+	actualStage := NewSfdiskStage(&options, device)
 	assert.Equal(t, expectedStage, actualStage)
 }