Repository: osbuild/osbuild-composer
Pull request #395 (2020-03-25 14:05:07)

distro: define valid image types for the arches
Certain image types are not supported on certain architectures. For
example VMWare works only on x86_64, so it is not possible nor necessary
to produce vmdk images for any other architecture. The same goes to
cloud-provider specific images, we only support architectures they
support, e.g. AWS: x86_64 and aarch64, but not ppc64le or s390x.

This patch move the imageTypes dictionary to the appropriate place which
is in the architecture specific structure. It also merges the two "arch"
structures because it wasn't clear which one to use for this purpose.
It does the same for the imageType structures.

Only Fedora distros are covered in this PR.


Patch:
diff --git a/internal/distro/distro.go b/internal/distro/distro.go
index d29c1f034..07144706d 100644
--- a/internal/distro/distro.go
+++ b/internal/distro/distro.go
@@ -31,11 +31,6 @@ type Distro interface {
 	// Returns an object representing the given architecture as support
 	// by this distro.
 	GetArch(arch string) (Arch, error)
-
-	// Returns the canonical filename and MIME type for a given output
-	// format. `outputFormat` must be one returned by
-	// TODO: remove when redundant
-	FilenameFromType(outputFormat string) (string, string, error)
 }
 
 // An Arch represents a given distribution's support for a given architecture.
