Repository: osbuild/osbuild-composer
Pull request #375 (2020-03-22 21:50:02)

Distro interfaces
Introduce new interfaces to represent a distro at a given architecture, and each image type for a architecture/distro pair. Methods on the ImageType object are used rather than passing in architecture and image type to methods on the distro objects, simplifying the code and making the need for much of the `common` package redundant.

As discussed with @msehnout and @larskarlitski .

teg: Thanks for the reviews! I addressed most, but left some for future work, just because there is so much clean-ups that can be done anyway.

@msehnout I have a pending patch doing all the renames. I will also delete all the redundant code from each of the distro packages (there is now a lot), so shouldn't add much overall.
msehnout: Travis is green, but forgot to tell Github. Merging it now so we can move forward.

Patch:
diff --git a/cmd/osbuild-composer/main.go b/cmd/osbuild-composer/main.go
index 5e4b39c96..9947a9983 100644
--- a/cmd/osbuild-composer/main.go
+++ b/cmd/osbuild-composer/main.go
@@ -4,12 +4,18 @@ import (
 	"crypto/tls"
 	"crypto/x509"
 	"flag"
-	"github.com/osbuild/osbuild-composer/internal/rcm"
 	"io/ioutil"
 	"log"
 	"os"
 	"path"
 
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora30"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora31"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora32"
+	"github.com/osbuild/osbuild-composer/internal/distro/rhel81"
+	"github.com/osbuild/osbuild-composer/internal/distro/rhel82"
+	"github.com/osbuild/osbuild-composer/internal/rcm"
+
 	"github.com/osbuild/osbuild-composer/internal/common"
 	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/jobqueue"
@@ -84,7 +90,7 @@ func main() {
 
 	rpm := rpmmd.NewRPMMD(path.Join(cacheDirectory, "rpmmd"))
 
-	distros, err := distro.NewDefaultRegistry()
+	distros, err := distro.NewRegistry(fedora30.New(), fedora31.New(), fedora32.New(), rhel81.New(), rhel82.New())
 	if err != nil {
 		log.Fatalf("Error loading distros: %v", err)
 	}
@@ -94,6 +100,11 @@ func main() {
 		log.Fatalf("Could not determine distro from host: " + err.Error())
 	}
 
+	arch, err := distribution.GetArch(common.CurrentArch())
+	if err != nil {
+		log.Fatalf("Host distro does not support host architecture: " + err.Error())
+	}
+
 	repoMap, err := rpmmd.LoadRepositories([]string{"/etc/osbuild-composer", "/usr/share/osbuild-composer"}, distribution.Name())
 	if err != nil {
 		log.Fatalf("Could not load repositories for %s: %v", distribution.Name(), err)
@@ -107,7 +118,7 @@ func main() {
 	store := store.New(&stateDir)
 
 	jobAPI := jobqueue.New(logger, store)
-	weldrAPI := weldr.New(rpm, common.CurrentArch(), distribution, repoMap[common.CurrentArch()], logger, store)
+	weldrAPI := weldr.New(rpm, arch, distribution, repoMap[common.CurrentArch()], logger, store)
 
 	go func() {
 		err := jobAPI.Serve(jobListener)
diff --git a/cmd/osbuild-pipeline/main.go b/cmd/osbuild-pipeline/main.go
index 8264d3117..1b65a9412 100644
--- a/cmd/osbuild-pipeline/main.go
+++ b/cmd/osbuild-pipeline/main.go
@@ -9,7 +9,11 @@ import (
 	"os"
 	"path"
 
-	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora30"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora31"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora32"
+	"github.com/osbuild/osbuild-composer/internal/distro/rhel81"
+	"github.com/osbuild/osbuild-composer/internal/distro/rhel82"
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/distro"
@@ -23,12 +27,12 @@ type rpmMD struct {
 }
 
 func main() {
-	var imageType string
+	var imageTypeArg string
 	var blueprintArg string
 	var archArg string
 	var distroArg string
 	var rpmmdArg bool
-	flag.StringVar(&imageType, "image-type", "", "image type, e.g. qcow2 or ami")
+	flag.StringVar(&imageTypeArg, "image-type", "", "image type, e.g. qcow2 or ami")
 	flag.StringVar(&archArg, "arch", "", "architecture to create image for, e.g. x86_64")
 	flag.StringVar(&distroArg, "distro", "", "distribution to create, e.g. fedora-30")
 	flag.BoolVar(&rpmmdArg, "rpmmd", false, "output rpmmd struct instead of pipeline manifest")
@@ -38,20 +42,11 @@ func main() {
 	blueprintArg = flag.Arg(0)
 
 	// Print help usage if one of the required arguments wasn't provided
-	if imageType == "" || archArg == "" || distroArg == "" {
+	if imageTypeArg == "" || archArg == "" || distroArg == "" {
 		flag.Usage()
 		return
 	}
 
-	// Validate architecture
-	if !common.ArchitectureExists(archArg) {
-		_, _ = fmt.Fprintf(os.Stderr, "The provided architecture (%s) is not supported. Use one of these:\n", archArg)
-		for _, arch := range common.ListArchitectures() {
-			_, _ = fmt.Fprintln(os.Stderr, " *", arch)
-		}
-		return
-	}
-
 	blueprint := &blueprint.Blueprint{}
 	if blueprintArg != "" {
 		var reader io.Reader
@@ -74,7 +69,7 @@ func main() {
 		}
 	}
 
-	distros, err := distro.NewDefaultRegistry()
+	distros, err := distro.NewRegistry(fedora30.New(), fedora31.New(), fedora32.New(), rhel81.New(), rhel82.New())
 	if err != nil {
 		panic(err)
 	}
@@ -88,6 +83,18 @@ func main() {
 		return
 	}
 
+	arch, err := d.GetArch(archArg)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "The provided architecture (%s) is not supported by distro %s.: %s\n", archArg, distroArg, err.Error())
+		return
+	}
+
+	imageType, err := arch.GetImageType(imageTypeArg)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "The provided image type (%s) is not supported by architecture %s on distro %s.: %s\n", imageTypeArg, archArg, distroArg, err.Error())
+		return
+	}
+
 	repos, err := rpmmd.LoadRepositories([]string{"."}, distroArg)
 	if err != nil {
 		panic(err)
@@ -105,10 +112,7 @@ func main() {
 		}
 	}
 
-	pkgs, exclude_pkgs, err := d.BasePackages(imageType, archArg)
-	if err != nil {
-		panic("could not get base packages: " + err.Error())
-	}
+	pkgs, exclude_pkgs := imageType.BasePackages()
 	packages = append(pkgs, packages...)
 
 	home, err := os.UserHomeDir()
@@ -122,10 +126,7 @@ func main() {
 		panic("Could not depsolve: " + err.Error())
 	}
 
-	buildPkgs, err := d.BuildPackages(archArg)
-	if err != nil {
-		panic("Could not get build packages: " + err.Error())
-	}
+	buildPkgs := imageType.BuildPackages()
 	buildPackageSpecs, _, err := rpmmd.Depsolve(buildPkgs, nil, repos[archArg], d.ModulePlatformID())
 	if err != nil {
 		panic("Could not depsolve build packages: " + err.Error())
@@ -143,8 +144,7 @@ func main() {
 			panic("could not marshal rpmmd struct into JSON")
 		}
 	} else {
-		size := d.GetSizeForOutputType(imageType, 0)
-		manifest, err := d.Manifest(blueprint.Customizations, repos[archArg], packageSpecs, buildPackageSpecs, archArg, imageType, size)
+		manifest, err := imageType.Manifest(blueprint.Customizations, repos[archArg], packageSpecs, buildPackageSpecs, imageType.Size(0))
 		if err != nil {
 			panic(err.Error())
 		}
diff --git a/internal/distro/distro.go b/internal/distro/distro.go
index 524c52ee2..1e02e9d59 100644
--- a/internal/distro/distro.go
+++ b/internal/distro/distro.go
@@ -9,106 +9,94 @@ import (
 	"sort"
 	"strings"
 
-	"github.com/osbuild/osbuild-composer/internal/common"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
-
-	"github.com/osbuild/osbuild-composer/internal/distro/fedora30"
-	"github.com/osbuild/osbuild-composer/internal/distro/fedora31"
-	"github.com/osbuild/osbuild-composer/internal/distro/fedora32"
-	"github.com/osbuild/osbuild-composer/internal/distro/rhel81"
-	"github.com/osbuild/osbuild-composer/internal/distro/rhel82"
 )
 
+// A Distro represents composer's notion of what a given distribution is.
 type Distro interface {
-	// Returns the name of the distro. This is the same name that was
-	// passed to New().
+	// Returns the name of the distro.
 	Name() string
 
-	// Return strong-typed distribution
-	Distribution() common.Distribution
-
 	// Returns the module platform id of the distro. This is used by DNF
 	// for modularity support.
 	ModulePlatformID() string
 
-	// Returns a sorted list of the output formats this distro supports.
-	ListOutputFormats() []string
+	// 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.
+type Arch interface {
+	// Returns the name of the architecture.
+	Name() string
+
+	// Returns a sorted list of the names of the image types this architecture
+	// supports.
+	ListImageTypes() []string
+
+	// Returns an object representing a given image format for this architecture,
+	// on this distro.
+	GetImageType(imageType string) (ImageType, error)
+}
+
+// An ImageType represents a given distribution's support for a given Image Type
+// for a given architecture.
+type ImageType interface {
+	// Returns the name of the image type.
+	Name() string
+
+	// Returns the canonical filename for the image type.
+	Filename() string
 
-	// Returns the proper image size for a given output format. If the size
+	// Retrns the MIME-type for the image type.
+	MIMEType() string
+
+	// Returns the proper image size for a given output format. If the input size
 	// is 0 the default value for the format will be returned.
-	GetSizeForOutputType(outputFormat string, size uint64) uint64
+	Size(size uint64) uint64
 
-	// Returns the base packages for a given output type and architecture
-	BasePackages(outputFormat, outputArchitecture string) ([]string, []string, error)
+	// Returns the default packages to include and exclude when making the image
+	// type.
+	BasePackages() ([]string, []string)
 
-	// Returns the build packages for a given output architecture
-	BuildPackages(outputArchitecture string) ([]string, error)
+	// Returns the build packages for the output type.
+	BuildPackages() []string
 
 	// Returns an osbuild manifest, containing the sources and pipeline necessary
-	// to generates an image in the given output format with all packages and
-	// customizations specified in the given blueprint.
-	Manifest(b *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, imageFormat string, size uint64) (*osbuild.Manifest, error)
-
-	// Returns a osbuild runner that can be used on this distro.
-	Runner() string
+	// to build an image, given output format with all packages and customizations
+	// specified in the given blueprint.
+	Manifest(b *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, size uint64) (*osbuild.Manifest, error)
 }
 
 type Registry struct {
-	distros map[common.Distribution]Distro
+	distros map[string]Distro
 }
 
 func NewRegistry(distros ...Distro) (*Registry, error) {
 	reg := &Registry{
-		distros: make(map[common.Distribution]Distro),
+		distros: make(map[string]Distro),
 	}
 	for _, distro := range distros {
-		distroTag := distro.Distribution()
-		if _, exists := reg.distros[distroTag]; exists {
+		name := distro.Name()
+		if _, exists := reg.distros[name]; exists {
 			return nil, fmt.Errorf("NewRegistry: passed two distros with the same name: %s", distro.Name())
 		}
-		reg.distros[distroTag] = distro
+		reg.distros[name] = distro
 	}
 	return reg, nil
 }
 
-// Create a new Registry containing all known distros.
-func NewDefaultRegistry() (*Registry, error) {
-	f30, err := fedora30.New()
-	if err != nil {
-		return nil, fmt.Errorf("error loading fedora30: %v", err)
-	}
-	f31, err := fedora31.New()
-	if err != nil {
-		return nil, fmt.Errorf("error loading fedora31: %v", err)
-	}
-	f32, err := fedora32.New()
-	if err != nil {
-		return nil, fmt.Errorf("error loading fedora32: %v", err)
-	}
-	el81, err := rhel81.New()
-	if err != nil {
-		return nil, fmt.Errorf("error loading rhel81: %v", err)
-	}
-	el82, err := rhel82.New()
-	if err != nil {
-		return nil, fmt.Errorf("error loading rhel82: %v", err)
-	}
-	return NewRegistry(f30, f31, f32, el81, el82)
-}
-
 func (r *Registry) GetDistro(name string) Distro {
-	distroTag, exists := common.DistributionFromString(name)
-	if !exists {
-		return nil
-	}
-	distro, ok := r.distros[distroTag]
+	distro, ok := r.distros[name]
 	if !ok {
 		return nil
 	}
@@ -116,7 +104,7 @@ func (r *Registry) GetDistro(name string) Distro {
 	return distro
 }
 
-// Returns the names of all distros in a Registry, sorted alphabetically.
+// List returns the names of all distros in a Registry, sorted alphabetically.
 func (r *Registry) List() []string {
 	list := []string{}
 	for _, distro := range r.distros {
diff --git a/internal/distro/fedora30/distro.go b/internal/distro/fedora30/distro.go
index 8809444a7..79c18953c 100644
--- a/internal/distro/fedora30/distro.go
+++ b/internal/distro/fedora30/distro.go
@@ -6,6 +6,7 @@ import (
 	"strconv"
 
 	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
 
 	"github.com/google/uuid"
@@ -15,12 +16,6 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
 
-type Fedora30 struct {
-	arches        map[string]arch
-	outputs       map[string]output
-	buildPackages []string
-}
-
 type arch struct {
 	Name               string
 	BootloaderPackages []string
@@ -44,7 +39,104 @@ type output struct {
 const Distro = common.Fedora30
 const ModulePlatformID = "platform:f30"
 
-func New() (*Fedora30, error) {
+type Fedora30 struct {
+	arches        map[string]arch
+	outputs       map[string]output
+	buildPackages []string
+}
+
+type Fedora30Arch struct {
+	name   string
+	distro *Fedora30
+	arch   *arch
+}
+
+type Fedora30ImageType struct {
+	name   string
+	arch   *Fedora30Arch
+	output *output
+}
+
+func (d *Fedora30) GetArch(arch string) (distro.Arch, error) {
+	a, exists := d.arches[arch]
+	if !exists {
+		return nil, errors.New("invalid architecture: " + arch)
+	}
+
+	return &Fedora30Arch{
+		name:   arch,
+		distro: d,
+		arch:   &a,
+	}, nil
+}
+
+func (a *Fedora30Arch) Name() string {
+	return a.name
+}
+
+func (a *Fedora30Arch) ListImageTypes() []string {
+	return a.distro.ListOutputFormats()
+}
+
+func (a *Fedora30Arch) GetImageType(imageType string) (distro.ImageType, error) {
+	t, exists := a.distro.outputs[imageType]
+	if !exists {
+		return nil, errors.New("invalid image type: " + imageType)
+	}
+
+	return &Fedora30ImageType{
+		name:   imageType,
+		arch:   a,
+		output: &t,
+	}, nil
+}
+
+func (t *Fedora30ImageType) Name() string {
+	return t.name
+}
+
+func (t *Fedora30ImageType) Filename() string {
+	return t.output.Name
+}
+
+func (t *Fedora30ImageType) MIMEType() string {
+	return t.output.MimeType
+}
+
+func (t *Fedora30ImageType) Size(size uint64) uint64 {
+	return t.arch.distro.GetSizeForOutputType(t.name, size)
+}
+
+func (t *Fedora30ImageType) BasePackages() ([]string, []string) {
+	packages := t.output.Packages
+	if t.output.Bootable {
+		packages = append(packages, t.arch.arch.BootloaderPackages...)
+	}
+
+	return packages, t.output.ExcludedPackages
+}
+
+func (t *Fedora30ImageType) BuildPackages() []string {
+	return append(t.arch.distro.buildPackages, t.arch.arch.BuildPackages...)
+}
+
+func (t *Fedora30ImageType) Manifest(c *blueprint.Customizations,
+	repos []rpmmd.RepoConfig,
+	packageSpecs,
+	buildPackageSpecs []rpmmd.PackageSpec,
+	size uint64) (*osbuild.Manifest, error) {
+	pipeline, err := t.arch.distro.pipeline(c, repos, packageSpecs, buildPackageSpecs, t.arch.name, t.name, size)
+	if err != nil {
+		return nil, err
+	}
+
+	return &osbuild.Manifest{
+		Sources:  *t.arch.distro.sources(append(packageSpecs, buildPackageSpecs...)),
+		Pipeline: *pipeline,
+	}, nil
+}
+
+func New() *Fedora30 {
 	const GigaByte = 1024 * 1024 * 1024
 
 	r := Fedora30{
@@ -286,7 +378,7 @@ func New() (*Fedora30, error) {
 		},
 	}
 
-	return &r, nil
+	return &r
 }
 
 func (r *Fedora30) Name() string {
diff --git a/internal/distro/fedora31/distro.go b/internal/distro/fedora31/distro.go
index 4a64ef55b..449bd03fd 100644
--- a/internal/distro/fedora31/distro.go
+++ b/internal/distro/fedora31/distro.go
@@ -6,6 +6,7 @@ import (
 	"strconv"
 
 	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
 
 	"github.com/google/uuid"
@@ -15,12 +16,6 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
 
-type Fedora31 struct {
-	arches        map[string]arch
-	outputs       map[string]output
-	buildPackages []string
-}
-
 type arch struct {
 	Name               string
 	BootloaderPackages []string
@@ -44,7 +39,104 @@ type output struct {
 const Distro = common.Fedora31
 const ModulePlatformID = "platform:f31"
 
-func New() (*Fedora31, error) {
+type Fedora31 struct {
+	arches        map[string]arch
+	outputs       map[string]output
+	buildPackages []string
+}
+
+type Fedora31Arch struct {
+	name   string
+	distro *Fedora31
+	arch   *arch
+}
+
+type Fedora31ImageType struct {
+	name   string
+	arch   *Fedora31Arch
+	output *output
+}
+
+func (d *Fedora31) GetArch(arch string) (distro.Arch, error) {
+	a, exists := d.arches[arch]
+	if !exists {
+		return nil, errors.New("invalid architecture: " + arch)
+	}
+
+	return &Fedora31Arch{
+		name:   arch,
+		distro: d,
+		arch:   &a,
+	}, nil
+}
+
+func (a *Fedora31Arch) Name() string {
+	return a.name
+}
+
+func (a *Fedora31Arch) ListImageTypes() []string {
+	return a.distro.ListOutputFormats()
+}
+
+func (a *Fedora31Arch) GetImageType(imageType string) (distro.ImageType, error) {
+	t, exists := a.distro.outputs[imageType]
+	if !exists {
+		return nil, errors.New("invalid image type: " + imageType)
+	}
+
+	return &Fedora31ImageType{
+		name:   imageType,
+		arch:   a,
+		output: &t,
+	}, nil
+}
+
+func (t *Fedora31ImageType) Name() string {
+	return t.name
+}
+
+func (t *Fedora31ImageType) Filename() string {
+	return t.output.Name
+}
+
+func (t *Fedora31ImageType) MIMEType() string {
+	return t.output.MimeType
+}
+
+func (t *Fedora31ImageType) Size(size uint64) uint64 {
+	return t.arch.distro.GetSizeForOutputType(t.name, size)
+}
+
+func (t *Fedora31ImageType) BasePackages() ([]string, []string) {
+	packages := t.output.Packages
+	if t.output.Bootable {
+		packages = append(packages, t.arch.arch.BootloaderPackages...)
+	}
+
+	return packages, t.output.ExcludedPackages
+}
+
+func (t *Fedora31ImageType) BuildPackages() []string {
+	return append(t.arch.distro.buildPackages, t.arch.arch.BuildPackages...)
+}
+
+func (t *Fedora31ImageType) Manifest(c *blueprint.Customizations,
+	repos []rpmmd.RepoConfig,
+	packageSpecs,
+	buildPackageSpecs []rpmmd.PackageSpec,
+	size uint64) (*osbuild.Manifest, error) {
+	pipeline, err := t.arch.distro.pipeline(c, repos, packageSpecs, buildPackageSpecs, t.arch.name, t.name, size)
+	if err != nil {
+		return nil, err
+	}
+
+	return &osbuild.Manifest{
+		Sources:  *t.arch.distro.sources(append(packageSpecs, buildPackageSpecs...)),
+		Pipeline: *pipeline,
+	}, nil
+}
+
+func New() *Fedora31 {
 	const GigaByte = 1024 * 1024 * 1024
 
 	r := Fedora31{
@@ -286,7 +378,7 @@ func New() (*Fedora31, error) {
 		},
 	}
 
-	return &r, nil
+	return &r
 }
 
 func (r *Fedora31) Name() string {
diff --git a/internal/distro/fedora32/distro.go b/internal/distro/fedora32/distro.go
index 9510cfa52..9bfe50429 100644
--- a/internal/distro/fedora32/distro.go
+++ b/internal/distro/fedora32/distro.go
@@ -6,6 +6,7 @@ import (
 	"strconv"
 
 	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
 
 	"github.com/google/uuid"
@@ -15,12 +16,6 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
 
-type Fedora32 struct {
-	arches        map[string]arch
-	outputs       map[string]output
-	buildPackages []string
-}
-
 type arch struct {
 	Name               string
 	BootloaderPackages []string
@@ -44,7 +39,104 @@ type output struct {
 const Distro = common.Fedora32
 const ModulePlatformID = "platform:f32"
 
-func New() (*Fedora32, error) {
+type Fedora32 struct {
+	arches        map[string]arch
+	outputs       map[string]output
+	buildPackages []string
+}
+
+type Fedora32Arch struct {
+	name   string
+	distro *Fedora32
+	arch   *arch
+}
+
+type Fedora32ImageType struct {
+	name   string
+	arch   *Fedora32Arch
+	output *output
+}
+
+func (d *Fedora32) GetArch(arch string) (distro.Arch, error) {
+	a, exists := d.arches[arch]
+	if !exists {
+		return nil, errors.New("invalid architecture: " + arch)
+	}
+
+	return &Fedora32Arch{
+		name:   arch,
+		distro: d,
+		arch:   &a,
+	}, nil
+}
+
+func (a *Fedora32Arch) Name() string {
+	return a.name
+}
+
+func (a *Fedora32Arch) ListImageTypes() []string {
+	return a.distro.ListOutputFormats()
+}
+
+func (a *Fedora32Arch) GetImageType(imageType string) (distro.ImageType, error) {
+	t, exists := a.distro.outputs[imageType]
+	if !exists {
+		return nil, errors.New("invalid image type: " + imageType)
+	}
+
+	return &Fedora32ImageType{
+		name:   imageType,
+		arch:   a,
+		output: &t,
+	}, nil
+}
+
+func (t *Fedora32ImageType) Name() string {
+	return t.name
+}
+
+func (t *Fedora32ImageType) Filename() string {
+	return t.output.Name
+}
+
+func (t *Fedora32ImageType) MIMEType() string {
+	return t.output.MimeType
+}
+
+func (t *Fedora32ImageType) Size(size uint64) uint64 {
+	return t.arch.distro.GetSizeForOutputType(t.name, size)
+}
+
+func (t *Fedora32ImageType) BasePackages() ([]string, []string) {
+	packages := t.output.Packages
+	if t.output.Bootable {
+		packages = append(packages, t.arch.arch.BootloaderPackages...)
+	}
+
+	return packages, t.output.ExcludedPackages
+}
+
+func (t *Fedora32ImageType) BuildPackages() []string {
+	return append(t.arch.distro.buildPackages, t.arch.arch.BuildPackages...)
+}
+
+func (t *Fedora32ImageType) Manifest(c *blueprint.Customizations,
+	repos []rpmmd.RepoConfig,
+	packageSpecs,
+	buildPackageSpecs []rpmmd.PackageSpec,
+	size uint64) (*osbuild.Manifest, error) {
+	pipeline, err := t.arch.distro.pipeline(c, repos, packageSpecs, buildPackageSpecs, t.arch.name, t.name, size)
+	if err != nil {
+		return nil, err
+	}
+
+	return &osbuild.Manifest{
+		Sources:  *t.arch.distro.sources(append(packageSpecs, buildPackageSpecs...)),
+		Pipeline: *pipeline,
+	}, nil
+}
+
+func New() *Fedora32 {
 	const GigaByte = 1024 * 1024 * 1024
 
 	r := Fedora32{
@@ -286,7 +378,7 @@ func New() (*Fedora32, error) {
 		},
 	}
 
-	return &r, nil
+	return &r
 }
 
 func (r *Fedora32) Name() string {
diff --git a/internal/distro/rhel81/distro.go b/internal/distro/rhel81/distro.go
index c8aacfe08..ed4287b13 100644
--- a/internal/distro/rhel81/distro.go
+++ b/internal/distro/rhel81/distro.go
@@ -6,6 +6,7 @@ import (
 	"strconv"
 
 	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
 
 	"github.com/google/uuid"
@@ -15,12 +16,6 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
 
-type RHEL81 struct {
-	arches        map[string]arch
-	outputs       map[string]output
-	buildPackages []string
-}
-
 type arch struct {
 	Name               string
 	BootloaderPackages []string
@@ -45,7 +40,104 @@ type output struct {
 const Distro = common.RHEL81
 const ModulePlatformID = "platform:el8"
 
-func New() (*RHEL81, error) {
+type RHEL81 struct {
+	arches        map[string]arch
+	outputs       map[string]output
+	buildPackages []string
+}
+
+type RHEL81Arch struct {
+	name   string
+	distro *RHEL81
+	arch   *arch
+}
+
+type RHEL81ImageType struct {
+	name   string
+	arch   *RHEL81Arch
+	output *output
+}
+
+func (d *RHEL81) GetArch(arch string) (distro.Arch, error) {
+	a, exists := d.arches[arch]
+	if !exists {
+		return nil, errors.New("invalid architecture: " + arch)
+	}
+
+	return &RHEL81Arch{
+		name:   arch,
+		distro: d,
+		arch:   &a,
+	}, nil
+}
+
+func (a *RHEL81Arch) Name() string {
+	return a.name
+}
+
+func (a *RHEL81Arch) ListImageTypes() []string {
+	return a.distro.ListOutputFormats()
+}
+
+func (a *RHEL81Arch) GetImageType(imageType string) (distro.ImageType, error) {
+	t, exists := a.distro.outputs[imageType]
+	if !exists {
+		return nil, errors.New("invalid image type: " + imageType)
+	}
+
+	return &RHEL81ImageType{
+		name:   imageType,
+		arch:   a,
+		output: &t,
+	}, nil
+}
+
+func (t *RHEL81ImageType) Name() string {
+	return t.name
+}
+
+func (t *RHEL81ImageType) Filename() string {
+	return t.output.Name
+}
+
+func (t *RHEL81ImageType) MIMEType() string {
+	return t.output.MimeType
+}
+
+func (t *RHEL81ImageType) Size(size uint64) uint64 {
+	return t.arch.distro.GetSizeForOutputType(t.name, size)
+}
+
+func (t *RHEL81ImageType) BasePackages() ([]string, []string) {
+	packages := t.output.Packages
+	if t.output.Bootable {
+		packages = append(packages, t.arch.arch.BootloaderPackages...)
+	}
+
+	return packages, t.output.ExcludedPackages
+}
+
+func (t *RHEL81ImageType) BuildPackages() []string {
+	return append(t.arch.distro.buildPackages, t.arch.arch.BuildPackages...)
+}
+
+func (t *RHEL81ImageType) Manifest(c *blueprint.Customizations,
+	repos []rpmmd.RepoConfig,
+	packageSpecs,
+	buildPackageSpecs []rpmmd.PackageSpec,
+	size uint64) (*osbuild.Manifest, error) {
+	pipeline, err := t.arch.distro.pipeline(c, repos, packageSpecs, buildPackageSpecs, t.arch.name, t.name, size)
+	if err != nil {
+		return nil, err
+	}
+
+	return &osbuild.Manifest{
+		Sources:  *t.arch.distro.sources(append(packageSpecs, buildPackageSpecs...)),
+		Pipeline: *pipeline,
+	}, nil
+}
+
+func New() *RHEL81 {
 	const GigaByte = 1024 * 1024 * 1024
 
 	r := RHEL81{
@@ -425,7 +517,7 @@ func New() (*RHEL81, error) {
 		},
 	}
 
-	return &r, nil
+	return &r
 }
 
 func (r *RHEL81) Name() string {
diff --git a/internal/distro/rhel82/distro.go b/internal/distro/rhel82/distro.go
index dc2a53fdb..e97e7fefe 100644
--- a/internal/distro/rhel82/distro.go
+++ b/internal/distro/rhel82/distro.go
@@ -6,6 +6,7 @@ import (
 	"strconv"
 
 	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
 
 	"github.com/google/uuid"
@@ -15,12 +16,6 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
 
-type RHEL82 struct {
-	arches        map[string]arch
-	outputs       map[string]output
-	buildPackages []string
-}
-
 type arch struct {
 	Name               string
 	BootloaderPackages []string
@@ -45,7 +40,104 @@ type output struct {
 const Distro = common.RHEL82
 const ModulePlatformID = "platform:el8"
 
-func New() (*RHEL82, error) {
+type RHEL82 struct {
+	arches        map[string]arch
+	outputs       map[string]output
+	buildPackages []string
+}
+
+type RHEL82Arch struct {
+	name   string
+	distro *RHEL82
+	arch   *arch
+}
+
+type RHEL82ImageType struct {
+	name   string
+	arch   *RHEL82Arch
+	output *output
+}
+
+func (d *RHEL82) GetArch(arch string) (distro.Arch, error) {
+	a, exists := d.arches[arch]
+	if !exists {
+		return nil, errors.New("invalid architecture: " + arch)
+	}
+
+	return &RHEL82Arch{
+		name:   arch,
+		distro: d,
+		arch:   &a,
+	}, nil
+}
+
+func (a *RHEL82Arch) Name() string {
+	return a.name
+}
+
+func (a *RHEL82Arch) ListImageTypes() []string {
+	return a.distro.ListOutputFormats()
+}
+
+func (a *RHEL82Arch) GetImageType(imageType string) (distro.ImageType, error) {
+	t, exists := a.distro.outputs[imageType]
+	if !exists {
+		return nil, errors.New("invalid image type: " + imageType)
+	}
+
+	return &RHEL82ImageType{
+		name:   imageType,
+		arch:   a,
+		output: &t,
+	}, nil
+}
+
+func (t *RHEL82ImageType) Name() string {
+	return t.name
+}
+
+func (t *RHEL82ImageType) Filename() string {
+	return t.output.Name
+}
+
+func (t *RHEL82ImageType) MIMEType() string {
+	return t.output.MimeType
+}
+
+func (t *RHEL82ImageType) Size(size uint64) uint64 {
+	return t.arch.distro.GetSizeForOutputType(t.name, size)
+}
+
+func (t *RHEL82ImageType) BasePackages() ([]string, []string) {
+	packages := t.output.Packages
+	if t.output.Bootable {
+		packages = append(packages, t.arch.arch.BootloaderPackages...)
+	}
+
+	return packages, t.output.ExcludedPackages
+}
+
+func (t *RHEL82ImageType) BuildPackages() []string {
+	return append(t.arch.distro.buildPackages, t.arch.arch.BuildPackages...)
+}
+
+func (t *RHEL82ImageType) Manifest(c *blueprint.Customizations,
+	repos []rpmmd.RepoConfig,
+	packageSpecs,
+	buildPackageSpecs []rpmmd.PackageSpec,
+	size uint64) (*osbuild.Manifest, error) {
+	pipeline, err := t.arch.distro.pipeline(c, repos, packageSpecs, buildPackageSpecs, t.arch.name, t.name, size)
+	if err != nil {
+		return nil, err
+	}
+
+	return &osbuild.Manifest{
+		Sources:  *t.arch.distro.sources(append(packageSpecs, buildPackageSpecs...)),
+		Pipeline: *pipeline,
+	}, nil
+}
+
+func New() *RHEL82 {
 	const GigaByte = 1024 * 1024 * 1024
 
 	r := RHEL82{
@@ -425,7 +517,7 @@ func New() (*RHEL82, error) {
 		},
 	}
 
-	return &r, nil
+	return &r
 }
 
 func (r *RHEL82) Name() string {
diff --git a/internal/rcm/api.go b/internal/rcm/api.go
index a02bc48a8..106811468 100644
--- a/internal/rcm/api.go
+++ b/internal/rcm/api.go
@@ -10,7 +10,6 @@ import (
 	"net/http"
 	"strings"
 
-	"github.com/osbuild/osbuild-composer/internal/common"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 
 	"github.com/google/uuid"
@@ -84,22 +83,14 @@ func notFoundHandler(writer http.ResponseWriter, request *http.Request) {
 
 // Depsolves packages and build packages for building an image for a given
 // distro, in the given architecture
-func depsolve(rpmmd rpmmd.RPMMD, distro distro.Distro, repos []rpmmd.RepoConfig, imageType, arch string) ([]rpmmd.PackageSpec, []rpmmd.PackageSpec, error) {
-	specs, excludeSpecs, err := distro.BasePackages(imageType, arch)
-	if err != nil {
-		return nil, nil, fmt.Errorf("Distro.BasePackages: %v", err)
-	}
-
+func depsolve(rpmmd rpmmd.RPMMD, distro distro.Distro, imageType distro.ImageType, repos []rpmmd.RepoConfig) ([]rpmmd.PackageSpec, []rpmmd.PackageSpec, error) {
+	specs, excludeSpecs := imageType.BasePackages()
 	packages, _, err := rpmmd.Depsolve(specs, excludeSpecs, repos, distro.ModulePlatformID())
 	if err != nil {
 		return nil, nil, fmt.Errorf("RPMMD.Depsolve: %v", err)
 	}
 
-	specs, err = distro.BuildPackages(arch)
-	if err != nil {
-		return nil, nil, fmt.Errorf("Distro.BuildPackages: %v", err)
-	}
-
+	specs = imageType.BuildPackages()
 	buildPackages, _, err := rpmmd.Depsolve(specs, nil, repos, distro.ModulePlatformID())
 	if err != nil {
 		return nil, nil, fmt.Errorf("RPMMD.Depsolve: %v", err)
@@ -122,10 +113,10 @@ func (api *API) submit(writer http.ResponseWriter, request *http.Request, _ http
 
 	// JSON structure expected from the client
 	var composeRequest struct {
-		Distribution  string                `json:"distribution"`
-		ImageTypes    []common.ImageType    `json:"image_types"`
-		Architectures []common.Architecture `json:"architectures"`
-		Repositories  []Repository          `json:"repositories"`
+		Distribution  string       `json:"distribution"`
+		ImageTypes    []string     `json:"image_types"`
+		Architectures []string     `json:"architectures"`
+		Repositories  []Repository `json:"repositories"`
 	}
 	// JSON structure with error message
 	var errorReason struct {
@@ -135,7 +126,7 @@ func (api *API) submit(writer http.ResponseWriter, request *http.Request, _ http
 	decoder := json.NewDecoder(request.Body)
 	decoder.DisallowUnknownFields()
 	err := decoder.Decode(&composeRequest)
-	if err != nil || len(composeRequest.Architectures) == 0 || len(composeRequest.Repositories) == 0 || len(composeRequest.ImageTypes) == 0 {
+	if err != nil || len(composeRequest.Architectures) != 1 || len(composeRequest.ImageTypes) != 1 || len(composeRequest.Repositories) == 0 {
 		writer.WriteHeader(http.StatusBadRequest)
 		errors := []string{}
 		if err != nil {
@@ -163,17 +154,6 @@ func (api *API) submit(writer http.ResponseWriter, request *http.Request, _ http
 		return
 	}
 
-	// Create repo configurations from the URLs in the request. Use made up repo id and name, because
-	// we don't want to bother clients of this API with details like this
-	repoConfigs := []rpmmd.RepoConfig{}
-	for n, repo := range composeRequest.Repositories {
-		repoConfigs = append(repoConfigs, rpmmd.RepoConfig{
-			Id:        fmt.Sprintf("repo-%d", n),
-			BaseURL:   repo.URL,
-			IgnoreSSL: false,
-		})
-	}
-
 	distro := api.distros.GetDistro(composeRequest.Distribution)
 	if distro == nil {
 		writer.WriteHeader(http.StatusBadRequest)
@@ -181,31 +161,54 @@ func (api *API) submit(writer http.ResponseWriter, request *http.Request, _ http
 		if err != nil {
 			panic("Failed to write response")
 		}
+		return
+	}
+
+	arch, err := distro.GetArch(composeRequest.Architectures[0])
+	if err != nil {
+		writer.WriteHeader(http.StatusBadRequest)
+		_, err := writer.Write([]byte("unknown architecture for distro"))
+		if err != nil {
+			panic("Failed to write response")
+		}
+		return
 	}
 
-	arch, ok := composeRequest.Architectures[0].ToString()
-	if !ok {
-		panic("architecture was validated before")
+	imageType, err := arch.GetImageType(composeRequest.ImageTypes[0])
+	if err != nil {
+		writer.WriteHeader(http.StatusBadRequest)
+		_, err := writer.Write([]byte("unknown image type for distro and architecture"))
+		if err != nil {
+			panic("Failed to write response")
+		}
+		return
 	}
 
-	imageType, ok := composeRequest.ImageTypes[0].ToString()
-	if !ok {
-		panic("image type was validated before")
+	// Create repo configurations from the URLs in the request. Use made up repo id and name, because
+	// we don't want to bother clients of this API with details like this
+	repoConfigs := []rpmmd.RepoConfig{}
+	for n, repo := range composeRequest.Repositories {
+		repoConfigs = append(repoConfigs, rpmmd.RepoConfig{
+			Id:        fmt.Sprintf("repo-%d", n),
+			BaseURL:   repo.URL,
+			IgnoreSSL: false,
+		})
 	}
 
-	packages, buildPackages, err := depsolve(api.rpmMetadata, distro, repoConfigs, imageType, arch)
+	packages, buildPackages, err := depsolve(api.rpmMetadata, distro, imageType, repoConfigs)
 	if err != nil {
 		writer.WriteHeader(http.StatusBadRequest)
 		_, err := writer.Write([]byte(err.Error()))
 		if err != nil {
 			panic("Failed to write response")
 		}
+		return
 	}
 
 	// Push the requested compose to the store
 	composeUUID := uuid.New()
 	// nil is used as an upload target, because LocalTarget is already used in the PushCompose function
-	err = api.store.PushCompose(distro, composeUUID, &blueprint.Blueprint{}, repoConfigs, packages, buildPackages, arch, imageType, 0, nil)
+	err = api.store.PushCompose(distro, arch, imageType, composeUUID, &blueprint.Blueprint{}, repoConfigs, packages, buildPackages, 0, nil)
 	if err != nil {
 		if api.logger != nil {
 			api.logger.Println("RCM API failed to push compose:", err)
diff --git a/internal/store/store.go b/internal/store/store.go
index 98539575d..22a713659 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -560,11 +560,11 @@ func (s *Store) getImageBuildDirectory(composeID uuid.UUID, imageBuildID int) st
 	return fmt.Sprintf("%s/%d", s.getComposeDirectory(composeID), imageBuildID)
 }
 
-func (s *Store) PushCompose(distro distro.Distro, composeID uuid.UUID, bp *blueprint.Blueprint, repos []rpmmd.RepoConfig, packages, buildPackages []rpmmd.PackageSpec, arch, composeType string, size uint64, uploadTarget *target.Target) error {
+func (s *Store) PushCompose(distro distro.Distro, arch distro.Arch, imageType distro.ImageType, composeID uuid.UUID, bp *blueprint.Blueprint, repos []rpmmd.RepoConfig, packages, buildPackages []rpmmd.PackageSpec, size uint64, uploadTarget *target.Target) error {
 	targets := []*target.Target{}
 
 	// Compatibility layer for image types in Weldr API v0
-	imageType, exists := common.ImageTypeFromCompatString(composeType)
+	imageTypeCommon, exists := common.ImageTypeFromCompatString(imageType.Name())
 	if !exists {
 		panic("fatal error, compose type does not exist")
 	}
@@ -577,20 +577,13 @@ func (s *Store) PushCompose(distro distro.Distro, composeID uuid.UUID, bp *bluep
 			return fmt.Errorf("cannot create output directory for job %v: %#v", composeID, err)
 		}
 
-		filename, _, err := distro.FilenameFromType(composeType)
-		if err != nil {
-			return fmt.Errorf("cannot query filename from image type %s: %#v", composeType, err)
-		}
-
 		targets = append(targets, target.NewLocalTarget(
 			&target.LocalTargetOptions{
-				Filename: filename,
+				Filename: imageType.Filename(),
 			},
 		))
 	}
 
-	size = distro.GetSizeForOutputType(composeType, size)
-
 	if uploadTarget != nil {
 		targets = append(targets, uploadTarget)
 	}
@@ -600,7 +593,7 @@ func (s *Store) PushCompose(distro distro.Distro, composeID uuid.UUID, bp *bluep
 		allRepos = append(allRepos, source.RepoConfig())
 	}
 
-	manifestStruct, err := distro.Manifest(bp.Customizations, allRepos, packages, buildPackages, arch, composeType, size)
+	manifestStruct, err := imageType.Manifest(bp.Customizations, allRepos, packages, buildPackages, imageType.Size(size))
 	if err != nil {
 		return err
 	}
@@ -612,7 +605,7 @@ func (s *Store) PushCompose(distro distro.Distro, composeID uuid.UUID, bp *bluep
 				{
 					QueueStatus: common.IBWaiting,
 					Manifest:    manifestStruct,
-					ImageType:   imageType,
+					ImageType:   imageTypeCommon,
 					Targets:     targets,
 					JobCreated:  time.Now(),
 					Size:        size,
@@ -626,7 +619,7 @@ func (s *Store) PushCompose(distro distro.Distro, composeID uuid.UUID, bp *bluep
 		ImageBuildID: 0,
 		Manifest:     manifestStruct,
 		Targets:      targets,
-		ImageType:    composeType,
+		ImageType:    imageType.Name(),
 	}
 
 	return nil
diff --git a/internal/weldr/api.go b/internal/weldr/api.go
index 9a317a03b..c37922a34 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -31,7 +31,7 @@ type API struct {
 	store *store.Store
 
 	rpmmd  rpmmd.RPMMD
-	arch   string
+	arch   distro.Arch
 	distro distro.Distro
 	repos  []rpmmd.RepoConfig
 
@@ -39,7 +39,7 @@ type API struct {
 	router *httprouter.Router
 }
 
-func New(rpmmd rpmmd.RPMMD, arch string, distro distro.Distro, repos []rpmmd.RepoConfig, logger *log.Logger, store *store.Store) *API {
+func New(rpmmd rpmmd.RPMMD, arch distro.Arch, distro distro.Distro, repos []rpmmd.RepoConfig, logger *log.Logger, store *store.Store) *API {
 	// This needs to be shared with the worker API so that they can communicate with each other
 	// builds := make(chan queue.Build, 200)
 	api := &API{
@@ -845,7 +845,7 @@ func (api *API) blueprintsDepsolveHandler(writer http.ResponseWriter, request *h
 			continue
 		}
 
-		dependencies, _, err := api.depsolveBlueprint(blueprint, "", api.arch)
+		dependencies, _, err := api.depsolveBlueprint(blueprint, nil)
 
 		if err != nil {
 			errors := responseError{
@@ -941,7 +941,7 @@ func (api *API) blueprintsFreezeHandler(writer http.ResponseWriter, request *htt
 			break
 		}
 
-		dependencies, _, err := api.depsolveBlueprint(&blueprint, "", api.arch)
+		dependencies, _, err := api.depsolveBlueprint(&blueprint, nil)
 		if err != nil {
 			rerr := responseError{
 				ID:  "BlueprintsError",
@@ -1389,14 +1389,19 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 		return
 	}
 
-	reply := ComposeReply{
-		BuildID: uuid.New(),
-		Status:  true,
+	imageType, err := api.arch.GetImageType(cr.ComposeType)
+	if err != nil {
+		errors := responseError{
+			ID:  "UnknownComposeType",
+			Msg: fmt.Sprintf("Unknown compose type for architecture: %s", cr.ComposeType),
+		}
+		statusResponseError(writer, http.StatusBadRequest, errors)
+		return
 	}
 
 	var uploadTarget *target.Target
 	if isRequestVersionAtLeast(params, 1) && cr.Upload != nil {
-		uploadTarget, err = uploadRequestToTarget(*cr.Upload, api.distro, cr.ComposeType)
+		uploadTarget, err = uploadRequestToTarget(*cr.Upload, imageType)
 		if err != nil {
 			errors := responseError{
 				ID:  "UploadError",
@@ -1409,31 +1414,7 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 	}
 
 	bp := api.store.GetBlueprintCommitted(cr.BlueprintName)
-	if bp != nil {
-		packages, buildPackages, err := api.depsolveBlueprint(bp, cr.ComposeType, api.arch)
-		if err != nil {
-			errors := responseError{
-				ID:  "DepsolveError",
-				Msg: err.Error(),
-			}
-			statusResponseError(writer, http.StatusInternalServerError, errors)
-			return
-		}
-
-		err = api.store.PushCompose(api.distro, reply.BuildID, bp, api.repos, packages, buildPackages, api.arch, cr.ComposeType, cr.Size, uploadTarget)
-
-		// TODO: we should probably do some kind of blueprint validation in future
-		// for now, let's just 500 and bail out
-		if err != nil {
-			log.Println("error when pushing new compose: ", err.Error())
-			errors := responseError{
-				ID:  "ComposePushErrored",
-				Msg: err.Error(),
-			}
-			statusResponseError(writer, http.StatusInternalServerError, errors)
-			return
-		}
-	} else {
+	if bp == nil {
 		errors := responseError{
 			ID:  "UnknownBlueprint",
 			Msg: fmt.Sprintf("Unknown blueprint name: %s", cr.BlueprintName),
@@ -1442,6 +1423,35 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 		return
 	}
 
+	packages, buildPackages, err := api.depsolveBlueprint(bp, imageType)
+	if err != nil {
+		errors := responseError{
+			ID:  "DepsolveError",
+			Msg: err.Error(),
+		}
+		statusResponseError(writer, http.StatusInternalServerError, errors)
+		return
+	}
+
+	reply := ComposeReply{
+		BuildID: uuid.New(),
+		Status:  true,
+	}
+
+	err = api.store.PushCompose(api.distro, api.arch, imageType, reply.BuildID, bp, api.repos, packages, buildPackages, cr.Size, uploadTarget)
+
+	// TODO: we should probably do some kind of blueprint validation in future
+	// for now, let's just 500 and bail out
+	if err != nil {
+		log.Println("error when pushing new compose: ", err.Error())
+		errors := responseError{
+			ID:  "ComposePushErrored",
+			Msg: err.Error(),
+		}
+		statusResponseError(writer, http.StatusInternalServerError, errors)
+		return
+	}
+
 	err = json.NewEncoder(writer).Encode(reply)
 	common.PanicOnError(err)
 }
@@ -1522,7 +1532,7 @@ func (api *API) composeTypesHandler(writer http.ResponseWriter, request *http.Re
 		Types []composeType `json:"types"`
 	}
 
-	for _, format := range api.distro.ListOutputFormats() {
+	for _, format := range api.arch.ListImageTypes() {
 		reply.Types = append(reply.Types, composeType{format, true})
 	}
 
@@ -1969,7 +1979,7 @@ func getPkgNameGlob(pkg blueprint.Package) string {
 	return pkg.Name
 }
 
-func (api *API) depsolveBlueprint(bp *blueprint.Blueprint, outputType, arch string) ([]rpmmd.PackageSpec, []rpmmd.PackageSpec, error) {
+func (api *API) depsolveBlueprint(bp *blueprint.Blueprint, imageType distro.ImageType) ([]rpmmd.PackageSpec, []rpmmd.PackageSpec, error) {
 	repos := append([]rpmmd.RepoConfig{}, api.repos...)
 	for _, source := range api.store.GetAllSources() {
 		repos = append(repos, source.RepoConfig())
@@ -1982,13 +1992,10 @@ func (api *API) depsolveBlueprint(bp *blueprint.Blueprint, outputType, arch stri
 		specs = append(specs, getPkgNameGlob(mod))
 	}
 	excludeSpecs := []string{}
-	if outputType != "" {
+	if imageType != nil {
 		// When the output type is known, include the base packages in the depsolve
 		// transaction.
-		packages, excludePackages, err := api.distro.BasePackages(outputType, arch)
-		if err != nil {
-			return nil, nil, err
-		}
+		packages, excludePackages := imageType.BasePackages()
 		specs = append(specs, packages...)
 		excludeSpecs = append(excludePackages, excludeSpecs...)
 	}
@@ -1999,11 +2006,8 @@ func (api *API) depsolveBlueprint(bp *blueprint.Blueprint, outputType, arch stri
 	}
 
 	buildPackages := []rpmmd.PackageSpec{}
-	if outputType != "" {
-		buildSpecs, err := api.distro.BuildPackages(arch)
-		if err != nil {
-			return nil, nil, err
-		}
+	if imageType != nil {
+		buildSpecs := imageType.BuildPackages()
 		buildPackages, _, err = api.rpmmd.Depsolve(buildSpecs, nil, repos, api.distro.ModulePlatformID())
 		if err != nil {
 			return nil, nil, err
diff --git a/internal/weldr/upload.go b/internal/weldr/upload.go
index 5e9029d6b..8e2d7356b 100644
--- a/internal/weldr/upload.go
+++ b/internal/weldr/upload.go
@@ -118,14 +118,9 @@ func targetsToUploadResponses(targets []*target.Target) []uploadResponse {
 	return uploads
 }
 
-func uploadRequestToTarget(u uploadRequest, d distro.Distro, imageType string) (*target.Target, error) {
+func uploadRequestToTarget(u uploadRequest, imageType distro.ImageType) (*target.Target, error) {
 	var t target.Target
 
-	filename, _, err := d.FilenameFromType(imageType)
-	if err != nil {
-		return nil, err
-	}
-
 	t.Uuid = uuid.New()
 	t.ImageName = u.ImageName
 	t.Status = common.IBWaiting
@@ -135,7 +130,7 @@ func uploadRequestToTarget(u uploadRequest, d distro.Distro, imageType string) (
 	case *awsUploadSettings:
 		t.Name = "org.osbuild.aws"
 		t.Options = &target.AWSTargetOptions{
-			Filename:        filename,
+			Filename:        imageType.Filename(),
 			Region:          options.Region,
 			AccessKeyID:     options.AccessKeyID,
 			SecretAccessKey: options.SecretAccessKey,
@@ -145,7 +140,7 @@ func uploadRequestToTarget(u uploadRequest, d distro.Distro, imageType string) (
 	case *azureUploadSettings:
 		t.Name = "org.osbuild.azure"
 		t.Options = &target.AzureTargetOptions{
-			Filename:  filename,
+			Filename:  imageType.Filename(),
 			Account:   options.Account,
 			AccessKey: options.AccessKey,
 			Container: options.Container,


Test patch:
diff --git a/internal/client/client_test.go b/internal/client/client_test.go
index 32fe5cab2..0358cb64d 100644
--- a/internal/client/client_test.go
+++ b/internal/client/client_test.go
@@ -13,7 +13,7 @@ import (
 	"testing"
 	"time"
 
-	test_distro "github.com/osbuild/osbuild-composer/internal/distro/fedoratest"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedoratest"
 	rpmmd_mock "github.com/osbuild/osbuild-composer/internal/mocks/rpmmd"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 	"github.com/osbuild/osbuild-composer/internal/weldr"
@@ -209,10 +209,14 @@ func TestMain(m *testing.M) {
 	// Create a mock API server listening on the temporary socket
 	fixture := rpmmd_mock.BaseFixture()
 	rpm := rpmmd_mock.NewRPMMDMock(fixture)
-	distro := test_distro.New()
+	distro := fedoratest.New()
+	arch, err := distro.GetArch("x86_64")
+	if err != nil {
+		panic(err)
+	}
 	repos := []rpmmd.RepoConfig{{Id: "test-id", BaseURL: "http://example.com/test/os/test_arch"}}
 	logger := log.New(os.Stdout, "", 0)
-	api := weldr.New(rpm, "test_arch", distro, repos, logger, fixture.Store)
+	api := weldr.New(rpm, arch, distro, repos, logger, fixture.Store)
 	server := http.Server{Handler: api}
 	defer server.Close()
 
diff --git a/internal/distro/distro_test.go b/internal/distro/distro_test.go
index 56c4f10b6..e664ec2c4 100644
--- a/internal/distro/distro_test.go
+++ b/internal/distro/distro_test.go
@@ -9,6 +9,11 @@ import (
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/distro"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora30"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora31"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora32"
+	"github.com/osbuild/osbuild-composer/internal/distro/rhel81"
+	"github.com/osbuild/osbuild-composer/internal/distro/rhel82"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
@@ -52,7 +57,7 @@ func TestDistro_Manifest(t *testing.T) {
 			t.Fatalf("rpmmd.LoadRepositories: %v", err)
 		}
 		t.Run(tt.ComposeRequest.OutputFormat, func(t *testing.T) {
-			distros, err := distro.NewDefaultRegistry()
+			distros, err := distro.NewRegistry(fedora30.New(), fedora31.New(), fedora32.New(), rhel81.New(), rhel82.New())
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -61,14 +66,21 @@ func TestDistro_Manifest(t *testing.T) {
 				t.Errorf("unknown distro: %v", tt.ComposeRequest.Distro)
 				return
 			}
-			size := d.GetSizeForOutputType(tt.ComposeRequest.OutputFormat, 0)
-			got, err := d.Manifest(tt.ComposeRequest.Blueprint.Customizations,
+			arch, err := d.GetArch(tt.ComposeRequest.Arch)
+			if err != nil {
+				t.Errorf("unknown arch: %v", tt.ComposeRequest.Arch)
+				return
+			}
+			imageType, err := arch.GetImageType(tt.ComposeRequest.OutputFormat)
+			if err != nil {
+				t.Errorf("unknown image type: %v", tt.ComposeRequest.OutputFormat)
+				return
+			}
+			got, err := imageType.Manifest(tt.ComposeRequest.Blueprint.Customizations,
 				repoMap[tt.ComposeRequest.Arch],
 				tt.RpmMD.Packages,
 				tt.RpmMD.BuildPackages,
-				tt.ComposeRequest.Arch,
-				tt.ComposeRequest.OutputFormat,
-				size)
+				imageType.Size(0))
 			if (err != nil) != (tt.Manifest == nil) {
 				t.Errorf("distro.Manifest() error = %v", err)
 				return
@@ -92,7 +104,7 @@ func TestDistro_RegistryList(t *testing.T) {
 		"rhel-8.2",
 	}
 
-	distros, err := distro.NewDefaultRegistry()
+	distros, err := distro.NewRegistry(fedora30.New(), fedora31.New(), fedora32.New(), rhel81.New(), rhel82.New())
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/internal/distro/fedora30/distro_test.go b/internal/distro/fedora30/distro_test.go
index fc5e258eb..ac8e89144 100644
--- a/internal/distro/fedora30/distro_test.go
+++ b/internal/distro/fedora30/distro_test.go
@@ -19,10 +19,7 @@ func TestListOutputFormats(t *testing.T) {
 		"vmdk",
 	}
 
-	f30, err := fedora30.New()
-	if err != nil {
-		t.Fatal(err)
-	}
+	f30 := fedora30.New()
 
 	if got := f30.ListOutputFormats(); !reflect.DeepEqual(got, want) {
 		t.Errorf("ListOutputFormats() = %v, want %v", got, want)
@@ -96,10 +93,7 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			f30, err := fedora30.New()
-			if err != nil {
-				t.Fatal(err)
-			}
+			f30 := fedora30.New()
 			got, got1, err := f30.FilenameFromType(tt.args.outputFormat)
 			if (err != nil) != tt.wantErr {
 				t.Errorf("FilenameFromType() error = %v, wantErr %v", err, tt.wantErr)
diff --git a/internal/distro/fedora31/distro_test.go b/internal/distro/fedora31/distro_test.go
index 104442bec..0bddb1020 100644
--- a/internal/distro/fedora31/distro_test.go
+++ b/internal/distro/fedora31/distro_test.go
@@ -19,10 +19,7 @@ func TestListOutputFormats(t *testing.T) {
 		"vmdk",
 	}
 
-	f31, err := fedora31.New()
-	if err != nil {
-		t.Fatal(err)
-	}
+	f31 := fedora31.New()
 
 	if got := f31.ListOutputFormats(); !reflect.DeepEqual(got, want) {
 		t.Errorf("ListOutputFormats() = %v, want %v", got, want)
@@ -96,10 +93,7 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			f31, err := fedora31.New()
-			if err != nil {
-				t.Fatal(err)
-			}
+			f31 := fedora31.New()
 			got, got1, err := f31.FilenameFromType(tt.args.outputFormat)
 			if (err != nil) != tt.wantErr {
 				t.Errorf("FilenameFromType() error = %v, wantErr %v", err, tt.wantErr)
diff --git a/internal/distro/fedora32/distro_test.go b/internal/distro/fedora32/distro_test.go
index 9d750a491..e01b049db 100644
--- a/internal/distro/fedora32/distro_test.go
+++ b/internal/distro/fedora32/distro_test.go
@@ -19,10 +19,7 @@ func TestListOutputFormats(t *testing.T) {
 		"vmdk",
 	}
 
-	f32, err := fedora32.New()
-	if err != nil {
-		t.Fatal(err)
-	}
+	f32 := fedora32.New()
 
 	if got := f32.ListOutputFormats(); !reflect.DeepEqual(got, want) {
 		t.Errorf("ListOutputFormats() = %v, want %v", got, want)
@@ -96,10 +93,8 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			f32, err := fedora32.New()
-			if err != nil {
-				t.Fatal(err)
-			}
+			f32 := fedora32.New()
+
 			got, got1, err := f32.FilenameFromType(tt.args.outputFormat)
 			if (err != nil) != tt.wantErr {
 				t.Errorf("FilenameFromType() error = %v, wantErr %v", err, tt.wantErr)
diff --git a/internal/distro/fedoratest/distro.go b/internal/distro/fedoratest/distro.go
index 5ac971036..158c33ae4 100644
--- a/internal/distro/fedoratest/distro.go
+++ b/internal/distro/fedoratest/distro.go
@@ -5,13 +5,89 @@ import (
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
 
+const ModulePlatformID = "platform:f30"
+
 type FedoraTestDistro struct{}
 
-const ModulePlatformID = "platform:f30"
+type FedoraTestDistroArch struct {
+	name   string
+	distro *FedoraTestDistro
+}
+
+type FedoraTestDistroImageType struct {
+	name string
+	arch *FedoraTestDistroArch
+}
+
+func (d *FedoraTestDistro) GetArch(arch string) (distro.Arch, error) {
+	if arch != "x86_64" {
+		return nil, errors.New("invalid architecture: " + arch)
+	}
+
+	return &FedoraTestDistroArch{
+		name:   arch,
+		distro: d,
+	}, nil
+}
+
+func (a *FedoraTestDistroArch) Name() string {
+	return a.name
+}
+
+func (a *FedoraTestDistroArch) ListImageTypes() []string {
+	return a.distro.ListOutputFormats()
+}
+
+func (a *FedoraTestDistroArch) GetImageType(imageType string) (distro.ImageType, error) {
+	if imageType != "qcow2" {
+		return nil, errors.New("invalid image type: " + imageType)
+	}
+
+	return &FedoraTestDistroImageType{
+		name: imageType,
+		arch: a,
+	}, nil
+}
+
+func (t *FedoraTestDistroImageType) Name() string {
+	return t.name
+}
+
+func (t *FedoraTestDistroImageType) Filename() string {
+	return "test.img"
+}
+
+func (t *FedoraTestDistroImageType) MIMEType() string {
+	return "application/x-test"
+}
+
+func (t *FedoraTestDistroImageType) Size(size uint64) uint64 {
+	return t.arch.distro.GetSizeForOutputType(t.name, size)
+}
+
+func (t *FedoraTestDistroImageType) BasePackages() ([]string, []string) {
+	return nil, nil
+}
+
+func (t *FedoraTestDistroImageType) BuildPackages() []string {
+	return nil
+}
+
+func (t *FedoraTestDistroImageType) Manifest(c *blueprint.Customizations,
+	repos []rpmmd.RepoConfig,
+	packageSpecs,
+	buildPackageSpecs []rpmmd.PackageSpec,
+	size uint64) (*osbuild.Manifest, error) {
+	return &osbuild.Manifest{
+		Pipeline: osbuild.Pipeline{},
+		Sources:  osbuild.Sources{},
+	}, nil
+}
 
 func New() *FedoraTestDistro {
 	return &FedoraTestDistro{}
diff --git a/internal/distro/rhel81/distro_test.go b/internal/distro/rhel81/distro_test.go
index 5cb02c4de..2cecf93f9 100644
--- a/internal/distro/rhel81/distro_test.go
+++ b/internal/distro/rhel81/distro_test.go
@@ -19,10 +19,7 @@ func TestListOutputFormats(t *testing.T) {
 		"vmdk",
 	}
 
-	el81, err := rhel81.New()
-	if err != nil {
-		t.Fatal(err)
-	}
+	el81 := rhel81.New()
 
 	if got := el81.ListOutputFormats(); !reflect.DeepEqual(got, want) {
 		t.Errorf("ListOutputFormats() = %v, want %v", got, want)
@@ -96,10 +93,7 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			el81, err := rhel81.New()
-			if err != nil {
-				t.Fatal(err)
-			}
+			el81 := rhel81.New()
 			got, got1, err := el81.FilenameFromType(tt.args.outputFormat)
 			if (err != nil) != tt.wantErr {
 				t.Errorf("FilenameFromType() error = %v, wantErr %v", err, tt.wantErr)
diff --git a/internal/distro/rhel82/distro_test.go b/internal/distro/rhel82/distro_test.go
index 444812fb3..6066eeac4 100644
--- a/internal/distro/rhel82/distro_test.go
+++ b/internal/distro/rhel82/distro_test.go
@@ -19,10 +19,7 @@ func TestListOutputFormats(t *testing.T) {
 		"vmdk",
 	}
 
-	el82, err := rhel82.New()
-	if err != nil {
-		t.Fatal(err)
-	}
+	el82 := rhel82.New()
 
 	if got := el82.ListOutputFormats(); !reflect.DeepEqual(got, want) {
 		t.Errorf("ListOutputFormats() = %v, want %v", got, want)
@@ -96,10 +93,7 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			el82, err := rhel82.New()
-			if err != nil {
-				t.Fatal(err)
-			}
+			el82 := rhel82.New()
 			got, got1, err := el82.FilenameFromType(tt.args.outputFormat)
 			if (err != nil) != tt.wantErr {
 				t.Errorf("FilenameFromType() error = %v, wantErr %v", err, tt.wantErr)
diff --git a/internal/distro/test/distro.go b/internal/distro/test/distro.go
index 5893570c0..6cf56a3a2 100644
--- a/internal/distro/test/distro.go
+++ b/internal/distro/test/distro.go
@@ -4,15 +4,68 @@ import (
 	"errors"
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
+	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
 
 type TestDistro struct{}
+type TestArch struct{}
+type TestImageType struct{}
 
 const Name = "test-distro"
 const ModulePlatformID = "platform:test"
 
+func (d *TestDistro) GetArch(arch string) (distro.Arch, error) {
+	if arch != "test_arch" {
+		return nil, errors.New("invalid arch: " + arch)
+	}
+	return &TestArch{}, nil
+}
+
+func (a *TestArch) Name() string {
+	return Name
+}
+
+func (a *TestArch) ListImageTypes() []string {
+	return []string{"test-format"}
+}
+
+func (a *TestArch) GetImageType(imageType string) (distro.ImageType, error) {
+	if imageType != "test_output" {
+		return nil, errors.New("invalid image type: " + imageType)
+	}
+	return &TestImageType{}, nil
+}
+
+func (t *TestImageType) Name() string {
+	return "test-format"
+}
+
+func (t *TestImageType) Filename() string {
+	return "test.img"
+}
+
+func (t *TestImageType) MIMEType() string {
+	return "application/x-test"
+}
+
+func (t *TestImageType) Size(size uint64) uint64 {
+	return 0
+}
+
+func (t *TestImageType) BasePackages() ([]string, []string) {
+	return nil, nil
+}
+
+func (t *TestImageType) BuildPackages() []string {
+	return nil
+}
+
+func (t *TestImageType) Manifest(b *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, size uint64) (*osbuild.Manifest, error) {
+	return &osbuild.Manifest{}, nil
+}
+
 func New() *TestDistro {
 	return &TestDistro{}
 }
diff --git a/internal/jobqueue/api_test.go b/internal/jobqueue/api_test.go
index be86055c5..ab88fc6c6 100644
--- a/internal/jobqueue/api_test.go
+++ b/internal/jobqueue/api_test.go
@@ -5,7 +5,7 @@ import (
 	"testing"
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
-	test_distro "github.com/osbuild/osbuild-composer/internal/distro/fedoratest"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedoratest"
 	"github.com/osbuild/osbuild-composer/internal/jobqueue"
 	"github.com/osbuild/osbuild-composer/internal/store"
 	"github.com/osbuild/osbuild-composer/internal/test"
@@ -40,11 +40,19 @@ func TestBasic(t *testing.T) {
 
 func TestCreate(t *testing.T) {
 	id, _ := uuid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff")
-	distroStruct := test_distro.New()
+	distroStruct := fedoratest.New()
+	arch, err := distroStruct.GetArch("x86_64")
+	if err != nil {
+		t.Fatalf("error getting arch from distro")
+	}
+	imageType, err := arch.GetImageType("qcow2")
+	if err != nil {
+		t.Fatalf("error getting image type from arch")
+	}
 	store := store.New(nil)
 	api := jobqueue.New(nil, store)
 
-	err := store.PushCompose(distroStruct, id, &blueprint.Blueprint{}, nil, nil, nil, "x86_64", "qcow2", 0, nil)
+	err = store.PushCompose(distroStruct, arch, imageType, id, &blueprint.Blueprint{}, nil, nil, nil, 0, nil)
 	if err != nil {
 		t.Fatalf("error pushing compose: %v", err)
 	}
@@ -55,12 +63,20 @@ func TestCreate(t *testing.T) {
 
 func testUpdateTransition(t *testing.T, from, to string, expectedStatus int, expectedResponse string) {
 	id, _ := uuid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff")
-	distroStruct := test_distro.New()
+	distroStruct := fedoratest.New()
+	arch, err := distroStruct.GetArch("x86_64")
+	if err != nil {
+		t.Fatalf("error getting arch from distro")
+	}
+	imageType, err := arch.GetImageType("qcow2")
+	if err != nil {
+		t.Fatalf("error getting image type from arch")
+	}
 	store := store.New(nil)
 	api := jobqueue.New(nil, store)
 
 	if from != "VOID" {
-		err := store.PushCompose(distroStruct, id, &blueprint.Blueprint{}, nil, nil, nil, "x86_64", "qcow2", 0, nil)
+		err := store.PushCompose(distroStruct, arch, imageType, id, &blueprint.Blueprint{}, nil, nil, nil, 0, nil)
 		if err != nil {
 			t.Fatalf("error pushing compose: %v", err)
 		}
diff --git a/internal/weldr/api_test.go b/internal/weldr/api_test.go
index d6b8ad7ca..38edf28b9 100644
--- a/internal/weldr/api_test.go
+++ b/internal/weldr/api_test.go
@@ -33,8 +33,12 @@ func createWeldrAPI(fixtureGenerator rpmmd_mock.FixtureGenerator) (*API, *store.
 	rpm := rpmmd_mock.NewRPMMDMock(fixture)
 	repos := []rpmmd.RepoConfig{{Id: "test-id", BaseURL: "http://example.com/test/os/x86_64"}}
 	d := test_distro.New()
+	arch, err := d.GetArch("x86_64")
+	if err != nil {
+		panic(err)
+	}
 
-	return New(rpm, "x86_64", d, repos, nil, fixture.Store), fixture.Store
+	return New(rpm, arch, d, repos, nil, fixture.Store), fixture.Store
 }
 
 func TestBasic(t *testing.T) {