diff --git a/internal/distro/fedora30/distro.go b/internal/distro/fedora30/distro.go
index cbf314dca..81d607690 100644
--- a/internal/distro/fedora30/distro.go
+++ b/internal/distro/fedora30/distro.go
@@ -25,14 +25,18 @@ type Fedora30 struct {
 }
 
 type arch struct {
+	distro             *Fedora30
 	name               string
 	bootloaderPackages []string
 	buildPackages      []string
 	uefi               bool
+	imageTypes         map[string]imageType
 }
 
 type imageType struct {
+	arch             *arch
 	name             string
+	filename         string
 	mimeType         string
 	packages         []string
 	excludedPackages []string
@@ -44,18 +48,6 @@ type imageType struct {
 	assembler        func(uefi bool, size uint64) *osbuild.Assembler
 }
 
-type fedora30Arch struct {
-	name   string
-	distro *Fedora30
-	arch   *arch
-}
-
-type fedora30ImageType struct {
-	name      string
-	arch      *fedora30Arch
-	imageType *imageType
-}
-
 func (d *Fedora30) ListArchs() []string {
 	archs := make([]string, 0, len(d.arches))
 	for name := range d.arches {
@@ -71,77 +63,103 @@ func (d *Fedora30) GetArch(arch string) (distro.Arch, error) {
 		return nil, errors.New("invalid architecture: " + arch)
 	}
 
-	return &fedora30Arch{
-		name:   arch,
-		distro: d,
-		arch:   &a,
-	}, nil
+	return &a, nil
 }
 
-func (a *fedora30Arch) Name() string {
+func (d *Fedora30) setArches(arches ...arch) {
+	d.arches = map[string]arch{}
+	for _, a := range arches {
+		d.arches[a.name] = arch{
+			distro:             d,
+			name:               a.name,
+			bootloaderPackages: a.bootloaderPackages,
+			buildPackages:      a.buildPackages,
+			uefi:               a.uefi,
+			imageTypes:         a.imageTypes,
+		}
+	}
+}
+
+func (a *arch) Name() string {
 	return a.name
 }
 
-func (a *fedora30Arch) ListImageTypes() []string {
-	formats := make([]string, 0, len(a.distro.imageTypes))
-	for name := range a.distro.imageTypes {
+func (a *arch) ListImageTypes() []string {
+	formats := make([]string, 0, len(a.imageTypes))
+	for name := range a.imageTypes {
 		formats = append(formats, name)
 	}
 	sort.Strings(formats)
 	return formats
 }
 
-func (a *fedora30Arch) GetImageType(imageType string) (distro.ImageType, error) {
-	t, exists := a.distro.imageTypes[imageType]
+func (a *arch) GetImageType(imageType string) (distro.ImageType, error) {
+	t, exists := a.imageTypes[imageType]
 	if !exists {
 		return nil, errors.New("invalid image type: " + imageType)
 	}
 
-	return &fedora30ImageType{
-		name:      imageType,
-		arch:      a,
-		imageType: &t,
-	}, nil
+	return &t, nil
+}
+
+func (a *arch) setImageTypes(imageTypes ...imageType) {
+	a.imageTypes = map[string]imageType{}
+	for _, it := range imageTypes {
+		a.imageTypes[it.name] = imageType{
+			arch:             a,
+			name:             it.name,
+			filename:         it.filename,
+			mimeType:         it.mimeType,
+			packages:         it.packages,
+			excludedPackages: it.excludedPackages,
+			enabledServices:  it.enabledServices,
+			disabledServices: it.disabledServices,
+			kernelOptions:    it.kernelOptions,
+			bootable:         it.bootable,
+			defaultSize:      it.defaultSize,
+			assembler:        it.assembler,
+		}
+	}
 }
 
-func (t *fedora30ImageType) Name() string {
+func (t *imageType) Name() string {
 	return t.name
 }
 
-func (t *fedora30ImageType) Filename() string {
-	return t.imageType.name
+func (t *imageType) Filename() string {
+	return t.filename
 }
 
-func (t *fedora30ImageType) MIMEType() string {
-	return t.imageType.mimeType
+func (t *imageType) MIMEType() string {
+	return t.mimeType
 }
 
-func (t *fedora30ImageType) Size(size uint64) uint64 {
+func (t *imageType) Size(size uint64) uint64 {
 	const MegaByte = 1024 * 1024
 	// Microsoft Azure requires vhd images to be rounded up to the nearest MB
 	if t.name == "vhd" && size%MegaByte != 0 {
 		size = (size/MegaByte + 1) * MegaByte
 	}
 	if size == 0 {
-		size = t.imageType.defaultSize
+		size = t.defaultSize
 	}
 	return size
 }
 
-func (t *fedora30ImageType) BasePackages() ([]string, []string) {
-	packages := t.imageType.packages
-	if t.imageType.bootable {
-		packages = append(packages, t.arch.arch.bootloaderPackages...)
+func (t *imageType) BasePackages() ([]string, []string) {
+	packages := t.packages
+	if t.bootable {
+		packages = append(packages, t.arch.bootloaderPackages...)
 	}
 
-	return packages, t.imageType.excludedPackages
+	return packages, t.excludedPackages
 }
 
-func (t *fedora30ImageType) BuildPackages() []string {
-	return append(t.arch.distro.buildPackages, t.arch.arch.buildPackages...)
+func (t *imageType) BuildPackages() []string {
+	return append(t.arch.distro.buildPackages, t.arch.buildPackages...)
 }
 
-func (t *fedora30ImageType) Manifest(c *blueprint.Customizations,
+func (t *imageType) Manifest(c *blueprint.Customizations,
 	repos []rpmmd.RepoConfig,
 	packageSpecs,
 	buildPackageSpecs []rpmmd.PackageSpec,
@@ -160,44 +178,9 @@ func (t *fedora30ImageType) Manifest(c *blueprint.Customizations,
 func New() *Fedora30 {
 	const GigaByte = 1024 * 1024 * 1024
 
-	r := Fedora30{
-		imageTypes: map[string]imageType{},
-		buildPackages: []string{
-			"dnf",
-			"dosfstools",
-			"e2fsprogs",
-			"policycoreutils",
-			"qemu-img",
-			"systemd",
-			"tar",
-			"xz",
-		},
-		arches: map[string]arch{
-			"x86_64": arch{
-				name: "x86_64",
-				bootloaderPackages: []string{
-					"grub2-pc",
-				},
-				buildPackages: []string{
-					"grub2-pc",
-				},
-			},
-			"aarch64": arch{
-				name: "aarch64",
-				bootloaderPackages: []string{
-					"dracut-config-generic",
-					"efibootmgr",
-					"grub2-efi-aa64",
-					"grub2-tools",
-					"shim-aa64",
-				},
-				uefi: true,
-			},
-		},
-	}
-
-	r.imageTypes["ami"] = imageType{
-		name:     "image.raw.xz",
+	amiImgType := imageType{
+		name:     "ami",
+		filename: "image.raw.xz",
 		mimeType: "application/octet-stream",
 		packages: []string{
 			"@Core",
@@ -221,12 +204,13 @@ func New() *Fedora30 {
 		bootable:      true,
 		defaultSize:   6 * GigaByte,
 		assembler: func(uefi bool, size uint64) *osbuild.Assembler {
-			return r.qemuAssembler("raw.xz", "image.raw.xz", uefi, size)
+			return qemuAssembler("raw.xz", "image.raw.xz", uefi, size)
 		},
 	}
 
-	r.imageTypes["ext4-filesystem"] = imageType{
-		name:     "filesystem.img",
+	ext4FilesystemType := imageType{
+		name:     "ext4-filesystem",
+		filename: "filesystem.img",
 		mimeType: "application/octet-stream",
 		packages: []string{
 			"policycoreutils",
@@ -242,11 +226,12 @@ func New() *Fedora30 {
 		kernelOptions: "ro biosdevname=0 net.ifnames=0",
 		bootable:      false,
 		defaultSize:   2 * GigaByte,
-		assembler:     func(uefi bool, size uint64) *osbuild.Assembler { return r.rawFSAssembler("filesystem.img", size) },
+		assembler:     func(uefi bool, size uint64) *osbuild.Assembler { return rawFSAssembler("filesystem.img", size) },
 	}
 
-	r.imageTypes["partitioned-disk"] = imageType{
-		name:     "disk.img",
+	partitionedDisk := imageType{
+		name:     "partitioned-disk",
+		filename: "disk.img",
 		mimeType: "application/octet-stream",
 		packages: []string{
 			"@core",
@@ -263,12 +248,13 @@ func New() *Fedora30 {
 		bootable:      true,
 		defaultSize:   2 * GigaByte,
 		assembler: func(uefi bool, size uint64) *osbuild.Assembler {
-			return r.qemuAssembler("raw", "disk.img", uefi, size)
+			return qemuAssembler("raw", "disk.img", uefi, size)
 		},
 	}
 
-	r.imageTypes["qcow2"] = imageType{
-		name:     "disk.qcow2",
+	qcow2ImageType := imageType{
+		name:     "qcow2",
+		filename: "disk.qcow2",
 		mimeType: "application/x-qemu-disk",
 		packages: []string{
 			"kernel-core",
@@ -290,12 +276,13 @@ func New() *Fedora30 {
 		bootable:      true,
 		defaultSize:   2 * GigaByte,
 		assembler: func(uefi bool, size uint64) *osbuild.Assembler {
-			return r.qemuAssembler("qcow2", "disk.qcow2", uefi, size)
+			return qemuAssembler("qcow2", "disk.qcow2", uefi, size)
 		},
 	}
 
-	r.imageTypes["openstack"] = imageType{
-		name:     "disk.qcow2",
+	openstackImgType := imageType{
+		name:     "openstack",
+		filename: "disk.qcow2",
 		mimeType: "application/x-qemu-disk",
 		packages: []string{
 			"@Core",
@@ -316,12 +303,13 @@ func New() *Fedora30 {
 		bootable:      true,
 		defaultSize:   2 * GigaByte,
 		assembler: func(uefi bool, size uint64) *osbuild.Assembler {
-			return r.qemuAssembler("qcow2", "disk.qcow2", uefi, size)
+			return qemuAssembler("qcow2", "disk.qcow2", uefi, size)
 		},
 	}
 
-	r.imageTypes["tar"] = imageType{
-		name:     "root.tar.xz",
+	tarImgType := imageType{
+		name:     "tar",
+		filename: "root.tar.xz",
 		mimeType: "application/x-tar",
 		packages: []string{
 			"policycoreutils",
@@ -337,11 +325,12 @@ func New() *Fedora30 {
 		kernelOptions: "ro biosdevname=0 net.ifnames=0",
 		bootable:      false,
 		defaultSize:   2 * GigaByte,
-		assembler:     func(uefi bool, size uint64) *osbuild.Assembler { return r.tarAssembler("root.tar.xz", "xz") },
+		assembler:     func(uefi bool, size uint64) *osbuild.Assembler { return tarAssembler("root.tar.xz", "xz") },
 	}
 
-	r.imageTypes["vhd"] = imageType{
-		name:     "disk.vhd",
+	vhdImgType := imageType{
+		name:     "vhd",
+		filename: "disk.vhd",
 		mimeType: "application/x-vhd",
 		packages: []string{
 			"@Core",
@@ -373,12 +362,13 @@ func New() *Fedora30 {
 		bootable:      true,
 		defaultSize:   2 * GigaByte,
 		assembler: func(uefi bool, size uint64) *osbuild.Assembler {
-			return r.qemuAssembler("vpc", "disk.vhd", uefi, size)
+			return qemuAssembler("vpc", "disk.vhd", uefi, size)
 		},
 	}
 
-	r.imageTypes["vmdk"] = imageType{
-		name:     "disk.vmdk",
+	vmdkImgType := imageType{
+		name:     "vmdk",
+		filename: "disk.vmdk",
 		mimeType: "application/x-vmdk",
 		packages: []string{
 			"@core",
@@ -396,9 +386,66 @@ func New() *Fedora30 {
 		bootable:      true,
 		defaultSize:   2 * GigaByte,
 		assembler: func(uefi bool, size uint64) *osbuild.Assembler {
-			return r.qemuAssembler("vmdk", "disk.vmdk", uefi, size)
+			return qemuAssembler("vmdk", "disk.vmdk", uefi, size)
+		},
+	}
+
+	r := Fedora30{
+		imageTypes: map[string]imageType{},
+		buildPackages: []string{
+			"dnf",
+			"dosfstools",
+			"e2fsprogs",
+			"policycoreutils",
+			"qemu-img",
+			"systemd",
+			"tar",
+			"xz",
+		},
+	}
+	x8664 := arch{
+		distro: &r,
+		name:   "x86_64",
+		bootloaderPackages: []string{
+			"grub2-pc",
+		},
+		buildPackages: []string{
+			"grub2-pc",
 		},
 	}
+	x8664.setImageTypes(
+		amiImgType,
+		ext4FilesystemType,
+		partitionedDisk,
+		qcow2ImageType,
+		openstackImgType,
+		tarImgType,
+		vhdImgType,
+		vmdkImgType,
+	)
+
+	aarch64 := arch{
+		distro: &r,
+		name:   "aarch64",
+		bootloaderPackages: []string{
+			"dracut-config-generic",
+			"efibootmgr",
+			"grub2-efi-aa64",
+			"grub2-tools",
+			"shim-aa64",
+		},
+		uefi: true,
+	}
+	aarch64.setImageTypes(
+		amiImgType,
+		ext4FilesystemType,
+		partitionedDisk,
+		qcow2ImageType,
+		openstackImgType,
+		tarImgType,
+	)
+
+	r.setArches(x8664, aarch64)
 
 	return &r
 }
@@ -411,13 +458,6 @@ func (r *Fedora30) ModulePlatformID() string {
 	return modulePlatformID
 }
 
-func (r *Fedora30) FilenameFromType(outputFormat string) (string, string, error) {
-	if output, exists := r.imageTypes[outputFormat]; exists {
-		return output.name, output.mimeType, nil
-	}
-	return "", "", errors.New("invalid output format: " + outputFormat)
-}
-
 func sources(packages []rpmmd.PackageSpec) *osbuild.Sources {
 	files := &osbuild.FilesSource{
 		URLs: make(map[string]string),
@@ -430,11 +470,11 @@ func sources(packages []rpmmd.PackageSpec) *osbuild.Sources {
 	}
 }
 
-func (t *fedora30ImageType) pipeline(c *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, size uint64) (*osbuild.Pipeline, error) {
+func (t *imageType) pipeline(c *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, size uint64) (*osbuild.Pipeline, error) {
 	p := &osbuild.Pipeline{}
-	p.SetBuild(t.buildPipeline(repos, *t.arch.arch, buildPackageSpecs), "org.osbuild.fedora30")
+	p.SetBuild(t.buildPipeline(repos, *t.arch, buildPackageSpecs), "org.osbuild.fedora30")
 
-	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(*t.arch.arch, repos, packageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(*t.arch, repos, packageSpecs)))
 	p.AddStage(osbuild.NewFixBLSStage())
 
 	// TODO support setting all languages and install corresponding langpack-* package
@@ -477,13 +517,13 @@ func (t *fedora30ImageType) pipeline(c *blueprint.Customizations, repos []rpmmd.
 		p.AddStage(osbuild.NewGroupsStage(t.groupStageOptions(groups)))
 	}
 
-	if t.imageType.bootable {
-		p.AddStage(osbuild.NewFSTabStage(t.fsTabStageOptions(t.arch.arch.uefi)))
+	if t.bootable {
+		p.AddStage(osbuild.NewFSTabStage(t.fsTabStageOptions(t.arch.uefi)))
 	}
-	p.AddStage(osbuild.NewGRUB2Stage(t.grub2StageOptions(t.imageType.kernelOptions, c.GetKernel(), t.arch.arch.uefi)))
+	p.AddStage(osbuild.NewGRUB2Stage(t.grub2StageOptions(t.kernelOptions, c.GetKernel(), t.arch.uefi)))
 
-	if services := c.GetServices(); services != nil || t.imageType.enabledServices != nil {
-		p.AddStage(osbuild.NewSystemdStage(t.systemdStageOptions(t.imageType.enabledServices, t.imageType.disabledServices, services)))
+	if services := c.GetServices(); services != nil || t.enabledServices != nil {
+		p.AddStage(osbuild.NewSystemdStage(t.systemdStageOptions(t.enabledServices, t.disabledServices, services)))
 	}
 
 	if firewall := c.GetFirewall(); firewall != nil {
@@ -492,18 +532,18 @@ func (t *fedora30ImageType) pipeline(c *blueprint.Customizations, repos []rpmmd.
 
 	p.AddStage(osbuild.NewSELinuxStage(t.selinuxStageOptions()))
 
-	p.Assembler = t.imageType.assembler(t.arch.arch.uefi, size)
+	p.Assembler = t.assembler(t.arch.uefi, size)
 
 	return p, nil
 }
 
-func (r *fedora30ImageType) buildPipeline(repos []rpmmd.RepoConfig, arch arch, packageSpecs []rpmmd.PackageSpec) *osbuild.Pipeline {
+func (r *imageType) buildPipeline(repos []rpmmd.RepoConfig, arch arch, packageSpecs []rpmmd.PackageSpec) *osbuild.Pipeline {
 	p := &osbuild.Pipeline{}
 	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, repos, packageSpecs)))
 	return p
 }
 
-func (r *fedora30ImageType) rpmStageOptions(arch arch, repos []rpmmd.RepoConfig, specs []rpmmd.PackageSpec) *osbuild.RPMStageOptions {
+func (r *imageType) rpmStageOptions(arch arch, repos []rpmmd.RepoConfig, specs []rpmmd.PackageSpec) *osbuild.RPMStageOptions {
 	var gpgKeys []string
 	for _, repo := range repos {
 		if repo.GPGKey == "" {
@@ -523,7 +563,7 @@ func (r *fedora30ImageType) rpmStageOptions(arch arch, repos []rpmmd.RepoConfig,
 	}
 }
 
-func (r *fedora30ImageType) userStageOptions(users []blueprint.UserCustomization) (*osbuild.UsersStageOptions, error) {
+func (r *imageType) userStageOptions(users []blueprint.UserCustomization) (*osbuild.UsersStageOptions, error) {
 	options := osbuild.UsersStageOptions{
 		Users: make(map[string]osbuild.UsersStageOptionsUser),
 	}
@@ -563,7 +603,7 @@ func (r *fedora30ImageType) userStageOptions(users []blueprint.UserCustomization
 	return &options, nil
 }
 
-func (r *fedora30ImageType) groupStageOptions(groups []blueprint.GroupCustomization) *osbuild.GroupsStageOptions {
+func (r *imageType) groupStageOptions(groups []blueprint.GroupCustomization) *osbuild.GroupsStageOptions {
 	options := osbuild.GroupsStageOptions{
 		Groups: map[string]osbuild.GroupsStageOptionsGroup{},
 	}
@@ -583,7 +623,7 @@ func (r *fedora30ImageType) groupStageOptions(groups []blueprint.GroupCustomizat
 	return &options
 }
 
-func (r *fedora30ImageType) firewallStageOptions(firewall *blueprint.FirewallCustomization) *osbuild.FirewallStageOptions {
+func (r *imageType) firewallStageOptions(firewall *blueprint.FirewallCustomization) *osbuild.FirewallStageOptions {
 	options := osbuild.FirewallStageOptions{
 		Ports: firewall.Ports,
 	}
@@ -596,7 +636,7 @@ func (r *fedora30ImageType) firewallStageOptions(firewall *blueprint.FirewallCus
 	return &options
 }
 
-func (r *fedora30ImageType) systemdStageOptions(enabledServices, disabledServices []string, s *blueprint.ServicesCustomization) *osbuild.SystemdStageOptions {
+func (r *imageType) systemdStageOptions(enabledServices, disabledServices []string, s *blueprint.ServicesCustomization) *osbuild.SystemdStageOptions {
 	if s != nil {
 		enabledServices = append(enabledServices, s.Enabled...)
 		disabledServices = append(disabledServices, s.Disabled...)
@@ -607,7 +647,7 @@ func (r *fedora30ImageType) systemdStageOptions(enabledServices, disabledService
 	}
 }
 
-func (r *fedora30ImageType) fsTabStageOptions(uefi bool) *osbuild.FSTabStageOptions {
+func (r *imageType) fsTabStageOptions(uefi bool) *osbuild.FSTabStageOptions {
 	options := osbuild.FSTabStageOptions{}
 	options.AddFilesystem("76a22bf4-f153-4541-b6c7-0332c0dfaeac", "ext4", "/", "defaults", 1, 1)
 	if uefi {
@@ -616,7 +656,7 @@ func (r *fedora30ImageType) fsTabStageOptions(uefi bool) *osbuild.FSTabStageOpti
 	return &options
 }
 
-func (r *fedora30ImageType) grub2StageOptions(kernelOptions string, kernel *blueprint.KernelCustomization, uefi bool) *osbuild.GRUB2StageOptions {
+func (r *imageType) grub2StageOptions(kernelOptions string, kernel *blueprint.KernelCustomization, uefi bool) *osbuild.GRUB2StageOptions {
 	id := uuid.MustParse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
 
 	if kernel != nil {
@@ -638,13 +678,13 @@ func (r *fedora30ImageType) grub2StageOptions(kernelOptions string, kernel *blue
 	}
 }
 
-func (r *fedora30ImageType) selinuxStageOptions() *osbuild.SELinuxStageOptions {
+func (r *imageType) selinuxStageOptions() *osbuild.SELinuxStageOptions {
 	return &osbuild.SELinuxStageOptions{
 		FileContexts: "etc/selinux/targeted/contexts/files/file_contexts",
 	}
 }
 
-func (r *Fedora30) qemuAssembler(format string, filename string, uefi bool, size uint64) *osbuild.Assembler {
+func qemuAssembler(format string, filename string, uefi bool, size uint64) *osbuild.Assembler {
 	var options osbuild.QEMUAssemblerOptions
 	if uefi {
 		fstype := uuid.MustParse("C12A7328-F81F-11D2-BA4B-00A0C93EC93B")
@@ -699,7 +739,7 @@ func (r *Fedora30) qemuAssembler(format string, filename string, uefi bool, size
 	return osbuild.NewQEMUAssembler(&options)
 }
 
-func (r *Fedora30) tarAssembler(filename, compression string) *osbuild.Assembler {
+func tarAssembler(filename, compression string) *osbuild.Assembler {
 	return osbuild.NewTarAssembler(
 		&osbuild.TarAssemblerOptions{
 			Filename:    filename,
@@ -707,7 +747,7 @@ func (r *Fedora30) tarAssembler(filename, compression string) *osbuild.Assembler
 		})
 }
 
-func (r *Fedora30) rawFSAssembler(filename string, size uint64) *osbuild.Assembler {
+func rawFSAssembler(filename string, size uint64) *osbuild.Assembler {
 	id := uuid.MustParse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
 	return osbuild.NewRawFSAssembler(
 		&osbuild.RawFSAssemblerOptions{
diff --git a/internal/distro/fedora31/distro.go b/internal/distro/fedora31/distro.go
index e84ed1a38..2d147249d 100644
--- a/internal/distro/fedora31/distro.go
+++ b/internal/distro/fedora31/distro.go
@@ -20,19 +20,13 @@ const modulePlatformID = "platform:f31"
 
 type Fedora31 struct {
 	arches        map[string]arch
-	imageTypes    map[string]imageType
 	buildPackages []string
 }
 
-type arch struct {
-	name               string
-	bootloaderPackages []string
-	buildPackages      []string
-	uefi               bool
-}
-
 type imageType struct {
+	arch             *arch
 	name             string
+	filename         string
 	mimeType         string
 	packages         []string
 	excludedPackages []string
@@ -44,16 +38,13 @@ type imageType struct {
 	assembler        func(uefi bool, size uint64) *osbuild.Assembler
 }
 
-type fedora31Arch struct {
-	name   string
-	distro *Fedora31
-	arch   *arch
-}
-
-type fedora31ImageType struct {
-	name      string
-	arch      *fedora31Arch
-	imageType *imageType
+type arch struct {
+	distro             *Fedora31
+	name               string
+	bootloaderPackages []string
+	buildPackages      []string
+	uefi               bool
+	imageTypes         map[string]imageType
 }
 
 func (d *Fedora31) ListArchs() []string {
@@ -71,77 +62,103 @@ func (d *Fedora31) GetArch(arch string) (distro.Arch, error) {
 		return nil, errors.New("invalid architecture: " + arch)
 	}
 
-	return &fedora31Arch{
-		name:   arch,
-		distro: d,
-		arch:   &a,
-	}, nil
+	return &a, nil
 }
 
-func (a *fedora31Arch) Name() string {
+func (d *Fedora31) setArches(arches ...arch) {
+	d.arches = map[string]arch{}
+	for _, a := range arches {
+		d.arches[a.name] = arch{
+			distro:             d,
+			name:               a.name,
+			bootloaderPackages: a.bootloaderPackages,
+			buildPackages:      a.buildPackages,
+			uefi:               a.uefi,
+			imageTypes:         a.imageTypes,
+		}
+	}
+}
+
+func (a *arch) Name() string {
 	return a.name
 }
 
-func (a *fedora31Arch) ListImageTypes() []string {
-	formats := make([]string, 0, len(a.distro.imageTypes))
-	for name := range a.distro.imageTypes {
+func (a *arch) ListImageTypes() []string {
+	formats := make([]string, 0, len(a.imageTypes))
+	for name := range a.imageTypes {
 		formats = append(formats, name)
 	}
 	sort.Strings(formats)
 	return formats
 }
 
-func (a *fedora31Arch) GetImageType(imageType string) (distro.ImageType, error) {
-	t, exists := a.distro.imageTypes[imageType]
+func (a *arch) GetImageType(imageType string) (distro.ImageType, error) {
+	t, exists := a.imageTypes[imageType]
 	if !exists {
 		return nil, errors.New("invalid image type: " + imageType)
 	}
 
-	return &fedora31ImageType{
-		name:      imageType,
-		arch:      a,
-		imageType: &t,
-	}, nil
+	return &t, nil
+}
+
+func (a *arch) setImageTypes(imageTypes ...imageType) {
+	a.imageTypes = map[string]imageType{}
+	for _, it := range imageTypes {
+		a.imageTypes[it.name] = imageType{
+			arch:             a,
+			name:             it.name,
+			filename:         it.filename,
+			mimeType:         it.mimeType,
+			packages:         it.packages,
+			excludedPackages: it.excludedPackages,
+			enabledServices:  it.enabledServices,
+			disabledServices: it.disabledServices,
+			kernelOptions:    it.kernelOptions,
+			bootable:         it.bootable,
+			defaultSize:      it.defaultSize,
+			assembler:        it.assembler,
+		}
+	}
 }
 
-func (t *fedora31ImageType) Name() string {
+func (t *imageType) Name() string {
 	return t.name
 }
 
-func (t *fedora31ImageType) Filename() string {
-	return t.imageType.name
+func (t *imageType) Filename() string {
+	return t.filename
 }
 
-func (t *fedora31ImageType) MIMEType() string {
-	return t.imageType.mimeType
+func (t *imageType) MIMEType() string {
+	return t.mimeType
 }
 
-func (t *fedora31ImageType) Size(size uint64) uint64 {
+func (t *imageType) Size(size uint64) uint64 {
 	const MegaByte = 1024 * 1024
 	// Microsoft Azure requires vhd images to be rounded up to the nearest MB
-	if t.name == "vhd" && size%MegaByte != 0 {
+	if t.filename == "vhd" && size%MegaByte != 0 {
 		size = (size/MegaByte + 1) * MegaByte
 	}
 	if size == 0 {
-		size = t.imageType.defaultSize
+		size = t.defaultSize
 	}
 	return size
 }
 
-func (t *fedora31ImageType) BasePackages() ([]string, []string) {
-	packages := t.imageType.packages
-	if t.imageType.bootable {
-		packages = append(packages, t.arch.arch.bootloaderPackages...)
+func (t *imageType) BasePackages() ([]string, []string) {
+	packages := t.packages
+	if t.bootable {
+		packages = append(packages, t.arch.bootloaderPackages...)
 	}
 
-	return packages, t.imageType.excludedPackages
+	return packages, t.excludedPackages
 }
 
-func (t *fedora31ImageType) BuildPackages() []string {
-	return append(t.arch.distro.buildPackages, t.arch.arch.buildPackages...)
+func (t *imageType) BuildPackages() []string {
+	return append(t.arch.distro.buildPackages, t.arch.buildPackages...)
 }
 
-func (t *fedora31ImageType) Manifest(c *blueprint.Customizations,
+func (t *imageType) Manifest(c *blueprint.Customizations,
 	repos []rpmmd.RepoConfig,
 	packageSpecs,
 	buildPackageSpecs []rpmmd.PackageSpec,
@@ -160,44 +177,9 @@ func (t *fedora31ImageType) Manifest(c *blueprint.Customizations,
 func New() *Fedora31 {
 	const GigaByte = 1024 * 1024 * 1024
 
-	r := Fedora31{
-		imageTypes: map[string]imageType{},
-		buildPackages: []string{
-			"dnf",
-			"dosfstools",
-			"e2fsprogs",
-			"policycoreutils",
-			"qemu-img",
-			"systemd",
-			"tar",
-			"xz",
-		},
-		arches: map[string]arch{
-			"x86_64": arch{
-				name: "x86_64",
-				bootloaderPackages: []string{
-					"grub2-pc",
-				},
-				buildPackages: []string{
-					"grub2-pc",
-				},
-			},
-			"aarch64": arch{
-				name: "aarch64",
-				bootloaderPackages: []string{
-					"dracut-config-generic",
-					"efibootmgr",
-					"grub2-efi-aa64",
-					"grub2-tools",
-					"shim-aa64",
-				},
-				uefi: true,
-			},
-		},
-	}
-
-	r.imageTypes["ami"] = imageType{
-		name:     "image.raw.xz",
+	amiImgType := imageType{
+		name:     "ami",
+		filename: "image.raw.xz",
 		mimeType: "application/octet-stream",
 		packages: []string{
 			"@Core",
@@ -221,12 +203,13 @@ func New() *Fedora31 {
 		bootable:      true,
 		defaultSize:   6 * GigaByte,
 		assembler: func(uefi bool, size uint64) *osbuild.Assembler {
-			return r.qemuAssembler("raw.xz", "image.raw.xz", uefi, size)
+			return qemuAssembler("raw.xz", "image.raw.xz", uefi, size)
 		},
 	}
 
-	r.imageTypes["ext4-filesystem"] = imageType{
-		name:     "filesystem.img",
+	ext4FilesystemType := imageType{
+		name:     "ext4-filesystem",
+		filename: "filesystem.img",
 		mimeType: "application/octet-stream",
 		packages: []string{
 			"policycoreutils",
@@ -242,11 +225,12 @@ func New() *Fedora31 {
 		kernelOptions: "ro biosdevname=0 net.ifnames=0",
 		bootable:      false,
 		defaultSize:   2 * GigaByte,
-		assembler:     func(uefi bool, size uint64) *osbuild.Assembler { return r.rawFSAssembler("filesystem.img", size) },
+		assembler:     func(uefi bool, size uint64) *osbuild.Assembler { return rawFSAssembler("filesystem.img", size) },
 	}
 
-	r.imageTypes["partitioned-disk"] = imageType{
-		name:     "disk.img",
+	partitionedDisk := imageType{
+		name:     "partitioned-disk",
+		filename: "disk.img",
 		mimeType: "application/octet-stream",
 		packages: []string{
 			"@core",
@@ -263,12 +247,13 @@ func New() *Fedora31 {
 		bootable:      true,
 		defaultSize:   2 * GigaByte,
 		assembler: func(uefi bool, size uint64) *osbuild.Assembler {
-			return r.qemuAssembler("raw", "disk.img", uefi, size)
+			return qemuAssembler("raw", "disk.img", uefi, size)
 		},
 	}
 
-	r.imageTypes["qcow2"] = imageType{
-		name:     "disk.qcow2",
+	qcow2ImageType := imageType{
+		name:     "qcow2",
+		filename: "disk.qcow2",
 		mimeType: "application/x-qemu-disk",
 		packages: []string{
 			"kernel-core",
@@ -290,12 +275,13 @@ func New() *Fedora31 {
 		bootable:      true,
 		defaultSize:   2 * GigaByte,
 		assembler: func(uefi bool, size uint64) *osbuild.Assembler {
-			return r.qemuAssembler("qcow2", "disk.qcow2", uefi, size)
+			return qemuAssembler("qcow2", "disk.qcow2", uefi, size)
 		},
 	}
 
-	r.imageTypes["openstack"] = imageType{
-		name:     "disk.qcow2",
+	openstackImgType := imageType{
+		name:     "openstack",
+		filename: "disk.qcow2",
 		mimeType: "application/x-qemu-disk",
 		packages: []string{
 			"@Core",
@@ -316,12 +302,13 @@ func New() *Fedora31 {
 		bootable:      true,
 		defaultSize:   2 * GigaByte,
 		assembler: func(uefi bool, size uint64) *osbuild.Assembler {
-			return r.qemuAssembler("qcow2", "disk.qcow2", uefi, size)
+			return qemuAssembler("qcow2", "disk.qcow2", uefi, size)
 		},
 	}
 
-	r.imageTypes["tar"] = imageType{
-		name:     "root.tar.xz",
+	tarImgType := imageType{
+		name:     "tar",
+		filename: "root.tar.xz",
 		mimeType: "application/x-tar",
 		packages: []string{
 			"policycoreutils",
@@ -337,11 +324,12 @@ func New() *Fedora31 {
 		kernelOptions: "ro biosdevname=0 net.ifnames=0",
 		bootable:      false,
 		defaultSize:   2 * GigaByte,
-		assembler:     func(uefi bool, size uint64) *osbuild.Assembler { return r.tarAssembler("root.tar.xz", "xz") },
+		assembler:     func(uefi bool, size uint64) *osbuild.Assembler { return tarAssembler("root.tar.xz", "xz") },
 	}
 
-	r.imageTypes["vhd"] = imageType{
-		name:     "disk.vhd",
+	vhdImgType := imageType{
+		name:     "vhd",
+		filename: "disk.vhd",
 		mimeType: "application/x-vhd",
 		packages: []string{
 			"@Core",
@@ -373,12 +361,13 @@ func New() *Fedora31 {
 		bootable:      true,
 		defaultSize:   2 * GigaByte,
 		assembler: func(uefi bool, size uint64) *osbuild.Assembler {
-			return r.qemuAssembler("vpc", "disk.vhd", uefi, size)
+			return qemuAssembler("vpc", "disk.vhd", uefi, size)
 		},
 	}
 
-	r.imageTypes["vmdk"] = imageType{
-		name:     "disk.vmdk",
+	vmdkImgType := imageType{
+		name:     "vmdk",
+		filename: "disk.vmdk",
 		mimeType: "application/x-vmdk",
 		packages: []string{
 			"@core",
@@ -396,9 +385,65 @@ func New() *Fedora31 {
 		bootable:      true,
 		defaultSize:   2 * GigaByte,
 		assembler: func(uefi bool, size uint64) *osbuild.Assembler {
-			return r.qemuAssembler("vmdk", "disk.vmdk", uefi, size)
+			return qemuAssembler("vmdk", "disk.vmdk", uefi, size)
+		},
+	}
+
+	r := Fedora31{
+		buildPackages: []string{
+			"dnf",
+			"dosfstools",
+			"e2fsprogs",
+			"policycoreutils",
+			"qemu-img",
+			"systemd",
+			"tar",
+			"xz",
+		},
+	}
+	x8664 := arch{
+		distro: &r,
+		name:   "x86_64",
+		bootloaderPackages: []string{
+			"grub2-pc",
+		},
+		buildPackages: []string{
+			"grub2-pc",
+		},
+	}
+	x8664.setImageTypes(
+		amiImgType,
+		ext4FilesystemType,
+		partitionedDisk,
+		qcow2ImageType,
+		openstackImgType,
+		tarImgType,
+		vhdImgType,
+		vmdkImgType,
+	)
+
+	aarch64 := arch{
+		distro: &r,
+		name:   "aarch64",
+		bootloaderPackages: []string{
+			"dracut-config-generic",
+			"efibootmgr",
+			"grub2-efi-aa64",
+			"grub2-tools",
+			"shim-aa64",
 		},
+		uefi: true,
 	}
+	aarch64.setImageTypes(
+		amiImgType,
+		ext4FilesystemType,
+		partitionedDisk,
+		qcow2ImageType,
+		openstackImgType,
+		tarImgType,
+	)
+
+	r.setArches(x8664, aarch64)
 
 	return &r
 }
@@ -411,13 +456,6 @@ func (r *Fedora31) ModulePlatformID() string {
 	return modulePlatformID
 }
 
-func (r *Fedora31) FilenameFromType(outputFormat string) (string, string, error) {
-	if output, exists := r.imageTypes[outputFormat]; exists {
-		return output.name, output.mimeType, nil
-	}
-	return "", "", errors.New("invalid output format: " + outputFormat)
-}
-
 func sources(packages []rpmmd.PackageSpec) *osbuild.Sources {
 	files := &osbuild.FilesSource{
 		URLs: make(map[string]string),
@@ -430,11 +468,11 @@ func sources(packages []rpmmd.PackageSpec) *osbuild.Sources {
 	}
 }
 
-func (t *fedora31ImageType) pipeline(c *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, size uint64) (*osbuild.Pipeline, error) {
+func (t *imageType) pipeline(c *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, size uint64) (*osbuild.Pipeline, error) {
 	p := &osbuild.Pipeline{}
-	p.SetBuild(t.buildPipeline(repos, *t.arch.arch, buildPackageSpecs), "org.osbuild.fedora31")
+	p.SetBuild(t.buildPipeline(repos, *t.arch, buildPackageSpecs), "org.osbuild.fedora31")
 
-	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(*t.arch.arch, repos, packageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(repos, packageSpecs)))
 	p.AddStage(osbuild.NewFixBLSStage())
 
 	// TODO support setting all languages and install corresponding langpack-* package
@@ -477,13 +515,13 @@ func (t *fedora31ImageType) pipeline(c *blueprint.Customizations, repos []rpmmd.
 		p.AddStage(osbuild.NewGroupsStage(t.groupStageOptions(groups)))
 	}
 
-	if t.imageType.bootable {
-		p.AddStage(osbuild.NewFSTabStage(t.fsTabStageOptions(t.arch.arch.uefi)))
+	if t.bootable {
+		p.AddStage(osbuild.NewFSTabStage(t.fsTabStageOptions(t.arch.uefi)))
 	}
-	p.AddStage(osbuild.NewGRUB2Stage(t.grub2StageOptions(t.imageType.kernelOptions, c.GetKernel(), t.arch.arch.uefi)))
+	p.AddStage(osbuild.NewGRUB2Stage(t.grub2StageOptions(t.kernelOptions, c.GetKernel(), t.arch.uefi)))
 
-	if services := c.GetServices(); services != nil || t.imageType.enabledServices != nil {
-		p.AddStage(osbuild.NewSystemdStage(t.systemdStageOptions(t.imageType.enabledServices, t.imageType.disabledServices, services)))
+	if services := c.GetServices(); services != nil || t.enabledServices != nil {
+		p.AddStage(osbuild.NewSystemdStage(t.systemdStageOptions(t.enabledServices, t.disabledServices, services)))
 	}
 
 	if firewall := c.GetFirewall(); firewall != nil {
@@ -492,18 +530,18 @@ func (t *fedora31ImageType) pipeline(c *blueprint.Customizations, repos []rpmmd.
 
 	p.AddStage(osbuild.NewSELinuxStage(t.selinuxStageOptions()))
 
-	p.Assembler = t.imageType.assembler(t.arch.arch.uefi, size)
+	p.Assembler = t.assembler(t.arch.uefi, size)
 
 	return p, nil
 }
 
-func (r *fedora31ImageType) buildPipeline(repos []rpmmd.RepoConfig, arch arch, buildPackageSpecs []rpmmd.PackageSpec) *osbuild.Pipeline {
+func (r *imageType) buildPipeline(repos []rpmmd.RepoConfig, arch arch, buildPackageSpecs []rpmmd.PackageSpec) *osbuild.Pipeline {
 	p := &osbuild.Pipeline{}
-	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, repos, buildPackageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(repos, buildPackageSpecs)))
 	return p
 }
 
-func (r *fedora31ImageType) rpmStageOptions(arch arch, repos []rpmmd.RepoConfig, specs []rpmmd.PackageSpec) *osbuild.RPMStageOptions {
+func (r *imageType) rpmStageOptions(repos []rpmmd.RepoConfig, specs []rpmmd.PackageSpec) *osbuild.RPMStageOptions {
 	var gpgKeys []string
 	for _, repo := range repos {
 		if repo.GPGKey == "" {
@@ -523,7 +561,7 @@ func (r *fedora31ImageType) rpmStageOptions(arch arch, repos []rpmmd.RepoConfig,
 	}
 }
 
-func (r *fedora31ImageType) userStageOptions(users []blueprint.UserCustomization) (*osbuild.UsersStageOptions, error) {
+func (r *imageType) userStageOptions(users []blueprint.UserCustomization) (*osbuild.UsersStageOptions, error) {
 	options := osbuild.UsersStageOptions{
 		Users: make(map[string]osbuild.UsersStageOptionsUser),
 	}
@@ -563,7 +601,7 @@ func (r *fedora31ImageType) userStageOptions(users []blueprint.UserCustomization
 	return &options, nil
 }
 
-func (r *fedora31ImageType) groupStageOptions(groups []blueprint.GroupCustomization) *osbuild.GroupsStageOptions {
+func (r *imageType) groupStageOptions(groups []blueprint.GroupCustomization) *osbuild.GroupsStageOptions {
 	options := osbuild.GroupsStageOptions{
 		Groups: map[string]osbuild.GroupsStageOptionsGroup{},
 	}
@@ -583,7 +621,7 @@ func (r *fedora31ImageType) groupStageOptions(groups []blueprint.GroupCustomizat
 	return &options
 }
 
-func (r *fedora31ImageType) firewallStageOptions(firewall *blueprint.FirewallCustomization) *osbuild.FirewallStageOptions {
+func (r *imageType) firewallStageOptions(firewall *blueprint.FirewallCustomization) *osbuild.FirewallStageOptions {
 	options := osbuild.FirewallStageOptions{
 		Ports: firewall.Ports,
 	}
@@ -596,7 +634,7 @@ func (r *fedora31ImageType) firewallStageOptions(firewall *blueprint.FirewallCus
 	return &options
 }
 
-func (r *fedora31ImageType) systemdStageOptions(enabledServices, disabledServices []string, s *blueprint.ServicesCustomization) *osbuild.SystemdStageOptions {
+func (r *imageType) systemdStageOptions(enabledServices, disabledServices []string, s *blueprint.ServicesCustomization) *osbuild.SystemdStageOptions {
 	if s != nil {
 		enabledServices = append(enabledServices, s.Enabled...)
 		disabledServices = append(disabledServices, s.Disabled...)
@@ -607,7 +645,7 @@ func (r *fedora31ImageType) systemdStageOptions(enabledServices, disabledService
 	}
 }
 
-func (r *fedora31ImageType) fsTabStageOptions(uefi bool) *osbuild.FSTabStageOptions {
+func (r *imageType) fsTabStageOptions(uefi bool) *osbuild.FSTabStageOptions {
 	options := osbuild.FSTabStageOptions{}
 	options.AddFilesystem("76a22bf4-f153-4541-b6c7-0332c0dfaeac", "ext4", "/", "defaults", 1, 1)
 	if uefi {
@@ -616,7 +654,7 @@ func (r *fedora31ImageType) fsTabStageOptions(uefi bool) *osbuild.FSTabStageOpti
 	return &options
 }
 
-func (r *fedora31ImageType) grub2StageOptions(kernelOptions string, kernel *blueprint.KernelCustomization, uefi bool) *osbuild.GRUB2StageOptions {
+func (r *imageType) grub2StageOptions(kernelOptions string, kernel *blueprint.KernelCustomization, uefi bool) *osbuild.GRUB2StageOptions {
 	id := uuid.MustParse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
 
 	if kernel != nil {
@@ -638,13 +676,13 @@ func (r *fedora31ImageType) grub2StageOptions(kernelOptions string, kernel *blue
 	}
 }
 
-func (r *fedora31ImageType) selinuxStageOptions() *osbuild.SELinuxStageOptions {
+func (r *imageType) selinuxStageOptions() *osbuild.SELinuxStageOptions {
 	return &osbuild.SELinuxStageOptions{
 		FileContexts: "etc/selinux/targeted/contexts/files/file_contexts",
 	}
 }
 
-func (r *Fedora31) qemuAssembler(format string, filename string, uefi bool, size uint64) *osbuild.Assembler {
+func qemuAssembler(format string, filename string, uefi bool, size uint64) *osbuild.Assembler {
 	var options osbuild.QEMUAssemblerOptions
 	if uefi {
 		fstype := uuid.MustParse("C12A7328-F81F-11D2-BA4B-00A0C93EC93B")
@@ -699,7 +737,7 @@ func (r *Fedora31) qemuAssembler(format string, filename string, uefi bool, size
 	return osbuild.NewQEMUAssembler(&options)
 }
 
-func (r *Fedora31) tarAssembler(filename, compression string) *osbuild.Assembler {
+func tarAssembler(filename, compression string) *osbuild.Assembler {
 	return osbuild.NewTarAssembler(
 		&osbuild.TarAssemblerOptions{
 			Filename:    filename,
@@ -707,7 +745,7 @@ func (r *Fedora31) tarAssembler(filename, compression string) *osbuild.Assembler
 		})
 }
 
-func (r *Fedora31) rawFSAssembler(filename string, size uint64) *osbuild.Assembler {
+func rawFSAssembler(filename string, size uint64) *osbuild.Assembler {
 	id := uuid.MustParse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
 	return osbuild.NewRawFSAssembler(
 		&osbuild.RawFSAssemblerOptions{
diff --git a/internal/distro/fedora32/distro.go b/internal/distro/fedora32/distro.go
index 26d0ea76d..0fbdbd3d7 100644
--- a/internal/distro/fedora32/distro.go
+++ b/internal/distro/fedora32/distro.go
@@ -25,14 +25,18 @@ type Fedora32 struct {
 }
 
 type arch struct {
+	distro             *Fedora32
 	name               string
 	bootloaderPackages []string
 	buildPackages      []string
 	uefi               bool
+	imageTypes         map[string]imageType
 }
 
 type imageType struct {
+	arch             *arch
 	name             string
+	filename         string
 	mimeType         string
 	packages         []string
 	excludedPackages []string
@@ -44,18 +48,6 @@ type imageType struct {
 	assembler        func(uefi bool, size uint64) *osbuild.Assembler
 }
 
-type fedora32Arch struct {
-	name   string
-	distro *Fedora32
-	arch   *arch
-}
-
-type fedora32ImageType struct {
-	name      string
-	arch      *fedora32Arch
-	imageType *imageType
-}
-
 func (d *Fedora32) ListArchs() []string {
 	archs := make([]string, 0, len(d.arches))
 	for name := range d.arches {
@@ -71,77 +63,103 @@ func (d *Fedora32) GetArch(arch string) (distro.Arch, error) {
 		return nil, errors.New("invalid architecture: " + arch)
 	}
 
-	return &fedora32Arch{
-		name:   arch,
-		distro: d,
-		arch:   &a,
-	}, nil
+	return &a, nil
 }
 
-func (a *fedora32Arch) Name() string {
+func (d *Fedora32) setArches(arches ...arch) {
+	d.arches = map[string]arch{}
+	for _, a := range arches {
+		d.arches[a.name] = arch{
+			distro:             d,
+			name:               a.name,
+			bootloaderPackages: a.bootloaderPackages,
+			buildPackages:      a.buildPackages,
+			uefi:               a.uefi,
+			imageTypes:         a.imageTypes,
+		}
+	}
+}
+
+func (a *arch) Name() string {
 	return a.name
 }
 
-func (a *fedora32Arch) ListImageTypes() []string {
-	formats := make([]string, 0, len(a.distro.imageTypes))
-	for name := range a.distro.imageTypes {
+func (a *arch) ListImageTypes() []string {
+	formats := make([]string, 0, len(a.imageTypes))
+	for name := range a.imageTypes {
 		formats = append(formats, name)
 	}
 	sort.Strings(formats)
 	return formats
 }
 
-func (a *fedora32Arch) GetImageType(imageType string) (distro.ImageType, error) {
-	t, exists := a.distro.imageTypes[imageType]
+func (a *arch) GetImageType(imageType string) (distro.ImageType, error) {
+	t, exists := a.imageTypes[imageType]
 	if !exists {
 		return nil, errors.New("invalid image type: " + imageType)
 	}
 
-	return &fedora32ImageType{
-		name:      imageType,
-		arch:      a,
-		imageType: &t,
-	}, nil
+	return &t, nil
+}
+
+func (a *arch) setImageTypes(imageTypes ...imageType) {
+	a.imageTypes = map[string]imageType{}
+	for _, it := range imageTypes {
+		a.imageTypes[it.name] = imageType{
+			arch:             a,
+			name:             it.name,
+			filename:         it.filename,
+			mimeType:         it.mimeType,
+			packages:         it.packages,
+			excludedPackages: it.excludedPackages,
+			enabledServices:  it.enabledServices,
+			disabledServices: it.disabledServices,
+			kernelOptions:    it.kernelOptions,
+			bootable:         it.bootable,
+			defaultSize:      it.defaultSize,
+			assembler:        it.assembler,
+		}
+	}
 }
 
-func (t *fedora32ImageType) Name() string {
+func (t *imageType) Name() string {
 	return t.name
 }
 
-func (t *fedora32ImageType) Filename() string {
-	return t.imageType.name
+func (t *imageType) Filename() string {
+	return t.filename
 }
 
-func (t *fedora32ImageType) MIMEType() string {
-	return t.imageType.mimeType
+func (t *imageType) MIMEType() string {
+	return t.mimeType
 }
 
-func (t *fedora32ImageType) Size(size uint64) uint64 {
+func (t *imageType) Size(size uint64) uint64 {
 	const MegaByte = 1024 * 1024
 	// Microsoft Azure requires vhd images to be rounded up to the nearest MB
 	if t.name == "vhd" && size%MegaByte != 0 {
 		size = (size/MegaByte + 1) * MegaByte
 	}
 	if size == 0 {
-		size = t.imageType.defaultSize
+		size = t.defaultSize
 	}
 	return size
 }
 
-func (t *fedora32ImageType) BasePackages() ([]string, []string) {
-	packages := t.imageType.packages
-	if t.imageType.bootable {
-		packages = append(packages, t.arch.arch.bootloaderPackages...)
+func (t *imageType) BasePackages() ([]string, []string) {
+	packages := t.packages
+	if t.bootable {
+		packages = append(packages, t.arch.bootloaderPackages...)
 	}
 
-	return packages, t.imageType.excludedPackages
+	return packages, t.excludedPackages
 }
 
-func (t *fedora32ImageType) BuildPackages() []string {
-	return append(t.arch.distro.buildPackages, t.arch.arch.buildPackages...)
+func (t *imageType) BuildPackages() []string {
+	return append(t.arch.distro.buildPackages, t.arch.buildPackages...)
 }
 
-func (t *fedora32ImageType) Manifest(c *blueprint.Customizations,
+func (t *imageType) Manifest(c *blueprint.Customizations,
 	repos []rpmmd.RepoConfig,
 	packageSpecs,
 	buildPackageSpecs []rpmmd.PackageSpec,
@@ -160,44 +178,9 @@ func (t *fedora32ImageType) Manifest(c *blueprint.Customizations,
 func New() *Fedora32 {
 	const GigaByte = 1024 * 1024 * 1024
 
-	r := Fedora32{
-		imageTypes: map[string]imageType{},
-		buildPackages: []string{
-			"dnf",
-			"dosfstools",
-			"e2fsprogs",
-			"policycoreutils",
-			"qemu-img",
-			"systemd",
-			"tar",
-			"xz",
-		},
-		arches: map[string]arch{
-			"x86_64": arch{
-				name: "x86_64",
-				bootloaderPackages: []string{
-					"grub2-pc",
-				},
-				buildPackages: []string{
-					"grub2-pc",
-				},
-			},
-			"aarch64": arch{
-				name: "aarch64",
-				bootloaderPackages: []string{
-					"dracut-config-generic",
-					"efibootmgr",
-					"grub2-efi-aa64",
-					"grub2-tools",
-					"shim-aa64",
-				},
-				uefi: true,
-			},
-		},
-	}
-
-	r.imageTypes["ami"] = imageType{
-		name:     "image.raw.xz",
+	amiImgType := imageType{
+		name:     "ami",
+		filename: "image.raw.xz",
 		mimeType: "application/octet-stream",
 		packages: []string{
 			"@Core",
@@ -221,12 +204,13 @@ func New() *Fedora32 {
 		bootable:      true,
 		defaultSize:   6 * GigaByte,
 		assembler: func(uefi bool, size uint64) *osbuild.Assembler {
-			return r.qemuAssembler("raw.xz", "image.raw.xz", uefi, size)
+			return qemuAssembler("raw.xz", "image.raw.xz", uefi, size)
 		},
 	}
 
-	r.imageTypes["ext4-filesystem"] = imageType{
-		name:     "filesystem.img",
+	ext4FilesystemType := imageType{
+		name:     "ext4-filesystem",
+		filename: "filesystem.img",
 		mimeType: "application/octet-stream",
 		packages: []string{
 			"policycoreutils",
@@ -242,11 +226,12 @@ func New() *Fedora32 {
 		kernelOptions: "ro biosdevname=0 net.ifnames=0",
 		bootable:      false,
 		defaultSize:   2 * GigaByte,
-		assembler:     func(uefi bool, size uint64) *osbuild.Assembler { return r.rawFSAssembler("filesystem.img", size) },
+		assembler:     func(uefi bool, size uint64) *osbuild.Assembler { return rawFSAssembler("filesystem.img", size) },
 	}
 
-	r.imageTypes["partitioned-disk"] = imageType{
-		name:     "disk.img",
+	partitionedDisk := imageType{
+		name:     "partitioned-disk",
+		filename: "disk.img",
 		mimeType: "application/octet-stream",
 		packages: []string{
 			"@core",
@@ -263,12 +248,13 @@ func New() *Fedora32 {
 		bootable:      true,
 		defaultSize:   2 * GigaByte,
 		assembler: func(uefi bool, size uint64) *osbuild.Assembler {
-			return r.qemuAssembler("raw", "disk.img", uefi, size)
+			return qemuAssembler("raw", "disk.img", uefi, size)
 		},
 	}
 
-	r.imageTypes["qcow2"] = imageType{
-		name:     "disk.qcow2",
+	qcow2ImageType := imageType{
+		name:     "qcow2",
+		filename: "disk.qcow2",
 		mimeType: "application/x-qemu-disk",
 		packages: []string{
 			"kernel-core",
@@ -290,12 +276,13 @@ func New() *Fedora32 {
 		bootable:      true,
 		defaultSize:   2 * GigaByte,
 		assembler: func(uefi bool, size uint64) *osbuild.Assembler {
-			return r.qemuAssembler("qcow2", "disk.qcow2", uefi, size)
+			return qemuAssembler("qcow2", "disk.qcow2", uefi, size)
 		},
 	}
 
-	r.imageTypes["openstack"] = imageType{
-		name:     "disk.qcow2",
+	openstackImgType := imageType{
+		name:     "openstack",
+		filename: "disk.qcow2",
 		mimeType: "application/x-qemu-disk",
 		packages: []string{
 			"@Core",
@@ -316,12 +303,13 @@ func New() *Fedora32 {
 		bootable:      true,
 		defaultSize:   2 * GigaByte,
 		assembler: func(uefi bool, size uint64) *osbuild.Assembler {
-			return r.qemuAssembler("qcow2", "disk.qcow2", uefi, size)
+			return qemuAssembler("qcow2", "disk.qcow2", uefi, size)
 		},
 	}
 
-	r.imageTypes["tar"] = imageType{
-		name:     "root.tar.xz",
+	tarImgType := imageType{
+		name:     "tar",
+		filename: "root.tar.xz",
 		mimeType: "application/x-tar",
 		packages: []string{
 			"policycoreutils",
@@ -337,11 +325,12 @@ func New() *Fedora32 {
 		kernelOptions: "ro biosdevname=0 net.ifnames=0",
 		bootable:      false,
 		defaultSize:   2 * GigaByte,
-		assembler:     func(uefi bool, size uint64) *osbuild.Assembler { return r.tarAssembler("root.tar.xz", "xz") },
+		assembler:     func(uefi bool, size uint64) *osbuild.Assembler { return tarAssembler("root.tar.xz", "xz") },
 	}
 
-	r.imageTypes["vhd"] = imageType{
-		name:     "disk.vhd",
+	vhdImgType := imageType{
+		name:     "vhd",
+		filename: "disk.vhd",
 		mimeType: "application/x-vhd",
 		packages: []string{
 			"@Core",
@@ -373,12 +362,13 @@ func New() *Fedora32 {
 		bootable:      true,
 		defaultSize:   2 * GigaByte,
 		assembler: func(uefi bool, size uint64) *osbuild.Assembler {
-			return r.qemuAssembler("vpc", "disk.vhd", uefi, size)
+			return qemuAssembler("vpc", "disk.vhd", uefi, size)
 		},
 	}
 
-	r.imageTypes["vmdk"] = imageType{
-		name:     "disk.vmdk",
+	vmdkImgType := imageType{
+		name:     "vmdk",
+		filename: "disk.vmdk",
 		mimeType: "application/x-vmdk",
 		packages: []string{
 			"@core",
@@ -396,9 +386,66 @@ func New() *Fedora32 {
 		bootable:      true,
 		defaultSize:   2 * GigaByte,
 		assembler: func(uefi bool, size uint64) *osbuild.Assembler {
-			return r.qemuAssembler("vmdk", "disk.vmdk", uefi, size)
+			return qemuAssembler("vmdk", "disk.vmdk", uefi, size)
+		},
+	}
+
+	r := Fedora32{
+		imageTypes: map[string]imageType{},
+		buildPackages: []string{
+			"dnf",
+			"dosfstools",
+			"e2fsprogs",
+			"policycoreutils",
+			"qemu-img",
+			"systemd",
+			"tar",
+			"xz",
+		},
+	}
+	x8664 := arch{
+		distro: &r,
+		name:   "x86_64",
+		bootloaderPackages: []string{
+			"grub2-pc",
+		},
+		buildPackages: []string{
+			"grub2-pc",
 		},
 	}
+	x8664.setImageTypes(
+		amiImgType,
+		ext4FilesystemType,
+		partitionedDisk,
+		qcow2ImageType,
+		openstackImgType,
+		tarImgType,
+		vhdImgType,
+		vmdkImgType,
+	)
+
+	aarch64 := arch{
+		distro: &r,
+		name:   "aarch64",
+		bootloaderPackages: []string{
+			"dracut-config-generic",
+			"efibootmgr",
+			"grub2-efi-aa64",
+			"grub2-tools",
+			"shim-aa64",
+		},
+		uefi: true,
+	}
+	aarch64.setImageTypes(
+		amiImgType,
+		ext4FilesystemType,
+		partitionedDisk,
+		qcow2ImageType,
+		openstackImgType,
+		tarImgType,
+	)
+
+	r.setArches(x8664, aarch64)
 
 	return &r
 }
@@ -411,13 +458,6 @@ func (r *Fedora32) ModulePlatformID() string {
 	return modulePlatformID
 }
 
-func (r *Fedora32) FilenameFromType(outputFormat string) (string, string, error) {
-	if output, exists := r.imageTypes[outputFormat]; exists {
-		return output.name, output.mimeType, nil
-	}
-	return "", "", errors.New("invalid output format: " + outputFormat)
-}
-
 func sources(packages []rpmmd.PackageSpec) *osbuild.Sources {
 	files := &osbuild.FilesSource{
 		URLs: make(map[string]string),
@@ -430,11 +470,11 @@ func sources(packages []rpmmd.PackageSpec) *osbuild.Sources {
 	}
 }
 
-func (t *fedora32ImageType) pipeline(c *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, size uint64) (*osbuild.Pipeline, error) {
+func (t *imageType) pipeline(c *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, size uint64) (*osbuild.Pipeline, error) {
 	p := &osbuild.Pipeline{}
-	p.SetBuild(t.buildPipeline(repos, *t.arch.arch, buildPackageSpecs), "org.osbuild.fedora32")
+	p.SetBuild(t.buildPipeline(repos, *t.arch, buildPackageSpecs), "org.osbuild.fedora32")
 
-	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(*t.arch.arch, repos, packageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(t.rpmStageOptions(*t.arch, repos, packageSpecs)))
 	p.AddStage(osbuild.NewFixBLSStage())
 
 	// TODO support setting all languages and install corresponding langpack-* package
@@ -477,13 +517,13 @@ func (t *fedora32ImageType) pipeline(c *blueprint.Customizations, repos []rpmmd.
 		p.AddStage(osbuild.NewGroupsStage(t.groupStageOptions(groups)))
 	}
 
-	if t.imageType.bootable {
-		p.AddStage(osbuild.NewFSTabStage(t.fsTabStageOptions(t.arch.arch.uefi)))
+	if t.bootable {
+		p.AddStage(osbuild.NewFSTabStage(t.fsTabStageOptions(t.arch.uefi)))
 	}
-	p.AddStage(osbuild.NewGRUB2Stage(t.grub2StageOptions(t.imageType.kernelOptions, c.GetKernel(), t.arch.arch.uefi)))
+	p.AddStage(osbuild.NewGRUB2Stage(t.grub2StageOptions(t.kernelOptions, c.GetKernel(), t.arch.uefi)))
 
-	if services := c.GetServices(); services != nil || t.imageType.enabledServices != nil {
-		p.AddStage(osbuild.NewSystemdStage(t.systemdStageOptions(t.imageType.enabledServices, t.imageType.disabledServices, services)))
+	if services := c.GetServices(); services != nil || t.enabledServices != nil {
+		p.AddStage(osbuild.NewSystemdStage(t.systemdStageOptions(t.enabledServices, t.disabledServices, services)))
 	}
 
 	if firewall := c.GetFirewall(); firewall != nil {
@@ -492,18 +532,18 @@ func (t *fedora32ImageType) pipeline(c *blueprint.Customizations, repos []rpmmd.
 
 	p.AddStage(osbuild.NewSELinuxStage(t.selinuxStageOptions()))
 
-	p.Assembler = t.imageType.assembler(t.arch.arch.uefi, size)
+	p.Assembler = t.assembler(t.arch.uefi, size)
 
 	return p, nil
 }
 
-func (r *fedora32ImageType) buildPipeline(repos []rpmmd.RepoConfig, arch arch, buildPackageSpecs []rpmmd.PackageSpec) *osbuild.Pipeline {
+func (r *imageType) buildPipeline(repos []rpmmd.RepoConfig, arch arch, buildPackageSpecs []rpmmd.PackageSpec) *osbuild.Pipeline {
 	p := &osbuild.Pipeline{}
 	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, repos, buildPackageSpecs)))
 	return p
 }
 
-func (r *fedora32ImageType) rpmStageOptions(arch arch, repos []rpmmd.RepoConfig, specs []rpmmd.PackageSpec) *osbuild.RPMStageOptions {
+func (r *imageType) rpmStageOptions(arch arch, repos []rpmmd.RepoConfig, specs []rpmmd.PackageSpec) *osbuild.RPMStageOptions {
 	var gpgKeys []string
 	for _, repo := range repos {
 		if repo.GPGKey == "" {
@@ -523,7 +563,7 @@ func (r *fedora32ImageType) rpmStageOptions(arch arch, repos []rpmmd.RepoConfig,
 	}
 }
 
-func (r *fedora32ImageType) userStageOptions(users []blueprint.UserCustomization) (*osbuild.UsersStageOptions, error) {
+func (r *imageType) userStageOptions(users []blueprint.UserCustomization) (*osbuild.UsersStageOptions, error) {
 	options := osbuild.UsersStageOptions{
 		Users: make(map[string]osbuild.UsersStageOptionsUser),
 	}
@@ -563,7 +603,7 @@ func (r *fedora32ImageType) userStageOptions(users []blueprint.UserCustomization
 	return &options, nil
 }
 
-func (r *fedora32ImageType) groupStageOptions(groups []blueprint.GroupCustomization) *osbuild.GroupsStageOptions {
+func (r *imageType) groupStageOptions(groups []blueprint.GroupCustomization) *osbuild.GroupsStageOptions {
 	options := osbuild.GroupsStageOptions{
 		Groups: map[string]osbuild.GroupsStageOptionsGroup{},
 	}
@@ -583,7 +623,7 @@ func (r *fedora32ImageType) groupStageOptions(groups []blueprint.GroupCustomizat
 	return &options
 }
 
-func (r *fedora32ImageType) firewallStageOptions(firewall *blueprint.FirewallCustomization) *osbuild.FirewallStageOptions {
+func (r *imageType) firewallStageOptions(firewall *blueprint.FirewallCustomization) *osbuild.FirewallStageOptions {
 	options := osbuild.FirewallStageOptions{
 		Ports: firewall.Ports,
 	}
@@ -596,7 +636,7 @@ func (r *fedora32ImageType) firewallStageOptions(firewall *blueprint.FirewallCus
 	return &options
 }
 
-func (r *fedora32ImageType) systemdStageOptions(enabledServices, disabledServices []string, s *blueprint.ServicesCustomization) *osbuild.SystemdStageOptions {
+func (r *imageType) systemdStageOptions(enabledServices, disabledServices []string, s *blueprint.ServicesCustomization) *osbuild.SystemdStageOptions {
 	if s != nil {
 		enabledServices = append(enabledServices, s.Enabled...)
 		disabledServices = append(disabledServices, s.Disabled...)
@@ -607,7 +647,7 @@ func (r *fedora32ImageType) systemdStageOptions(enabledServices, disabledService
 	}
 }
 
-func (r *fedora32ImageType) fsTabStageOptions(uefi bool) *osbuild.FSTabStageOptions {
+func (r *imageType) fsTabStageOptions(uefi bool) *osbuild.FSTabStageOptions {
 	options := osbuild.FSTabStageOptions{}
 	options.AddFilesystem("76a22bf4-f153-4541-b6c7-0332c0dfaeac", "ext4", "/", "defaults", 1, 1)
 	if uefi {
@@ -616,7 +656,7 @@ func (r *fedora32ImageType) fsTabStageOptions(uefi bool) *osbuild.FSTabStageOpti
 	return &options
 }
 
-func (r *fedora32ImageType) grub2StageOptions(kernelOptions string, kernel *blueprint.KernelCustomization, uefi bool) *osbuild.GRUB2StageOptions {
+func (r *imageType) grub2StageOptions(kernelOptions string, kernel *blueprint.KernelCustomization, uefi bool) *osbuild.GRUB2StageOptions {
 	id := uuid.MustParse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
 
 	if kernel != nil {
@@ -638,13 +678,13 @@ func (r *fedora32ImageType) grub2StageOptions(kernelOptions string, kernel *blue
 	}
 }
 
-func (r *fedora32ImageType) selinuxStageOptions() *osbuild.SELinuxStageOptions {
+func (r *imageType) selinuxStageOptions() *osbuild.SELinuxStageOptions {
 	return &osbuild.SELinuxStageOptions{
 		FileContexts: "etc/selinux/targeted/contexts/files/file_contexts",
 	}
 }
 
-func (r *Fedora32) qemuAssembler(format string, filename string, uefi bool, size uint64) *osbuild.Assembler {
+func qemuAssembler(format string, filename string, uefi bool, size uint64) *osbuild.Assembler {
 	var options osbuild.QEMUAssemblerOptions
 	if uefi {
 		fstype := uuid.MustParse("C12A7328-F81F-11D2-BA4B-00A0C93EC93B")
@@ -699,7 +739,7 @@ func (r *Fedora32) qemuAssembler(format string, filename string, uefi bool, size
 	return osbuild.NewQEMUAssembler(&options)
 }
 
-func (r *Fedora32) tarAssembler(filename, compression string) *osbuild.Assembler {
+func tarAssembler(filename, compression string) *osbuild.Assembler {
 	return osbuild.NewTarAssembler(
 		&osbuild.TarAssemblerOptions{
 			Filename:    filename,
@@ -707,7 +747,7 @@ func (r *Fedora32) tarAssembler(filename, compression string) *osbuild.Assembler
 		})
 }
 
-func (r *Fedora32) rawFSAssembler(filename string, size uint64) *osbuild.Assembler {
+func rawFSAssembler(filename string, size uint64) *osbuild.Assembler {
 	id := uuid.MustParse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
 	return osbuild.NewRawFSAssembler(
 		&osbuild.RawFSAssemblerOptions{
diff --git a/internal/distro/rhel81/distro.go b/internal/distro/rhel81/distro.go
index ab80749ad..e1f082ead 100644
--- a/internal/distro/rhel81/distro.go
+++ b/internal/distro/rhel81/distro.go
@@ -550,13 +550,6 @@ func (r *RHEL81) ModulePlatformID() string {
 	return modulePlatformID
 }
 
-func (r *RHEL81) FilenameFromType(outputFormat string) (string, string, error) {
-	if output, exists := r.imageTypes[outputFormat]; exists {
-		return output.name, output.mimeType, nil
-	}
-	return "", "", errors.New("invalid output format: " + outputFormat)
-}
-
 func sources(packages []rpmmd.PackageSpec) *osbuild.Sources {
 	files := &osbuild.FilesSource{
 		URLs: make(map[string]string),
diff --git a/internal/distro/rhel82/distro.go b/internal/distro/rhel82/distro.go
index 3f69de9a8..f9d85f17d 100644
--- a/internal/distro/rhel82/distro.go
+++ b/internal/distro/rhel82/distro.go
@@ -550,13 +550,6 @@ func (r *RHEL82) ModulePlatformID() string {
 	return modulePlatformID
 }
 
-func (r *RHEL82) FilenameFromType(outputFormat string) (string, string, error) {
-	if output, exists := r.imageTypes[outputFormat]; exists {
-		return output.name, output.mimeType, nil
-	}
-	return "", "", errors.New("invalid output format: " + outputFormat)
-}
-
 func (r *RHEL82) BasePackages(outputFormat string, outputArchitecture string) ([]string, []string, error) {
 	output, exists := r.imageTypes[outputFormat]
 	if !exists {
diff --git a/internal/weldr/api.go b/internal/weldr/api.go
index 335dae581..68cdf9e54 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -1754,16 +1754,18 @@ func (api *API) composeImageHandler(writer http.ResponseWriter, request *http.Re
 
 	imageBuild := compose.ImageBuilds[0]
 	imageType, _ := imageBuild.ImageType.ToCompatString()
-	imageName, imageMime, err := api.distro.FilenameFromType(imageType)
-
+	imageTypeStruct, err := api.arch.GetImageType(imageType)
 	if err != nil {
 		errors := responseError{
-			ID:  "BadCompose",
-			Msg: fmt.Sprintf("Compose %s is ill-formed: output type %v is invalid for distro %s", uuidString, imageBuild.ImageType, api.distro.Name()),
+			ID: "BadCompose",
+			Msg: fmt.Sprintf("Compose %s is ill-formed: output type %v is invalid for distro %s on %s",
+				uuidString, imageBuild.ImageType, api.distro.Name(), api.arch.Name()),
 		}
 		statusResponseError(writer, http.StatusInternalServerError, errors)
 		return
 	}
+	imageName := imageTypeStruct.Filename()
+	imageMime := imageTypeStruct.MIMEType()
 
 	reader, fileSize, err := api.store.GetImageBuildImage(uuid, 0)
 


Test patch:
diff --git a/internal/distro/fedora30/distro_test.go b/internal/distro/fedora30/distro_test.go
index 3c72a0e94..b1d3bb9d0 100644
--- a/internal/distro/fedora30/distro_test.go
+++ b/internal/distro/fedora30/distro_test.go
@@ -1,9 +1,9 @@
 package fedora30_test
 
 import (
-	"testing"
-
 	"github.com/osbuild/osbuild-composer/internal/distro/fedora30"
+	"reflect"
+	"testing"
 )
 
 func TestFilenameFromType(t *testing.T) {
@@ -73,20 +73,67 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			f30 := fedora30.New()
-			got, got1, err := f30.FilenameFromType(tt.args.outputFormat)
+			dist := fedora30.New()
+			arch, _ := dist.GetArch("x86_64")
+			imgType, err := arch.GetImageType(tt.args.outputFormat)
 			if (err != nil) != tt.wantErr {
-				t.Errorf("FilenameFromType() error = %v, wantErr %v", err, tt.wantErr)
+				t.Errorf("Arch.GetImageType() error = %v, wantErr %v", err, tt.wantErr)
 				return
 			}
 			if !tt.wantErr {
+				got := imgType.Filename()
+				got1 := imgType.MIMEType()
 				if got != tt.want {
-					t.Errorf("FilenameFromType() got = %v, want %v", got, tt.want)
+					t.Errorf("ImageType.Filename()  got = %v, want %v", got, tt.want)
 				}
 				if got1 != tt.want1 {
-					t.Errorf("FilenameFromType() got1 = %v, want %v", got1, tt.want1)
+					t.Errorf("ImageType.MIMEType() got1 = %v, want %v", got1, tt.want1)
 				}
 			}
 		})
 	}
 }
+
+func TestImageType_BuildPackages(t *testing.T) {
+	x8664BuildPackages := []string{
+		"dnf",
+		"dosfstools",
+		"e2fsprogs",
+		"grub2-pc",
+		"policycoreutils",
+		"qemu-img",
+		"systemd",
+		"tar",
+		"xz",
+	}
+	aarch64BuildPackages := []string{
+		"dnf",
+		"dosfstools",
+		"e2fsprogs",
+		"policycoreutils",
+		"qemu-img",
+		"systemd",
+		"tar",
+		"xz",
+	}
+	buildPackages := map[string][]string{
+		"x86_64":  x8664BuildPackages,
+		"aarch64": aarch64BuildPackages,
+	}
+	d := fedora30.New()
+	for _, archLabel := range d.ListArchs() {
+		archStruct, err := d.GetArch(archLabel)
+		if err != nil {
+			t.Errorf("d.GetArch(%v) returned err = %v; expected nil", archLabel, err)
+			continue
+		}
+		for _, itLabel := range archStruct.ListImageTypes() {
+			itStruct, err := archStruct.GetImageType(itLabel)
+			if err != nil {
+				t.Errorf("d.GetArch(%v) returned err = %v; expected nil", archLabel, err)
+				continue
+			}
+			reflect.DeepEqual(itStruct.BuildPackages(), buildPackages[archLabel])
+		}
+	}
+}
diff --git a/internal/distro/fedora31/distro_test.go b/internal/distro/fedora31/distro_test.go
index 7332a10f4..2b2bf6d2a 100644
--- a/internal/distro/fedora31/distro_test.go
+++ b/internal/distro/fedora31/distro_test.go
@@ -1,9 +1,9 @@
 package fedora31_test
 
 import (
-	"testing"
-
 	"github.com/osbuild/osbuild-composer/internal/distro/fedora31"
+	"reflect"
+	"testing"
 )
 
 func TestFilenameFromType(t *testing.T) {
@@ -73,20 +73,67 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			f31 := fedora31.New()
-			got, got1, err := f31.FilenameFromType(tt.args.outputFormat)
+			dist := fedora31.New()
+			arch, _ := dist.GetArch("x86_64")
+			imgType, err := arch.GetImageType(tt.args.outputFormat)
 			if (err != nil) != tt.wantErr {
-				t.Errorf("FilenameFromType() error = %v, wantErr %v", err, tt.wantErr)
+				t.Errorf("Arch.GetImageType() error = %v, wantErr %v", err, tt.wantErr)
 				return
 			}
 			if !tt.wantErr {
+				got := imgType.Filename()
+				got1 := imgType.MIMEType()
 				if got != tt.want {
-					t.Errorf("FilenameFromType() got = %v, want %v", got, tt.want)
+					t.Errorf("ImageType.Filename()  got = %v, want %v", got, tt.want)
 				}
 				if got1 != tt.want1 {
-					t.Errorf("FilenameFromType() got1 = %v, want %v", got1, tt.want1)
+					t.Errorf("ImageType.MIMEType() got1 = %v, want %v", got1, tt.want1)
 				}
 			}
 		})
 	}
 }
+
+func TestImageType_BuildPackages(t *testing.T) {
+	x8664BuildPackages := []string{
+		"dnf",
+		"dosfstools",
+		"e2fsprogs",
+		"grub2-pc",
+		"policycoreutils",
+		"qemu-img",
+		"systemd",
+		"tar",
+		"xz",
+	}
+	aarch64BuildPackages := []string{
+		"dnf",
+		"dosfstools",
+		"e2fsprogs",
+		"policycoreutils",
+		"qemu-img",
+		"systemd",
+		"tar",
+		"xz",
+	}
+	buildPackages := map[string][]string{
+		"x86_64":  x8664BuildPackages,
+		"aarch64": aarch64BuildPackages,
+	}
+	d := fedora31.New()
+	for _, archLabel := range d.ListArchs() {
+		archStruct, err := d.GetArch(archLabel)
+		if err != nil {
+			t.Errorf("d.GetArch(%v) returned err = %v; expected nil", archLabel, err)
+			continue
+		}
+		for _, itLabel := range archStruct.ListImageTypes() {
+			itStruct, err := archStruct.GetImageType(itLabel)
+			if err != nil {
+				t.Errorf("d.GetArch(%v) returned err = %v; expected nil", archLabel, err)
+				continue
+			}
+			reflect.DeepEqual(itStruct.BuildPackages(), buildPackages[archLabel])
+		}
+	}
+}
diff --git a/internal/distro/fedora32/distro_test.go b/internal/distro/fedora32/distro_test.go
index 4c2f2bd8f..40ad6f0a3 100644
--- a/internal/distro/fedora32/distro_test.go
+++ b/internal/distro/fedora32/distro_test.go
@@ -1,9 +1,9 @@
 package fedora32_test
 
 import (
-	"testing"
-
 	"github.com/osbuild/osbuild-composer/internal/distro/fedora32"
+	"reflect"
+	"testing"
 )
 
 func TestFilenameFromType(t *testing.T) {
@@ -73,21 +73,67 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			f32 := fedora32.New()
-
-			got, got1, err := f32.FilenameFromType(tt.args.outputFormat)
+			dist := fedora32.New()
+			arch, _ := dist.GetArch("x86_64")
+			imgType, err := arch.GetImageType(tt.args.outputFormat)
 			if (err != nil) != tt.wantErr {
-				t.Errorf("FilenameFromType() error = %v, wantErr %v", err, tt.wantErr)
+				t.Errorf("Arch.GetImageType() error = %v, wantErr %v", err, tt.wantErr)
 				return
 			}
 			if !tt.wantErr {
+				got := imgType.Filename()
+				got1 := imgType.MIMEType()
 				if got != tt.want {
-					t.Errorf("FilenameFromType() got = %v, want %v", got, tt.want)
+					t.Errorf("ImageType.Filename()  got = %v, want %v", got, tt.want)
 				}
 				if got1 != tt.want1 {
-					t.Errorf("FilenameFromType() got1 = %v, want %v", got1, tt.want1)
+					t.Errorf("ImageType.MIMEType() got1 = %v, want %v", got1, tt.want1)
 				}
 			}
 		})
 	}
 }
+
+func TestImageType_BuildPackages(t *testing.T) {
+	x8664BuildPackages := []string{
+		"dnf",
+		"dosfstools",
+		"e2fsprogs",
+		"grub2-pc",
+		"policycoreutils",
+		"qemu-img",
+		"systemd",
+		"tar",
+		"xz",
+	}
+	aarch64BuildPackages := []string{
+		"dnf",
+		"dosfstools",
+		"e2fsprogs",
+		"policycoreutils",
+		"qemu-img",
+		"systemd",
+		"tar",
+		"xz",
+	}
+	buildPackages := map[string][]string{
+		"x86_64":  x8664BuildPackages,
+		"aarch64": aarch64BuildPackages,
+	}
+	d := fedora32.New()
+	for _, archLabel := range d.ListArchs() {
+		archStruct, err := d.GetArch(archLabel)
+		if err != nil {
+			t.Errorf("d.GetArch(%v) returned err = %v; expected nil", archLabel, err)
+			continue
+		}
+		for _, itLabel := range archStruct.ListImageTypes() {
+			itStruct, err := archStruct.GetImageType(itLabel)
+			if err != nil {
+				t.Errorf("d.GetArch(%v) returned err = %v; expected nil", archLabel, err)
+				continue
+			}
+			reflect.DeepEqual(itStruct.BuildPackages(), buildPackages[archLabel])
+		}
+	}
+}
diff --git a/internal/distro/fedoratest/distro.go b/internal/distro/fedoratest/distro.go
index 61232e54c..72c6ade49 100644
--- a/internal/distro/fedoratest/distro.go
+++ b/internal/distro/fedoratest/distro.go
@@ -104,11 +104,3 @@ func (d *FedoraTestDistro) Name() string {
 func (d *FedoraTestDistro) ModulePlatformID() string {
 	return modulePlatformID
 }
-
-func (d *FedoraTestDistro) FilenameFromType(outputFormat string) (string, string, error) {
-	if outputFormat == "qcow2" {
-		return "test.img", "application/x-test", nil
-	} else {
-		return "", "", errors.New("invalid output format: " + outputFormat)
-	}
-}
diff --git a/internal/distro/rhel81/distro_test.go b/internal/distro/rhel81/distro_test.go
index 6f10917b2..d495037a2 100644
--- a/internal/distro/rhel81/distro_test.go
+++ b/internal/distro/rhel81/distro_test.go
@@ -1,9 +1,8 @@
 package rhel81_test
 
 import (
-	"testing"
-
 	"github.com/osbuild/osbuild-composer/internal/distro/rhel81"
+	"testing"
 )
 
 func TestFilenameFromType(t *testing.T) {
@@ -73,18 +72,21 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			el81 := rhel81.New()
-			got, got1, err := el81.FilenameFromType(tt.args.outputFormat)
+			dist := rhel81.New()
+			arch, _ := dist.GetArch("x86_64")
+			imgType, err := arch.GetImageType(tt.args.outputFormat)
 			if (err != nil) != tt.wantErr {
-				t.Errorf("FilenameFromType() error = %v, wantErr %v", err, tt.wantErr)
+				t.Errorf("Arch.GetImageType() error = %v, wantErr %v", err, tt.wantErr)
 				return
 			}
 			if !tt.wantErr {
+				got := imgType.Filename()
+				got1 := imgType.MIMEType()
 				if got != tt.want {
-					t.Errorf("FilenameFromType() got = %v, want %v", got, tt.want)
+					t.Errorf("ImageType.Filename()  got = %v, want %v", got, tt.want)
 				}
 				if got1 != tt.want1 {
-					t.Errorf("FilenameFromType() got1 = %v, want %v", got1, tt.want1)
+					t.Errorf("ImageType.MIMEType() got1 = %v, want %v", got1, tt.want1)
 				}
 			}
 		})
diff --git a/internal/distro/rhel82/distro_test.go b/internal/distro/rhel82/distro_test.go
index 4874b40ac..96964ebd9 100644
--- a/internal/distro/rhel82/distro_test.go
+++ b/internal/distro/rhel82/distro_test.go
@@ -73,18 +73,21 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			el82 := rhel82.New()
-			got, got1, err := el82.FilenameFromType(tt.args.outputFormat)
+			dist := rhel82.New()
+			arch, _ := dist.GetArch("x86_64")
+			imgType, err := arch.GetImageType(tt.args.outputFormat)
 			if (err != nil) != tt.wantErr {
-				t.Errorf("FilenameFromType() error = %v, wantErr %v", err, tt.wantErr)
+				t.Errorf("Arch.GetImageType() error = %v, wantErr %v", err, tt.wantErr)
 				return
 			}
 			if !tt.wantErr {
+				got := imgType.Filename()
+				got1 := imgType.MIMEType()
 				if got != tt.want {
-					t.Errorf("FilenameFromType() got = %v, want %v", got, tt.want)
+					t.Errorf("ImageType.Filename()  got = %v, want %v", got, tt.want)
 				}
 				if got1 != tt.want1 {
-					t.Errorf("FilenameFromType() got1 = %v, want %v", got1, tt.want1)
+					t.Errorf("ImageType.MIMEType() got1 = %v, want %v", got1, tt.want1)
 				}
 			}
 		})