Repository: osbuild/osbuild-composer
Pull request #221 (2020-02-03 13:42:14)

Rcm api continued
see individual commits for detailed description

msehnout: Nice, thanks for the review :)
msehnout: > The current blocker for me is that this PR breaks the weldr API (tested on RHEL 8.2). For some reason when job succeeds or fails, this doesn't change the job state, so in cockpit-composer it is still shown as running.

Good catch! I didn't notice, because the resulting image was there as usual. It should work now or at least it works for my on my hybrid Fedora 31-30 machine :D.
msehnout: All remaining errors in journal have been fixed! (except for modularity which is not part of this PR :) )

Patch:
diff --git a/cmd/osbuild-composer/main.go b/cmd/osbuild-composer/main.go
index 522e55aac..e8d7fb07e 100644
--- a/cmd/osbuild-composer/main.go
+++ b/cmd/osbuild-composer/main.go
@@ -62,7 +62,7 @@ func main() {
 		logger = log.New(os.Stdout, "", 0)
 	}
 
-	store := store.New(&stateDir, distribution)
+	store := store.New(&stateDir, distribution, *distros)
 
 	jobAPI := jobqueue.New(logger, store)
 	weldrAPI := weldr.New(rpm, currentArch(), distribution, logger, store)
diff --git a/cmd/osbuild-worker/main.go b/cmd/osbuild-worker/main.go
index 0a1efe98f..bfaf03c04 100644
--- a/cmd/osbuild-worker/main.go
+++ b/cmd/osbuild-worker/main.go
@@ -6,13 +6,13 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"github.com/osbuild/osbuild-composer/internal/compose"
 	"log"
 	"net"
 	"net/http"
 
 	"github.com/osbuild/osbuild-composer/internal/common"
 	"github.com/osbuild/osbuild-composer/internal/jobqueue"
-	"github.com/osbuild/osbuild-composer/internal/store"
 )
 
 type ComposerClient struct {
@@ -55,9 +55,9 @@ func (c *ComposerClient) AddJob() (*jobqueue.Job, error) {
 	return job, nil
 }
 
-func (c *ComposerClient) UpdateJob(job *jobqueue.Job, status string, image *store.Image, result *common.ComposeResult) error {
+func (c *ComposerClient) UpdateJob(job *jobqueue.Job, status common.ImageBuildState, image *compose.Image, result *common.ComposeResult) error {
 	var b bytes.Buffer
-	json.NewEncoder(&b).Encode(&jobqueue.JobStatus{status, image, result})
+	json.NewEncoder(&b).Encode(&jobqueue.JobStatus{status, job.ImageBuildID, image, result})
 	req, err := http.NewRequest("PATCH", "http://localhost/job-queue/v1/jobs/"+job.ID.String(), &b)
 	if err != nil {
 		return err
@@ -84,7 +84,7 @@ func handleJob(client *ComposerClient) error {
 		return err
 	}
 
-	err = client.UpdateJob(job, "RUNNING", nil, nil)
+	err = client.UpdateJob(job, common.IBRunning, nil, nil)
 	if err != nil {
 		return err
 	}
@@ -93,10 +93,10 @@ func handleJob(client *ComposerClient) error {
 	image, result, err := job.Run()
 	if err != nil {
 		log.Printf("  Job failed: %v", err)
-		return client.UpdateJob(job, "FAILED", nil, result)
+		return client.UpdateJob(job, common.IBFailed, nil, result)
 	}
 
-	return client.UpdateJob(job, "FINISHED", image, result)
+	return client.UpdateJob(job, common.IBFinished, image, result)
 }
 
 func main() {
diff --git a/internal/common/states.go b/internal/common/states.go
new file mode 100644
index 000000000..ddc5e4dd2
--- /dev/null
+++ b/internal/common/states.go
@@ -0,0 +1,79 @@
+package common
+
+import (
+	"encoding/json"
+)
+
+func getStateMapping() []string {
+	return []string{"WAITING", "RUNNING", "FINISHED", "FAILED"}
+}
+
+type ImageBuildState int
+
+const (
+	IBWaiting ImageBuildState = iota
+	IBRunning
+	IBFinished
+	IBFailed
+)
+
+// ToString converts ImageBuildState into a human readable string
+func (ibs ImageBuildState) ToString() string {
+	return getStateMapping()[int(ibs)]
+}
+
+func unmarshalStateHelper(data []byte, mapping []string) (int, error) {
+	var stringInput string
+	err := json.Unmarshal(data, &stringInput)
+	if err != nil {
+		return 0, err
+	}
+	for n, str := range getStateMapping() {
+		if str == stringInput {
+			return n, nil
+		}
+	}
+	return 0, &CustomJsonConversionError{"invalid image build status:" + stringInput}
+}
+
+// UnmarshalJSON converts a JSON string into an ImageBuildState
+func (ibs *ImageBuildState) UnmarshalJSON(data []byte) error {
+	val, err := unmarshalStateHelper(data, getStateMapping())
+	if err != nil {
+		return err
+	}
+	*ibs = ImageBuildState(val)
+	return nil
+}
+
+func (ibs ImageBuildState) MarshalJSON() ([]byte, error) {
+	return json.Marshal(getStateMapping()[ibs])
+}
+
+type ComposeState int
+
+const (
+	CWaiting ComposeState = iota
+	CRunning
+	CFinished
+	CFailed
+)
+
+// ToString converts ImageBuildState into a human readable string
+func (cs ComposeState) ToString() string {
+	return getStateMapping()[int(cs)]
+}
+
+// UnmarshalJSON converts a JSON string into an ImageBuildState
+func (ibs *ComposeState) UnmarshalJSON(data []byte) error {
+	val, err := unmarshalStateHelper(data, getStateMapping())
+	if err != nil {
+		return err
+	}
+	*ibs = ComposeState(val)
+	return nil
+}
+
+func (ibs ComposeState) MarshalJSON() ([]byte, error) {
+	return json.Marshal(getStateMapping()[ibs])
+}
diff --git a/internal/common/types.go b/internal/common/types.go
index 38c8846bc..292e9d35a 100644
--- a/internal/common/types.go
+++ b/internal/common/types.go
@@ -4,6 +4,8 @@ import (
 	"encoding/json"
 	"fmt"
 	"github.com/google/uuid"
+	"github.com/osbuild/osbuild-composer/internal/blueprint"
+	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
 
 // CustomJsonConversionError is thrown when parsing strings into enumerations
@@ -11,10 +13,14 @@ type CustomJsonConversionError struct {
 	reason string
 }
 
+// Error returns the error as a string
 func (err *CustomJsonConversionError) Error() string {
 	return err.reason
 }
 
+// Since Go has no generics, this is the only way to write common code for all the types present in this package.
+// It uses weakly typed maps to convert between strings and integers which are then wrapped into type aliases.
+// Specific implementations are bellow each type.
 func unmarshalHelper(data []byte, jsonError, typeConversionError string, mapping map[string]int) (int, error) {
 	var stringInput string
 	err := json.Unmarshal(data, &stringInput)
@@ -28,6 +34,7 @@ func unmarshalHelper(data []byte, jsonError, typeConversionError string, mapping
 	return value, nil
 }
 
+// See unmarshalHelper for explanation
 func marshalHelper(input int, mapping map[string]int, errorMessage string) ([]byte, error) {
 	for k, v := range mapping {
 		if v == input {
@@ -37,14 +44,16 @@ func marshalHelper(input int, mapping map[string]int, errorMessage string) ([]by
 	return nil, &CustomJsonConversionError{fmt.Sprintf("%d %s", input, errorMessage)}
 }
 
+// See unmarshalHelper for introduction. This helper can create a list of possible values of a single type.
 func listHelper(mapping map[string]int) []string {
 	ret := make([]string, 0)
-	for k,_ := range mapping {
+	for k, _ := range mapping {
 		ret = append(ret, k)
 	}
 	return ret
 }
 
+// See unmarshalHelper for introduction. With this helper one can make sure a value exists in a set of existing values.
 func existsHelper(mapping map[string]int, testedValue string) bool {
 	for k, _ := range mapping {
 		if k == testedValue {
@@ -54,6 +63,22 @@ func existsHelper(mapping map[string]int, testedValue string) bool {
 	return false
 }
 
+// See unmarshalHelper for introduction. Converts between TypeAlias(int) and string
+func toStringHelper(mapping map[string]int, tag int) (string, bool) {
+	for k, v := range mapping {
+		if v == tag {
+			return k, true
+		}
+	}
+	return "", false
+}
+
+// See unmarshalHelper for introduction. Converts between string and TypeAlias(int)
+func fromStringHelper(mapping map[string]int, stringInput string) (int, bool) {
+	value, exists := mapping[stringInput]
+	return value, exists
+}
+
 // Architecture represents one of the supported CPU architectures available for images
 // produced by osbuild-composer. It is represented as an integer because if it
 // was a string it would unmarshal from JSON just fine even in case that the architecture
@@ -113,6 +138,10 @@ func (arch Architecture) MarshalJSON() ([]byte, error) {
 	return marshalHelper(int(arch), getArchMapping(), "is not a valid architecture tag")
 }
 
+func (arch Architecture) ToString() (string, bool) {
+	return toStringHelper(getDistributionMapping(), int(arch))
+}
+
 type ImageType int
 
 // NOTE: If you want to add more constants here, don't forget to add a mapping below
@@ -126,21 +155,46 @@ const (
 	OpenStack
 	Qcow2Generic
 	Vmware
+	RawFilesystem
+	PartitionedDisk
+	TarArchive
 )
 
 // getArchMapping is a helper function that defines the conversion from JSON string value
 // to ImageType.
 func getImageTypeMapping() map[string]int {
 	mapping := map[string]int{
-		"Alibaba":      int(Alibaba),
-		"Azure":        int(Azure),
-		"AWS":          int(Aws),
-		"Google Cloud": int(GoogleCloud),
-		"Hyper-V":      int(HyperV),
-		"LiveISO":      int(LiveISO),
-		"OpenStack":    int(OpenStack),
-		"qcow2":        int(Qcow2Generic),
-		"VMWare":       int(Vmware),
+		"Alibaba":          int(Alibaba),
+		"Azure":            int(Azure),
+		"AWS":              int(Aws),
+		"Google-Cloud":     int(GoogleCloud),
+		"Hyper-V":          int(HyperV),
+		"LiveISO":          int(LiveISO),
+		"OpenStack":        int(OpenStack),
+		"qcow2":            int(Qcow2Generic),
+		"VMWare":           int(Vmware),
+		"Raw-filesystem":   int(RawFilesystem),
+		"Partitioned-disk": int(PartitionedDisk),
+		"Tar":              int(TarArchive),
+	}
+	return mapping
+}
+
+// TODO: check the mapping here:
+func getCompatImageTypeMapping() map[int]string {
+	mapping := map[int]string{
+		int(Alibaba):         "ami",
+		int(Azure):           "vhd",
+		int(Aws):             "ami",
+		int(GoogleCloud):     "ami",
+		int(HyperV):          "vhd",
+		int(LiveISO):         "liveiso",
+		int(OpenStack):       "openstack",
+		int(Qcow2Generic):    "qcow2",
+		int(Vmware):          "vmdk",
+		int(RawFilesystem):   "ext4-filesystem",
+		int(PartitionedDisk): "partitioned-disk",
+		int(TarArchive):      "tar",
 	}
 	return mapping
 }
@@ -158,6 +212,24 @@ func (imgType ImageType) MarshalJSON() ([]byte, error) {
 	return marshalHelper(int(imgType), getImageTypeMapping(), "is not a valid image type tag")
 }
 
+func (imgType ImageType) ToCompatString() (string, bool) {
+	str, exists := getCompatImageTypeMapping()[int(imgType)]
+	return str, exists
+}
+
+func ImageTypeFromCompatString(input string) (ImageType, bool) {
+	for k, v := range getCompatImageTypeMapping() {
+		if v == input {
+			return ImageType(k), true
+		}
+	}
+	return ImageType(999), false
+}
+
+func (imgType ImageType) ToString() (string, bool) {
+	return toStringHelper(getImageTypeMapping(), int(imgType))
+}
+
 type Distribution int
 
 // NOTE: If you want to add more constants here, don't forget to add a mapping below
@@ -178,7 +250,6 @@ func getDistributionMapping() map[string]int {
 	return mapping
 }
 
-
 func ListDistributions() []string {
 	return listHelper(getDistributionMapping())
 }
@@ -200,6 +271,15 @@ func (distro Distribution) MarshalJSON() ([]byte, error) {
 	return marshalHelper(int(distro), getDistributionMapping(), "is not a valid distribution tag")
 }
 
+func (distro Distribution) ToString() (string, bool) {
+	return toStringHelper(getDistributionMapping(), int(distro))
+}
+
+func DistributionFromString(distro string) (Distribution, bool) {
+	tag, exists := fromStringHelper(getDistributionMapping(), distro)
+	return Distribution(tag), exists
+}
+
 type UploadTarget int
 
 // NOTE: If you want to add more constants here, don't forget to add a mapping below
@@ -238,9 +318,10 @@ type ImageRequest struct {
 
 // ComposeRequest is used to submit a new compose to the store
 type ComposeRequest struct {
-	BlueprintName   string         `json:"blueprint_name"`
-	ComposeID       uuid.UUID      `json:"uuid"`
-	Distro          Distribution   `json:"distro"`
-	Arch            Architecture   `json:"arch"`
-	RequestedImages []ImageRequest `json:"requested_images"`
+	Blueprint       blueprint.Blueprint `json:"blueprint"`
+	ComposeID       uuid.UUID           `json:"uuid"`
+	Distro          Distribution        `json:"distro"`
+	Arch            Architecture        `json:"arch"`
+	Repositories    []rpmmd.RepoConfig  `json:"repositories"`
+	RequestedImages []ImageRequest      `json:"requested_images"`
 }
diff --git a/internal/compose/compose.go b/internal/compose/compose.go
new file mode 100644
index 000000000..232f7fa92
--- /dev/null
+++ b/internal/compose/compose.go
@@ -0,0 +1,172 @@
+// Package compose encapsulates the concept of a compose. It is a separate module from common, because it includes
+// target which in turn includes common and thus it would create a cyclic dependency, which is forbidden in golang.
+package compose
+
+import (
+	"github.com/osbuild/osbuild-composer/internal/blueprint"
+	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/pipeline"
+	"github.com/osbuild/osbuild-composer/internal/target"
+	"time"
+)
+
+type StateTransitionError struct {
+	message string
+}
+
+func (ste *StateTransitionError) Error() string {
+	return ste.message
+}
+
+// Image represents the image resulting from a compose.
+type Image struct {
+	Path string
+	Mime string
+	Size uint64
+}
+
+// ImageBuild represents a single image build inside a compose
+type ImageBuild struct {
+	Id          int                    `json:"id"`
+	Distro      common.Distribution    `json:"distro"`
+	QueueStatus common.ImageBuildState `json:"queue_status"`
+	ImageType   common.ImageType       `json:"image_type"`
+	Pipeline    *pipeline.Pipeline     `json:"pipeline"`
+	Targets     []*target.Target       `json:"targets"`
+	JobCreated  time.Time              `json:"job_created"`
+	JobStarted  time.Time              `json:"job_started"`
+	JobFinished time.Time              `json:"job_finished"`
+	Image       *Image                 `json:"image"`
+	Size        uint64                 `json:"size"`
+}
+
+// DeepCopy creates a copy of the ImageBuild structure
+func (ib *ImageBuild) DeepCopy() ImageBuild {
+	var newImagePtr *Image = nil
+	if ib.Image != nil {
+		imageCopy := *ib.Image
+		newImagePtr = &imageCopy
+	}
+	var newPipelinePtr *pipeline.Pipeline = nil
+	if ib.Pipeline != nil {
+		pipelineCopy := *ib.Pipeline
+		newPipelinePtr = &pipelineCopy
+	}
+	var newTargets []*target.Target
+	for _, t := range ib.Targets {
+		newTarget := *t
+		newTargets = append(newTargets, &newTarget)
+	}
+	// Create new image build struct
+	return ImageBuild{
+		Distro:      ib.Distro,
+		QueueStatus: ib.QueueStatus,
+		ImageType:   ib.ImageType,
+		Pipeline:    newPipelinePtr,
+		Targets:     newTargets,
+		JobCreated:  ib.JobCreated,
+		JobStarted:  ib.JobStarted,
+		JobFinished: ib.JobFinished,
+		Image:       newImagePtr,
+	}
+}
+
+// A Compose represent the task of building a set of images from a single blueprint.
+// It contains all the information necessary to generate the inputs for the job, as
+// well as the job's state.
+type Compose struct {
+	Blueprint   *blueprint.Blueprint `json:"blueprint"`
+	ImageBuilds []ImageBuild         `json:"image_builds"`
+}
+
+// DeepCopy creates a copy of the Compose structure
+func (c *Compose) DeepCopy() Compose {
+	var newBpPtr *blueprint.Blueprint = nil
+	if c.Blueprint != nil {
+		bpCopy := *c.Blueprint
+		newBpPtr = &bpCopy
+	}
+	newImageBuilds := []ImageBuild{}
+	for _, ib := range c.ImageBuilds {
+		newImageBuilds = append(newImageBuilds, ib.DeepCopy())
+	}
+	return Compose{
+		Blueprint:   newBpPtr,
+		ImageBuilds: newImageBuilds,
+	}
+}
+
+func anyImageBuild(fn func(common.ImageBuildState) bool, list []common.ImageBuildState) bool {
+	acc := false
+	for _, i := range list {
+		if fn(i) {
+			acc = true
+		}
+	}
+	return acc
+}
+
+func allImageBuilds(fn func(common.ImageBuildState) bool, list []common.ImageBuildState) bool {
+	acc := true
+	for _, i := range list {
+		if !fn(i) {
+			acc = false
+		}
+	}
+	return acc
+}
+
+// GetState returns a state of the whole compose which is derived from the states of
+// individual image builds inside the compose
+func (c *Compose) GetState() common.ComposeState {
+	var imageBuildsStates []common.ImageBuildState
+	for _, ib := range c.ImageBuilds {
+		imageBuildsStates = append(imageBuildsStates, ib.QueueStatus)
+	}
+	// In case all states are the same
+	if allImageBuilds(func(ib common.ImageBuildState) bool { return ib == common.IBWaiting }, imageBuildsStates) {
+		return common.CWaiting
+	}
+	if allImageBuilds(func(ib common.ImageBuildState) bool { return ib == common.IBFinished }, imageBuildsStates) {
+		return common.CFinished
+	}
+	if allImageBuilds(func(ib common.ImageBuildState) bool { return ib == common.IBFailed }, imageBuildsStates) {
+		return common.CFailed
+	}
+	// In case the states are mixed
+	// TODO: can this condition be removed because it is already covered by the default?
+	if anyImageBuild(func(ib common.ImageBuildState) bool { return ib == common.IBRunning }, imageBuildsStates) {
+		return common.CRunning
+	}
+	if allImageBuilds(func(ib common.ImageBuildState) bool { return ib == common.IBFailed || ib == common.IBFinished }, imageBuildsStates) {
+		return common.CFailed
+	}
+	// Default value
+	return common.CRunning
+}
+
+// UpdateState changes a state of a single image build inside the Compose
+func (c *Compose) UpdateState(imageBuildId int, newState common.ImageBuildState) error {
+	switch newState {
+	case common.IBWaiting:
+		return &StateTransitionError{"image build cannot be moved into waiting state"}
+	case common.IBRunning:
+		if c.ImageBuilds[imageBuildId].QueueStatus == common.IBWaiting || c.ImageBuilds[imageBuildId].QueueStatus == common.IBRunning {
+			c.ImageBuilds[imageBuildId].QueueStatus = newState
+		} else {
+			return &StateTransitionError{"only waiting image build can be transitioned into running state"}
+		}
+	case common.IBFinished, common.IBFailed:
+		if c.ImageBuilds[imageBuildId].QueueStatus == common.IBRunning {
+			c.ImageBuilds[imageBuildId].QueueStatus = newState
+			for _, t := range c.ImageBuilds[imageBuildId].Targets {
+				t.Status = newState
+			}
+		} else {
+			return &StateTransitionError{"only running image build can be transitioned into finished or failed state"}
+		}
+	default:
+		return &StateTransitionError{"invalid state"}
+	}
+	return nil
+}
diff --git a/internal/distro/distro.go b/internal/distro/distro.go
index 40ee85a20..4b25cdd32 100644
--- a/internal/distro/distro.go
+++ b/internal/distro/distro.go
@@ -3,6 +3,7 @@ package distro
 import (
 	"bufio"
 	"errors"
+	"github.com/osbuild/osbuild-composer/internal/common"
 	"io"
 	"os"
 	"strings"
@@ -20,6 +21,9 @@ type Distro interface {
 	// passed to New().
 	Name() string
 
+	// Return strong-typed distribution
+	Distribution() common.Distribution
+
 	// Returns a list of repositories from which this distribution gets its
 	// content.
 	Repositories(arch string) []rpmmd.RepoConfig
@@ -45,12 +49,12 @@ type Distro interface {
 }
 
 type Registry struct {
-	distros map[string]Distro
+	distros map[common.Distribution]Distro
 }
 
 func NewRegistry(confPaths []string) *Registry {
 	distros := &Registry{
-		distros: make(map[string]Distro),
+		distros: make(map[common.Distribution]Distro),
 	}
 	distros.register(fedora30.New(confPaths))
 	distros.register(rhel82.New(confPaths))
@@ -58,15 +62,19 @@ func NewRegistry(confPaths []string) *Registry {
 }
 
 func (r *Registry) register(distro Distro) {
-	name := distro.Name()
-	if _, exists := r.distros[name]; exists {
-		panic("a distro with this name already exists: " + name)
+	distroTag := distro.Distribution()
+	if _, exists := r.distros[distroTag]; exists {
+		panic("a distro with this name already exists: " + distro.Name())
 	}
-	r.distros[name] = distro
+	r.distros[distroTag] = distro
 }
 
 func (r *Registry) GetDistro(name string) Distro {
-	distro, ok := r.distros[name]
+	distroTag, exists := common.DistributionFromString(name)
+	if !exists {
+		return nil
+	}
+	distro, ok := r.distros[distroTag]
 	if !ok {
 		return nil
 	}
diff --git a/internal/distro/fedora30/distro.go b/internal/distro/fedora30/distro.go
index 16522b91f..d72e26b61 100644
--- a/internal/distro/fedora30/distro.go
+++ b/internal/distro/fedora30/distro.go
@@ -2,6 +2,7 @@ package fedora30
 
 import (
 	"errors"
+	"github.com/osbuild/osbuild-composer/internal/common"
 	"log"
 	"sort"
 	"strconv"
@@ -40,7 +41,7 @@ type output struct {
 	Assembler        func(uefi bool, size uint64) *pipeline.Assembler
 }
 
-const Name = "fedora-30"
+const Distro = common.Fedora30
 
 func New(confPaths []string) *Fedora30 {
 	const GigaByte = 1024 * 1024 * 1024
@@ -50,15 +51,15 @@ func New(confPaths []string) *Fedora30 {
 		outputs: map[string]output{},
 	}
 
-	repoMap, err := rpmmd.LoadRepositories(confPaths, Name)
+	repoMap, err := rpmmd.LoadRepositories(confPaths, r.Name())
 	if err != nil {
-		log.Printf("Could not load repository data for %s: %s", Name, err.Error())
+		log.Printf("Could not load repository data for %s: %s", r.Name(), err.Error())
 		return nil
 	}
 
 	repos, exists := repoMap["x86_64"]
 	if !exists {
-		log.Printf("Could not load architecture-specific repository data for x86_64 (%s): %s", Name, err.Error())
+		log.Printf("Could not load architecture-specific repository data for x86_64 (%s): %s", r.Name(), err.Error())
 	} else {
 		r.arches["x86_64"] = arch{
 			Name: "x86_64",
@@ -74,7 +75,7 @@ func New(confPaths []string) *Fedora30 {
 
 	repos, exists = repoMap["aarch64"]
 	if !exists {
-		log.Printf("Could not load architecture-specific repository data for x86_64 (%s): %s", Name, err.Error())
+		log.Printf("Could not load architecture-specific repository data for x86_64 (%s): %s", r.Name(), err.Error())
 	} else {
 		r.arches["aarch64"] = arch{
 			Name: "aarch64",
@@ -298,7 +299,15 @@ func New(confPaths []string) *Fedora30 {
 }
 
 func (r *Fedora30) Name() string {
-	return Name
+	name, exists := Distro.ToString()
+	if !exists {
+		panic("Fatal error, hardcoded distro value in fedora30 package is not valid!")
+	}
+	return name
+}
+
+func (r *Fedora30) Distribution() common.Distribution {
+	return Distro
 }
 
 func (r *Fedora30) Repositories(arch string) []rpmmd.RepoConfig {
diff --git a/internal/distro/rhel82/distro.go b/internal/distro/rhel82/distro.go
index f2ed9f499..031b05551 100644
--- a/internal/distro/rhel82/distro.go
+++ b/internal/distro/rhel82/distro.go
@@ -2,6 +2,7 @@ package rhel82
 
 import (
 	"errors"
+	"github.com/osbuild/osbuild-composer/internal/common"
 	"log"
 	"sort"
 	"strconv"
@@ -41,7 +42,7 @@ type output struct {
 	Assembler        func(uefi bool, size uint64) *pipeline.Assembler
 }
 
-const Name = "rhel-8.2"
+const Distro = common.RHEL82
 
 func New(confPaths []string) *RHEL82 {
 	const GigaByte = 1024 * 1024 * 1024
@@ -51,15 +52,15 @@ func New(confPaths []string) *RHEL82 {
 		outputs: map[string]output{},
 	}
 
-	repoMap, err := rpmmd.LoadRepositories(confPaths, Name)
+	repoMap, err := rpmmd.LoadRepositories(confPaths, r.Name())
 	if err != nil {
-		log.Printf("Could not load repository data for %s: %s", Name, err.Error())
+		log.Printf("Could not load repository data for %s: %s", r.Name(), err.Error())
 		return nil
 	}
 
 	repos, exists := repoMap["x86_64"]
 	if !exists {
-		log.Printf("Could not load architecture-specific repository data for x86_64 (%s): %s", Name, err.Error())
+		log.Printf("Could not load architecture-specific repository data for x86_64 (%s): %s", r.Name(), err.Error())
 	} else {
 		r.arches["x86_64"] = arch{
 			Name: "x86_64",
@@ -75,7 +76,7 @@ func New(confPaths []string) *RHEL82 {
 
 	repos, exists = repoMap["aarch64"]
 	if !exists {
-		log.Printf("Could not load architecture-specific repository data for aarch64 (%s): %s", Name, err.Error())
+		log.Printf("Could not load architecture-specific repository data for aarch64 (%s): %s", r.Name(), err.Error())
 	} else {
 		r.arches["aarch64"] = arch{
 			Name: "aarch64",
@@ -433,7 +434,16 @@ func New(confPaths []string) *RHEL82 {
 }
 
 func (r *RHEL82) Name() string {
-	return Name
+	name, exists := Distro.ToString()
+	if !exists {
+		panic("Fatal error, hardcoded distro value in rhel82 package is not valid!")
+	}
+	return name
+}
+
+
+func (r *RHEL82) Distribution() common.Distribution {
+	return Distro
 }
 
 func (r *RHEL82) Repositories(arch string) []rpmmd.RepoConfig {
diff --git a/internal/jobqueue/api.go b/internal/jobqueue/api.go
index 0bffa1681..2b4c72fe2 100644
--- a/internal/jobqueue/api.go
+++ b/internal/jobqueue/api.go
@@ -89,15 +89,17 @@ func (api *API) addJobHandler(writer http.ResponseWriter, request *http.Request,
 		return
 	}
 
-	nextJob := api.store.PopCompose()
+	nextJob := api.store.PopJob()
 
 	writer.WriteHeader(http.StatusCreated)
-	json.NewEncoder(writer).Encode(Job{
-		ID:         nextJob.ComposeID,
-		Distro:     nextJob.Distro,
-		Pipeline:   nextJob.Pipeline,
-		Targets:    nextJob.Targets,
-		OutputType: nextJob.OutputType,
+	// FIXME: handle or comment this possible error
+	_ = json.NewEncoder(writer).Encode(Job{
+		ID: nextJob.ComposeID,
+		ImageBuildID: nextJob.ImageBuildID,
+		Distro: nextJob.Distro,
+		Pipeline: nextJob.Pipeline,
+		Targets: nextJob.Targets,
+		OutputType: nextJob.ImageType,
 	})
 }
 
@@ -121,7 +123,7 @@ func (api *API) updateJobHandler(writer http.ResponseWriter, request *http.Reque
 		return
 	}
 
-	err = api.store.UpdateCompose(id, body.Status, body.Image, body.Result)
+	err = api.store.UpdateImageBuildInCompose(id, body.ImageBuildID, body.Status, body.Image, body.Result)
 	if err != nil {
 		switch err.(type) {
 		case *store.NotFoundError:
diff --git a/internal/jobqueue/job.go b/internal/jobqueue/job.go
index 92d103f29..5d6911887 100644
--- a/internal/jobqueue/job.go
+++ b/internal/jobqueue/job.go
@@ -3,6 +3,7 @@ package jobqueue
 import (
 	"encoding/json"
 	"fmt"
+	"github.com/osbuild/osbuild-composer/internal/compose"
 	"io/ioutil"
 	"os"
 	"os/exec"
@@ -12,23 +13,24 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/common"
 	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/pipeline"
-	"github.com/osbuild/osbuild-composer/internal/store"
 	"github.com/osbuild/osbuild-composer/internal/target"
 	"github.com/osbuild/osbuild-composer/internal/upload/awsupload"
 )
 
 type Job struct {
-	ID         uuid.UUID          `json:"id"`
-	Distro     string             `json:"distro"`
-	Pipeline   *pipeline.Pipeline `json:"pipeline"`
-	Targets    []*target.Target   `json:"targets"`
-	OutputType string             `json:"output_type"`
+	ID           uuid.UUID          `json:"id"`
+	ImageBuildID int                `json:"image_build_id"`
+	Distro       string             `json:"distro"`
+	Pipeline     *pipeline.Pipeline `json:"pipeline"`
+	Targets      []*target.Target   `json:"targets"`
+	OutputType   string             `json:"output_type"`
 }
 
 type JobStatus struct {
-	Status string                `json:"status"`
-	Image  *store.Image          `json:"image"`
-	Result *common.ComposeResult `json:"result"`
+	Status       common.ImageBuildState `json:"status"`
+	ImageBuildID int                    `json:"image_build_id"`
+	Image        *compose.Image         `json:"image"`
+	Result       *common.ComposeResult  `json:"result"`
 }
 
 type TargetsError struct {
@@ -45,7 +47,7 @@ func (e *TargetsError) Error() string {
 	return errString
 }
 
-func (job *Job) Run() (*store.Image, *common.ComposeResult, error) {
+func (job *Job) Run() (*compose.Image, *common.ComposeResult, error) {
 	distros := distro.NewRegistry([]string{"/etc/osbuild-composer", "/usr/share/osbuild-composer"})
 	d := distros.GetDistro(job.Distro)
 	if d == nil {
@@ -60,6 +62,7 @@ func (job *Job) Run() (*store.Image, *common.ComposeResult, error) {
 	if err != nil {
 		return nil, nil, err
 	}
+	// FIXME: how to handle errors in defer?
 	defer os.Remove(buildFile.Name())
 
 	err = json.NewEncoder(buildFile).Encode(build)
@@ -71,6 +74,7 @@ func (job *Job) Run() (*store.Image, *common.ComposeResult, error) {
 	if err != nil {
 		return nil, nil, fmt.Errorf("error setting up osbuild store: %v", err)
 	}
+	// FIXME: how to handle errors in defer?
 	defer os.RemoveAll(tmpStore)
 
 	cmd := exec.Command(
@@ -100,7 +104,8 @@ func (job *Job) Run() (*store.Image, *common.ComposeResult, error) {
 	if err != nil {
 		return nil, nil, fmt.Errorf("error encoding osbuild pipeline: %v", err)
 	}
-	stdin.Close()
+	// FIXME: handle or comment this possible error
+	_ = stdin.Close()
 
 	var result common.ComposeResult
 	err = json.NewDecoder(stdout).Decode(&result)
@@ -118,7 +123,7 @@ func (job *Job) Run() (*store.Image, *common.ComposeResult, error) {
 		return nil, &result, fmt.Errorf("cannot fetch information about output type %s: %v", job.OutputType, err)
 	}
 
-	var image store.Image
+	var image compose.Image
 
 	var r []error
 
@@ -150,10 +155,10 @@ func (job *Job) Run() (*store.Image, *common.ComposeResult, error) {
 				return nil, &result, err
 			}
 
-			image = store.Image{
+			image = compose.Image{
 				Path: imagePath,
 				Mime: mimeType,
-				Size: fileStat.Size(),
+				Size: uint64(fileStat.Size()),
 			}
 
 		case *target.AWSTargetOptions:
diff --git a/internal/mocks/rpmmd/fixtures.go b/internal/mocks/rpmmd/fixtures.go
index 229743fc1..11976cccd 100644
--- a/internal/mocks/rpmmd/fixtures.go
+++ b/internal/mocks/rpmmd/fixtures.go
@@ -2,12 +2,15 @@ package rpmmd_mock
 
 import (
 	"fmt"
+	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/compose"
+	"github.com/osbuild/osbuild-composer/internal/distro"
 	"sort"
 	"time"
 
 	"github.com/google/uuid"
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
-	test_distro "github.com/osbuild/osbuild-composer/internal/distro/test"
+	test_distro "github.com/osbuild/osbuild-composer/internal/distro/fedoratest"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 	"github.com/osbuild/osbuild-composer/internal/store"
 	"github.com/osbuild/osbuild-composer/internal/target"
@@ -71,7 +74,7 @@ func createBaseStoreFixture() *store.Store {
 		Name:      "org.osbuild.local",
 		ImageName: "localimage",
 		Created:   date,
-		Status:    "WAITING",
+		Status:    common.IBWaiting,
 		Options: &target.LocalTargetOptions{
 			Location: "/tmp/localimage",
 		},
@@ -82,7 +85,7 @@ func createBaseStoreFixture() *store.Store {
 		Name:      "org.osbuild.aws",
 		ImageName: "awsimage",
 		Created:   date,
-		Status:    "WAITING",
+		Status:    common.IBWaiting,
 		Options: &target.AWSTargetOptions{
 			Region:          "frankfurt",
 			AccessKeyID:     "accesskey",
@@ -93,42 +96,59 @@ func createBaseStoreFixture() *store.Store {
 	}
 
 	d := test_distro.New()
-	s := store.New(nil, d)
+	r := distro.NewRegistry([]string{"."})
+	s := store.New(nil, d, *r)
 
 	s.Blueprints[bName] = b
-	s.Composes = map[uuid.UUID]store.Compose{
-		uuid.MustParse("30000000-0000-0000-0000-000000000000"): store.Compose{
-			QueueStatus: "WAITING",
-			Blueprint:   &b,
-			OutputType:  "test_output",
-			Targets:     []*target.Target{localTarget, awsTarget},
-			JobCreated:  date,
+	s.Composes = map[uuid.UUID]compose.Compose{
+		uuid.MustParse("30000000-0000-0000-0000-000000000000"): compose.Compose{
+			Blueprint: &b,
+			ImageBuilds: []compose.ImageBuild{
+				{
+					QueueStatus: common.IBWaiting,
+					ImageType:   common.Qcow2Generic,
+					Targets:     []*target.Target{localTarget, awsTarget},
+					JobCreated:  date,
+				},
+			},
 		},
-		uuid.MustParse("30000000-0000-0000-0000-000000000001"): store.Compose{
-			QueueStatus: "RUNNING",
-			Blueprint:   &b,
-			OutputType:  "test_output",
-			Targets:     []*target.Target{localTarget},
-			JobCreated:  date,
-			JobStarted:  date,
+		uuid.MustParse("30000000-0000-0000-0000-000000000001"): compose.Compose{
+			Blueprint: &b,
+			ImageBuilds: []compose.ImageBuild{
+				{
+					QueueStatus: common.IBRunning,
+					ImageType:   common.Qcow2Generic,
+					Targets:     []*target.Target{localTarget},
+					JobCreated:  date,
+					JobStarted:  date,
+				},
+			},
 		},
-		uuid.MustParse("30000000-0000-0000-0000-000000000002"): store.Compose{
-			QueueStatus: "FINISHED",
-			Blueprint:   &b,
-			OutputType:  "test_output",
-			Targets:     []*target.Target{localTarget, awsTarget},
-			JobCreated:  date,
-			JobStarted:  date,
-			JobFinished: date,
+		uuid.MustParse("30000000-0000-0000-0000-000000000002"): compose.Compose{
+			Blueprint: &b,
+			ImageBuilds: []compose.ImageBuild{
+				{
+					QueueStatus: common.IBFinished,
+					ImageType:   common.Qcow2Generic,
+					Targets:     []*target.Target{localTarget, awsTarget},
+					JobCreated:  date,
+					JobStarted:  date,
+					JobFinished: date,
+				},
+			},
 		},
-		uuid.MustParse("30000000-0000-0000-0000-000000000003"): store.Compose{
-			QueueStatus: "FAILED",
-			Blueprint:   &b,
-			OutputType:  "test_output",
-			Targets:     []*target.Target{localTarget, awsTarget},
-			JobCreated:  date,
-			JobStarted:  date,
-			JobFinished: date,
+		uuid.MustParse("30000000-0000-0000-0000-000000000003"): compose.Compose{
+			Blueprint: &b,
+			ImageBuilds: []compose.ImageBuild{
+				{
+					QueueStatus: common.IBFailed,
+					ImageType:   common.Qcow2Generic,
+					Targets:     []*target.Target{localTarget, awsTarget},
+					JobCreated:  date,
+					JobStarted:  date,
+					JobFinished: date,
+				},
+			},
 		},
 	}
 
@@ -166,7 +186,8 @@ func createStoreWithoutComposesFixture() *store.Store {
 	}
 
 	d := test_distro.New()
-	s := store.New(nil, d)
+	r := distro.NewRegistry([]string{"."})
+	s := store.New(nil, d, *r)
 
 	s.Blueprints[bName] = b
 
diff --git a/internal/rcm/api.go b/internal/rcm/api.go
index 2cf4d11a1..da77035aa 100644
--- a/internal/rcm/api.go
+++ b/internal/rcm/api.go
@@ -81,7 +81,6 @@ func (api *API) submit(writer http.ResponseWriter, request *http.Request, _ http
 
 	type Repository struct {
 		URL      string `json:"url"`
-		Checksum string `json:"checksum"`
 	}
 
 	// JSON structure expected from the client
@@ -122,21 +121,24 @@ func (api *API) submit(writer http.ResponseWriter, request *http.Request, _ http
 			errors = append(errors, "input must specify repositories")
 		}
 		errorReason.Error = strings.Join(errors, ", ")
-		json.NewEncoder(writer).Encode(errorReason)
+		// TODO: handle error
+		_ = json.NewEncoder(writer).Encode(errorReason)
 		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
+	// TODO: replace this with generalized version of push compose
 	err = api.store.PushCompose(composeUUID, &blueprint.Blueprint{}, make(map[string]string), composeRequest.Architectures[0], composeRequest.ImageTypes[0], 0, nil)
 	if err != nil {
 		if api.logger != nil {
 			api.logger.Println("RCM API failed to push compose:", err)
 		}
-		writer.WriteHeader(http.StatusInternalServerError)
+		writer.WriteHeader(http.StatusBadRequest)
 		errorReason.Error = "failed to push compose: " + err.Error()
-		json.NewEncoder(writer).Encode(errorReason)
+		// TODO: handle error
+		_ = json.NewEncoder(writer).Encode(errorReason)
 		return
 	}
 
@@ -145,7 +147,8 @@ func (api *API) submit(writer http.ResponseWriter, request *http.Request, _ http
 		UUID uuid.UUID `json:"compose_id"`
 	}
 	reply.UUID = composeUUID
-	json.NewEncoder(writer).Encode(reply)
+	// TODO: handle error
+	_ = json.NewEncoder(writer).Encode(reply)
 }
 
 func (api *API) status(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -159,7 +162,8 @@ func (api *API) status(writer http.ResponseWriter, request *http.Request, params
 	if err != nil {
 		writer.WriteHeader(http.StatusBadRequest)
 		errorReason.Error = "Malformed UUID"
-		json.NewEncoder(writer).Encode(errorReason)
+		// TODO: handle error
+		_ = json.NewEncoder(writer).Encode(errorReason)
 		return
 	}
 
@@ -168,7 +172,8 @@ func (api *API) status(writer http.ResponseWriter, request *http.Request, params
 	if !exists {
 		writer.WriteHeader(http.StatusBadRequest)
 		errorReason.Error = "Compose UUID does not exist"
-		json.NewEncoder(writer).Encode(errorReason)
+		// TODO: handle error
+		_ = json.NewEncoder(writer).Encode(errorReason)
 		return
 	}
 
@@ -178,6 +183,7 @@ func (api *API) status(writer http.ResponseWriter, request *http.Request, params
 	}
 
 	// TODO: return per-job status like Koji does (requires changes in the store)
-	reply.Status = compose.QueueStatus
-	json.NewEncoder(writer).Encode(reply)
+	reply.Status = compose.GetState().ToString()
+	// TODO: handle error
+	_ = json.NewEncoder(writer).Encode(reply)
 }
diff --git a/internal/store/store.go b/internal/store/store.go
index 80e44f732..2408e22c5 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -10,6 +10,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"github.com/osbuild/osbuild-composer/internal/compose"
 	"io"
 	"io/ioutil"
 	"log"
@@ -36,46 +37,26 @@ import (
 type Store struct {
 	Blueprints        map[string]blueprint.Blueprint         `json:"blueprints"`
 	Workspace         map[string]blueprint.Blueprint         `json:"workspace"`
-	Composes          map[uuid.UUID]Compose                  `json:"composes"`
+	Composes          map[uuid.UUID]compose.Compose          `json:"composes"`
 	Sources           map[string]SourceConfig                `json:"sources"`
 	BlueprintsChanges map[string]map[string]blueprint.Change `json:"changes"`
 
-	mu           sync.RWMutex // protects all fields
-	pendingJobs  chan Job
-	stateChannel chan []byte
-	distro       distro.Distro
-	stateDir     *string
-}
-
-// A Compose represent the task of building one image. It contains all the information
-// necessary to generate the inputs for the job, as well as the job's state.
-type Compose struct {
-	QueueStatus string               `json:"queue_status"`
-	Blueprint   *blueprint.Blueprint `json:"blueprint"`
-	OutputType  string               `json:"output-type"`
-	Pipeline    *pipeline.Pipeline   `json:"pipeline"`
-	Targets     []*target.Target     `json:"targets"`
-	JobCreated  time.Time            `json:"job_created"`
-	JobStarted  time.Time            `json:"job_started"`
-	JobFinished time.Time            `json:"job_finished"`
-	Image       *Image               `json:"image"`
-	Size        uint64               `json:"size"`
+	mu             sync.RWMutex // protects all fields
+	pendingJobs    chan Job
+	stateChannel   chan []byte
+	distro         distro.Distro
+	distroRegistry distro.Registry
+	stateDir       *string
 }
 
 // A Job contains the information about a compose a worker needs to process it.
 type Job struct {
-	ComposeID  uuid.UUID
-	Distro     string
-	Pipeline   *pipeline.Pipeline
-	Targets    []*target.Target
-	OutputType string
-}
-
-// An Image represents the image resulting from a compose.
-type Image struct {
-	Path string
-	Mime string
-	Size int64
+	ComposeID    uuid.UUID
+	ImageBuildID int
+	Distro       string
+	Pipeline     *pipeline.Pipeline
+	Targets      []*target.Target
+	ImageType    string
 }
 
 type SourceConfig struct {
@@ -119,7 +100,7 @@ func (e *InvalidRequestError) Error() string {
 	return e.message
 }
 
-func New(stateDir *string, distro distro.Distro) *Store {
+func New(stateDir *string, distroArg distro.Distro, distroRegistryArg distro.Registry) *Store {
 	var s Store
 
 	if stateDir != nil {
@@ -153,7 +134,8 @@ func New(stateDir *string, distro distro.Distro) *Store {
 	}
 
 	s.pendingJobs = make(chan Job, 200)
-	s.distro = distro
+	s.distro = distroArg
+	s.distroRegistry = distroRegistryArg
 	s.stateDir = stateDir
 
 	if s.Blueprints == nil {
@@ -163,22 +145,36 @@ func New(stateDir *string, distro distro.Distro) *Store {
 		s.Workspace = make(map[string]blueprint.Blueprint)
 	}
 	if s.Composes == nil {
-		s.Composes = make(map[uuid.UUID]Compose)
+		s.Composes = make(map[uuid.UUID]compose.Compose)
 	} else {
 		for composeID, compose := range s.Composes {
-			switch compose.QueueStatus {
-			case "RUNNING":
-				// We do not support resuming an in-flight build
-				compose.QueueStatus = "FAILED"
-				s.Composes[composeID] = compose
-			case "WAITING":
-				// Push waiting composes back into the pending jobs queue
-				s.pendingJobs <- Job{
-					ComposeID:  composeID,
-					Distro:     s.distro.Name(),
-					Pipeline:   compose.Pipeline,
-					Targets:    compose.Targets,
-					OutputType: compose.OutputType,
+			if len(compose.ImageBuilds) == 0 {
+				panic("the was a compose with zero image builds, that is forbidden")
+			}
+			for _, imgBuild := range compose.ImageBuilds {
+				switch imgBuild.QueueStatus {
+				case common.IBRunning:
+					// We do not support resuming an in-flight build
+					imgBuild.QueueStatus = common.IBFailed
+					// s.Composes[composeID] = compose
+				case common.IBWaiting:
+					// Push waiting composes back into the pending jobs queue
+					distroStr, exists := imgBuild.Distro.ToString()
+					if !exists {
+						panic("fatal error, distro tag should exist but does not")
+					}
+					imageTypeCompat, exists := imgBuild.ImageType.ToCompatString()
+					if !exists {
+						panic("fatal error, image type tag should exist but does not")
+					}
+					s.pendingJobs <- Job{
+						ComposeID:    composeID,
+						ImageBuildID: imgBuild.Id,
+						Distro:       distroStr,
+						Pipeline:     imgBuild.Pipeline,
+						Targets:      imgBuild.Targets,
+						ImageType:    imageTypeCompat,
+					}
 				}
 			}
 		}
@@ -203,7 +199,8 @@ func writeFileAtomically(filename string, data []byte, mode os.FileMode) error {
 
 	_, err = tmpfile.Write(data)
 	if err != nil {
-		os.Remove(tmpfile.Name())
+		// FIXME: handle or comment this possible error
+		_ = os.Remove(tmpfile.Name())
 		return err
 	}
 
@@ -214,13 +211,15 @@ func writeFileAtomically(filename string, data []byte, mode os.FileMode) error {
 
 	err = tmpfile.Close()
 	if err != nil {
-		os.Remove(tmpfile.Name())
+		// FIXME: handle or comment this possible error
+		_ = os.Remove(tmpfile.Name())
 		return err
 	}
 
 	err = os.Rename(tmpfile.Name(), filename)
 	if err != nil {
-		os.Remove(tmpfile.Name())
+		// FIXME: handle or comment this possible error
+		_ = os.Remove(tmpfile.Name())
 		return err
 	}
 
@@ -367,7 +366,8 @@ func bumpVersion(str string) string {
 }
 
 func (s *Store) PushBlueprint(bp blueprint.Blueprint, commitMsg string) {
-	s.change(func() error {
+	// FIXME: handle or comment this possible error
+	_ = s.change(func() error {
 		commit, err := randomSHA1String()
 		if err != nil {
 			return err
@@ -397,14 +397,16 @@ func (s *Store) PushBlueprint(bp blueprint.Blueprint, commitMsg string) {
 }
 
 func (s *Store) PushBlueprintToWorkspace(bp blueprint.Blueprint) {
-	s.change(func() error {
+	// FIXME: handle or comment this possible error
+	_ = s.change(func() error {
 		s.Workspace[bp.Name] = bp
 		return nil
 	})
 }
 
 func (s *Store) DeleteBlueprint(name string) {
-	s.change(func() error {
+	// FIXME: handle or comment this possible error
+	_ = s.change(func() error {
 		delete(s.Workspace, name)
 		delete(s.Blueprints, name)
 		return nil
@@ -412,13 +414,14 @@ func (s *Store) DeleteBlueprint(name string) {
 }
 
 func (s *Store) DeleteBlueprintFromWorkspace(name string) {
-	s.change(func() error {
+	// FIXME: handle or comment this possible error
+	_ = s.change(func() error {
 		delete(s.Workspace, name)
 		return nil
 	})
 }
 
-func (s *Store) GetCompose(id uuid.UUID) (Compose, bool) {
+func (s *Store) GetCompose(id uuid.UUID) (compose.Compose, bool) {
 	s.mu.RLock()
 	defer s.mu.RUnlock()
 
@@ -426,24 +429,16 @@ func (s *Store) GetCompose(id uuid.UUID) (Compose, bool) {
 	return compose, exists
 }
 
-func (s *Store) GetAllComposes() map[uuid.UUID]Compose {
+// GetAllComposes creates a deep copy of all composes present in this store
+// and returns them as a dictionary with compose UUIDs as keys
+func (s *Store) GetAllComposes() map[uuid.UUID]compose.Compose {
 	s.mu.RLock()
 	defer s.mu.RUnlock()
 
-	composes := make(map[uuid.UUID]Compose)
-
-	for id, compose := range s.Composes {
-		newCompose := compose
-		newCompose.Targets = []*target.Target{}
-
-		for _, t := range compose.Targets {
-			newTarget := *t
-			newCompose.Targets = append(newCompose.Targets, &newTarget)
-		}
-
-		newBlueprint := *compose.Blueprint
-		newCompose.Blueprint = &newBlueprint
+	composes := make(map[uuid.UUID]compose.Compose)
 
+	for id, singleCompose := range s.Composes {
+		newCompose := singleCompose.DeepCopy()
 		composes[id] = newCompose
 	}
 
@@ -457,11 +452,24 @@ func (s *Store) GetComposeResult(id uuid.UUID) (io.ReadCloser, error) {
 	return os.Open(*s.stateDir + "/outputs/" + id.String() + "/result.json")
 }
 
+func (s *Store) getImageLocationForLocalTarget(composeID uuid.UUID) string {
+	// This might result in conflicts because one compose can have multiple images, but (!) the Weldr API
+	// does not support multiple images per build and the RCM API does not support Local target, so it does
+	// not really matter any more.
+	return *s.stateDir + "/outputs/" + composeID.String()
+}
+
 func (s *Store) PushCompose(composeID uuid.UUID, bp *blueprint.Blueprint, checksums map[string]string, arch, composeType string, size uint64, uploadTarget *target.Target) error {
 	targets := []*target.Target{}
 
+	// Compatibility layer for image types in Weldr API v0
+	imageType, exists := common.ImageTypeFromCompatString(composeType)
+	if !exists {
+		panic("fatal error, compose type does not exist")
+	}
+
 	if s.stateDir != nil {
-		outputDir := *s.stateDir + "/outputs/" + composeID.String()
+		outputDir := s.getImageLocationForLocalTarget(composeID)
 
 		err := os.MkdirAll(outputDir, 0755)
 		if err != nil {
@@ -475,6 +483,8 @@ func (s *Store) PushCompose(composeID uuid.UUID, bp *blueprint.Blueprint, checks
 		))
 	}
 
+	size = s.distro.GetSizeForOutputType(composeType, size)
+
 	if uploadTarget != nil {
 		targets = append(targets, uploadTarget)
 	}
@@ -484,35 +494,119 @@ func (s *Store) PushCompose(composeID uuid.UUID, bp *blueprint.Blueprint, checks
 		repos = append(repos, source.RepoConfig())
 	}
 
-	size = s.distro.GetSizeForOutputType(composeType, size)
-
-	pipeline, err := s.distro.Pipeline(bp, repos, checksums, arch, composeType, size)
+	pipelineStruct, err := s.distro.Pipeline(bp, repos, checksums, arch, composeType, size)
 	if err != nil {
 		return err
 	}
-	s.change(func() error {
-		s.Composes[composeID] = Compose{
-			QueueStatus: "WAITING",
-			Blueprint:   bp,
-			Pipeline:    pipeline,
-			OutputType:  composeType,
-			Targets:     targets,
-			JobCreated:  time.Now(),
-			Size:        size,
+	// FIXME: handle or comment this possible error
+	_ = s.change(func() error {
+		s.Composes[composeID] = compose.Compose{
+			Blueprint: bp,
+			ImageBuilds: []compose.ImageBuild{
+				{
+					QueueStatus: common.IBWaiting,
+					Pipeline:    pipelineStruct,
+					ImageType:   imageType,
+					Targets:     targets,
+					JobCreated:  time.Now(),
+					Size:        size,
+				},
+			},
 		}
 		return nil
 	})
 	s.pendingJobs <- Job{
-		ComposeID:  composeID,
-		Distro:     s.distro.Name(),
-		Pipeline:   pipeline,
-		Targets:    targets,
-		OutputType: composeType,
+		ComposeID:    composeID,
+		ImageBuildID: 0,
+		Distro:       s.distro.Name(),
+		Pipeline:     pipelineStruct,
+		Targets:      targets,
+		ImageType:    composeType,
 	}
 
 	return nil
 }
 
+// PushComposeRequest is an alternative to PushCompose which does not assume a pre-defined distro, as such it is better
+// suited for RCM API and possible future API that would respect the fact that we can build any distro and any arch
+func (s *Store) PushComposeRequest(request common.ComposeRequest) error {
+	// This should never happen and once distro.Pipeline is refactored this check will go away
+	arch, exists := request.Arch.ToString()
+	if !exists {
+		panic("fatal error, arch should exist but it does not")
+	}
+	distroString, exists := request.Distro.ToString()
+	if !exists {
+		panic("fatal error, distro should exist but it does not")
+	}
+
+	distroStruct := s.distroRegistry.GetDistro(distroString)
+	if distroStruct == nil {
+		panic("fatal error, distro should exist but it is not in the registry")
+	}
+
+	// This will be a list of imageBuilds that will be submitted to the state channel
+	imageBuilds := []compose.ImageBuild{}
+	newJobs := []Job{}
+
+	// TODO: remove this
+	if len(request.RequestedImages) > 1 {
+		panic("Multiple image requests are not yet properly implemented")
+	}
+
+	for n, imageRequest := range request.RequestedImages {
+		// TODO: handle custom upload targets
+		// TODO: this requires changes in the Compose Request struct
+		// TODO: implment support for no checksums
+		// This should never happen and once distro.Pipeline is refactored this check will go away
+		imgTypeCompatStr, exists := imageRequest.ImgType.ToCompatString()
+		if !exists {
+			panic("fatal error, image type should exist but it does not")
+		}
+		pipelineStruct, err := distroStruct.Pipeline(&request.Blueprint, request.Repositories, nil, arch, imgTypeCompatStr, 0)
+		if err != nil {
+			return err
+		}
+
+		// This will make the job submission atomic: either all of them or none of them
+		newJobs = append(newJobs, Job{
+			ComposeID:    request.ComposeID,
+			ImageBuildID: n,
+			Distro:       distroString,
+			Pipeline:     pipelineStruct,
+			Targets:      []*target.Target{},
+			ImageType:    imgTypeCompatStr,
+		})
+
+		imageBuilds = append(imageBuilds, compose.ImageBuild{
+			Distro:      request.Distro,
+			QueueStatus: common.IBWaiting,
+			ImageType:   imageRequest.ImgType,
+			Pipeline:    pipelineStruct,
+			Targets:     []*target.Target{},
+			JobCreated:  time.Now(),
+		})
+	}
+
+	// submit all the jobs now
+	for _, job := range newJobs {
+		s.pendingJobs <- job
+	}
+
+	// ignore error because the previous implementation does the same
+	_ = s.change(func() error {
+		s.Composes[request.ComposeID] = compose.Compose{
+			Blueprint:   &request.Blueprint,
+			ImageBuilds: imageBuilds,
+		}
+		return nil
+	})
+
+	return nil
+}
+
+// DeleteCompose deletes the compose from the state file and also removes all files on disk that are
+// associated with this compose
 func (s *Store) DeleteCompose(id uuid.UUID) error {
 	return s.change(func() error {
 		compose, exists := s.Composes[id]
@@ -521,7 +615,14 @@ func (s *Store) DeleteCompose(id uuid.UUID) error {
 			return &NotFoundError{}
 		}
 
-		if compose.QueueStatus != "FINISHED" && compose.QueueStatus != "FAILED" {
+		// If any of the image builds have build artifacts, remove them
+		invalidRequest := true
+		for _, imageBuild := range compose.ImageBuilds {
+			if imageBuild.QueueStatus == common.IBFinished || imageBuild.QueueStatus == common.IBFailed {
+				invalidRequest = false
+			}
+		}
+		if invalidRequest {
 			return &InvalidRequestError{fmt.Sprintf("Compose %s is not in FINISHED or FAILED.", id)}
 		}
 
@@ -529,92 +630,115 @@ func (s *Store) DeleteCompose(id uuid.UUID) error {
 
 		var err error
 		if s.stateDir != nil {
-			err = os.RemoveAll(*s.stateDir + "/outputs/" + id.String())
+			// TODO: we need to rename the files as the compose will have multiple images
+			for _, imageBuild := range compose.ImageBuilds {
+				if imageBuild.QueueStatus == common.IBFinished || imageBuild.QueueStatus == common.IBFailed {
+					err = os.RemoveAll(s.getImageLocationForLocalTarget(id))
+					if err != nil {
+						return err
+					}
+				}
+			}
 		}
 
 		return err
 	})
 }
 
-func (s *Store) PopCompose() Job {
+// PopJob returns a job from the job queue and changes the status of the corresponding image build to running
+func (s *Store) PopJob() Job {
 	job := <-s.pendingJobs
-	s.change(func() error {
+	// FIXME: handle or comment this possible error
+	_ = s.change(func() error {
+		// Get the compose from the map
 		compose, exists := s.Composes[job.ComposeID]
-		if !exists || compose.QueueStatus != "WAITING" {
+		// Check that it exists
+		if !exists {
 			panic("Invalid job in queue.")
 		}
-		compose.JobStarted = time.Now()
-		compose.QueueStatus = "RUNNING"
-		for _, t := range compose.Targets {
-			t.Status = "RUNNING"
+		// Loop over the image builds and find the right one according to the image type
+		// Change queue status to running for the image build as well as for the targets
+		for n, imageBuild := range compose.ImageBuilds {
+			imgTypeCompatStr, exists := imageBuild.ImageType.ToCompatString()
+			if !exists {
+				panic("fatal error, image type should exist but it does not")
+			}
+			if imgTypeCompatStr != job.ImageType {
+				continue
+			}
+			if imageBuild.QueueStatus != common.IBWaiting {
+				panic("Invalid job in queue.")
+			}
+			compose.ImageBuilds[n].QueueStatus = common.IBRunning
+			for m := range compose.ImageBuilds[n].Targets {
+				compose.ImageBuilds[n].Targets[m].Status = common.IBRunning
+			}
 		}
+		// Replace the compose struct with the new one
+		// TODO: I'm not sure this is needed, but I don't know what is the golang semantics in this case
 		s.Composes[job.ComposeID] = compose
 		return nil
 	})
 	return job
 }
 
-func (s *Store) UpdateCompose(composeID uuid.UUID, status string, image *Image, result *common.ComposeResult) error {
+// UpdateImageBuildInCompose sets the status and optionally also the final image.
+func (s *Store) UpdateImageBuildInCompose(composeID uuid.UUID, imageBuildID int, status common.ImageBuildState, image *compose.Image, result *common.ComposeResult) error {
 	return s.change(func() error {
-		compose, exists := s.Composes[composeID]
+		// Check that the compose exists
+		currentCompose, exists := s.Composes[composeID]
 		if !exists {
 			return &NotFoundError{"compose does not exist"}
 		}
-		if compose.QueueStatus == "WAITING" {
+		// Check that the image build was waiting
+		if currentCompose.ImageBuilds[imageBuildID].QueueStatus == common.IBWaiting {
 			return &NotPendingError{"compose has not been popped"}
 		}
 
 		// write result into file
 		if s.stateDir != nil && result != nil {
-			f, err := os.Create(*s.stateDir + "/outputs/" + composeID.String() + "/result.json")
+			f, err := os.Create(s.getImageLocationForLocalTarget(composeID) + "/result.json")
 
 			if err != nil {
 				return fmt.Errorf("cannot open result.json for job %v: %#v", composeID, err)
 			}
 
-			json.NewEncoder(f).Encode(result)
+			// FIXME: handle error
+			_ = json.NewEncoder(f).Encode(result)
 		}
 
-		switch status {
-		case "RUNNING":
-			switch compose.QueueStatus {
-			case "RUNNING":
-			default:
-				return &NotRunningError{"compose was not running"}
-			}
-		case "FINISHED", "FAILED":
-			switch compose.QueueStatus {
-			case "RUNNING":
-				compose.JobFinished = time.Now()
-			default:
-				return &NotRunningError{"compose was not running"}
-			}
-			compose.QueueStatus = status
-			for _, t := range compose.Targets {
-				t.Status = status
-			}
+		// Update the image build state including all target states
+		err := currentCompose.UpdateState(imageBuildID, status)
+		if err != nil {
+			// TODO: log error
+			return &InvalidRequestError{"invalid state transition: " + err.Error()}
+		}
 
-			if status == "FINISHED" {
-				compose.Image = image
+		// In case the image build is done, store the time and possibly also the image
+		if status == common.IBFinished || status == common.IBFailed {
+			currentCompose.ImageBuilds[imageBuildID].JobFinished = time.Now()
+			if status == common.IBFinished {
+				currentCompose.ImageBuilds[imageBuildID].Image = image
 			}
-
-			s.Composes[composeID] = compose
-		default:
-			return &InvalidRequestError{"invalid state transition"}
 		}
+
+		s.Composes[composeID] = currentCompose
+
 		return nil
 	})
 }
 
 func (s *Store) PushSource(source SourceConfig) {
-	s.change(func() error {
+	// FIXME: handle or comment this possible error
+	_ = s.change(func() error {
 		s.Sources[source.Name] = source
 		return nil
 	})
 }
 
 func (s *Store) DeleteSource(name string) {
-	s.change(func() error {
+	// FIXME: handle or comment this possible error
+	_ = s.change(func() error {
 		delete(s.Sources, name)
 		return nil
 	})
diff --git a/internal/target/target.go b/internal/target/target.go
index 19d8abd52..e078dfc2b 100644
--- a/internal/target/target.go
+++ b/internal/target/target.go
@@ -4,16 +4,17 @@ import (
 	"encoding/json"
 	"errors"
 	"github.com/google/uuid"
+	"github.com/osbuild/osbuild-composer/internal/common"
 	"time"
 )
 
 type Target struct {
-	Uuid      uuid.UUID     `json:"uuid"`
-	ImageName string        `json:"image_name"`
-	Name      string        `json:"name"`
-	Created   time.Time     `json:"created"`
-	Status    string        `json:"status"`
-	Options   TargetOptions `json:"options"`
+	Uuid      uuid.UUID              `json:"uuid"`
+	ImageName string                 `json:"image_name"`
+	Name      string                 `json:"name"`
+	Created   time.Time              `json:"created"`
+	Status    common.ImageBuildState `json:"status"`
+	Options   TargetOptions          `json:"options"`
 }
 
 func newTarget(name string, options TargetOptions) *Target {
@@ -21,7 +22,7 @@ func newTarget(name string, options TargetOptions) *Target {
 		Uuid:    uuid.New(),
 		Name:    name,
 		Created: time.Now(),
-		Status:  "WAITING",
+		Status:  common.IBWaiting,
 		Options: options,
 	}
 }
@@ -31,12 +32,12 @@ type TargetOptions interface {
 }
 
 type rawTarget struct {
-	Uuid      uuid.UUID       `json:"uuid"`
-	ImageName string          `json:"image_name"`
-	Name      string          `json:"name"`
-	Created   time.Time       `json:"created"`
-	Status    string          `json:"status"`
-	Options   json.RawMessage `json:"options"`
+	Uuid      uuid.UUID              `json:"uuid"`
+	ImageName string                 `json:"image_name"`
+	Name      string                 `json:"name"`
+	Created   time.Time              `json:"created"`
+	Status    common.ImageBuildState `json:"status"`
+	Options   json.RawMessage        `json:"options"`
 }
 
 func (target *Target) UnmarshalJSON(data []byte) error {
diff --git a/internal/weldr/api.go b/internal/weldr/api.go
index 746ee45de..6fdbf02ef 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -4,7 +4,7 @@ import (
 	"archive/tar"
 	"bytes"
 	"encoding/json"
-	"errors"
+	errors_package "errors"
 	"fmt"
 	"io"
 	"log"
@@ -356,7 +356,7 @@ func (api *API) sourceNewHandler(writer http.ResponseWriter, request *http.Reque
 	} else if contentType[0] == "text/x-toml" {
 		_, err = toml.DecodeReader(request.Body, &source)
 	} else {
-		err = errors.New("blueprint must be in json or toml format")
+		err = errors_package.New("blueprint must be in json or toml format")
 	}
 
 	if err != nil {
@@ -1103,7 +1103,7 @@ func (api *API) blueprintsNewHandler(writer http.ResponseWriter, request *http.R
 	} else if contentType[0] == "text/x-toml" {
 		_, err = toml.DecodeReader(request.Body, &blueprint)
 	} else {
-		err = errors.New("blueprint must be in json or toml format")
+		err = errors_package.New("blueprint must be in json or toml format")
 	}
 
 	if err != nil {
@@ -1143,7 +1143,7 @@ func (api *API) blueprintsWorkspaceHandler(writer http.ResponseWriter, request *
 	} else if contentType[0] == "text/x-toml" {
 		_, err = toml.DecodeReader(request.Body, &blueprint)
 	} else {
-		err = errors.New("blueprint must be in json or toml format")
+		err = errors_package.New("blueprint must be in json or toml format")
 	}
 
 	if err != nil {
@@ -1211,6 +1211,7 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 		BuildID uuid.UUID `json:"build_id"`
 		Status  bool      `json:"status"`
 	}
+	const MegaByte = 1024 * 1024
 
 	contentType := request.Header["Content-Type"]
 	if len(contentType) != 1 || contentType[0] != "application/json" {
@@ -1256,6 +1257,13 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 
 	bp := api.store.GetBlueprintCommitted(cr.BlueprintName)
 
+	size := cr.Size
+
+	// Microsoft Azure requires vhd images to be rounded up to the nearest MB
+	if cr.ComposeType == "vhd" && size%MegaByte != 0 {
+		size = (size/MegaByte + 1) * MegaByte
+	}
+
 	if bp != nil {
 		_, checksums, err := api.depsolveBlueprint(bp, true)
 		if err != nil {
@@ -1267,7 +1275,7 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 			return
 		}
 
-		err = api.store.PushCompose(reply.BuildID, bp, checksums, api.arch, cr.ComposeType, cr.Size, uploadTarget)
+		err = api.store.PushCompose(reply.BuildID, bp, checksums, api.arch, cr.ComposeType, size, uploadTarget)
 
 		// TODO: we should probably do some kind of blueprint validation in future
 		// for now, let's just 500 and bail out
@@ -1386,10 +1394,10 @@ func (api *API) composeQueueHandler(writer http.ResponseWriter, request *http.Re
 
 	composes := api.store.GetAllComposes()
 	for id, compose := range composes {
-		switch compose.QueueStatus {
-		case "WAITING":
+		switch compose.GetState() {
+		case common.CWaiting:
 			reply.New = append(reply.New, composeToComposeEntry(id, compose, isRequestVersionAtLeast(params, 1)))
-		case "RUNNING":
+		case common.CRunning:
 			reply.Run = append(reply.Run, composeToComposeEntry(id, compose, isRequestVersionAtLeast(params, 1)))
 		}
 	}
@@ -1428,7 +1436,6 @@ func (api *API) composeStatusHandler(writer http.ResponseWriter, request *http.R
 		}
 	}
 	composes := api.store.GetAllComposes()
-
 	reply.UUIDs = composesToComposeEntries(composes, uuids, isRequestVersionAtLeast(params, 1))
 
 	json.NewEncoder(writer).Encode(reply)
@@ -1482,14 +1489,16 @@ func (api *API) composeInfoHandler(writer http.ResponseWriter, request *http.Req
 	reply.Deps = Dependencies{
 		Packages: make([]map[string]interface{}, 0),
 	}
-	reply.ComposeType = compose.OutputType
-	reply.QueueStatus = compose.QueueStatus
-	if compose.Image != nil {
-		reply.ImageSize = compose.Size
+	// Weldr API assumes only one image build per compose, that's why only the
+	// 1st build is considered
+	reply.ComposeType, _ = compose.ImageBuilds[0].ImageType.ToCompatString()
+	reply.QueueStatus = compose.GetState().ToString()
+	if compose.ImageBuilds[0].Image != nil {
+		reply.ImageSize = compose.ImageBuilds[0].Size
 	}
 
 	if isRequestVersionAtLeast(params, 1) {
-		reply.Uploads = TargetsToUploadResponses(compose.Targets)
+		reply.Uploads = TargetsToUploadResponses(compose.ImageBuilds[0].Targets)
 	}
 
 	json.NewEncoder(writer).Encode(reply)
@@ -1521,16 +1530,16 @@ func (api *API) composeImageHandler(writer http.ResponseWriter, request *http.Re
 		return
 	}
 
-	if compose.QueueStatus != "FINISHED" {
+	if compose.GetState() != common.CFinished {
 		errors := responseError{
 			ID:  "BuildInWrongState",
-			Msg: fmt.Sprintf("Build %s is in wrong state: %s", uuidString, compose.QueueStatus),
+			Msg: fmt.Sprintf("Build %s is in wrong state: %s", uuidString, compose.GetState().ToString()),
 		}
 		statusResponseError(writer, http.StatusBadRequest, errors)
 		return
 	}
 
-	if compose.Image == nil {
+	if compose.ImageBuilds[0].Image == nil {
 		errors := responseError{
 			ID:  "BuildMissingFile",
 			Msg: fmt.Sprintf("Compose %s doesn't have an image assigned", uuidString),
@@ -1539,9 +1548,9 @@ func (api *API) composeImageHandler(writer http.ResponseWriter, request *http.Re
 		return
 	}
 
-	imageName := filepath.Base(compose.Image.Path)
+	imageName := filepath.Base(compose.ImageBuilds[0].Image.Path)
 
-	file, err := os.Open(compose.Image.Path)
+	file, err := os.Open(compose.ImageBuilds[0].Image.Path)
 	if err != nil {
 		errors := responseError{
 			ID:  "BuildMissingFile",
@@ -1552,8 +1561,8 @@ func (api *API) composeImageHandler(writer http.ResponseWriter, request *http.Re
 	}
 
 	writer.Header().Set("Content-Disposition", "attachment; filename="+uuid.String()+"-"+imageName)
-	writer.Header().Set("Content-Type", compose.Image.Mime)
-	writer.Header().Set("Content-Length", fmt.Sprintf("%d", compose.Image.Size))
+	writer.Header().Set("Content-Type", compose.ImageBuilds[0].Image.Mime)
+	writer.Header().Set("Content-Length", fmt.Sprintf("%d", compose.ImageBuilds[0].Image.Size))
 
 	io.Copy(writer, file)
 }
@@ -1584,7 +1593,7 @@ func (api *API) composeLogsHandler(writer http.ResponseWriter, request *http.Req
 		return
 	}
 
-	if compose.QueueStatus != "FINISHED" && compose.QueueStatus != "FAILED" {
+	if compose.GetState() != common.CFinished && compose.GetState() != common.CFailed {
 		errors := responseError{
 			ID:  "BuildInWrongState",
 			Msg: fmt.Sprintf("Build %s not in FINISHED or FAILED state.", uuidString),
@@ -1664,7 +1673,7 @@ func (api *API) composeLogHandler(writer http.ResponseWriter, request *http.Requ
 		return
 	}
 
-	if compose.QueueStatus == "WAITING" {
+	if compose.GetState() == common.CWaiting {
 		errors := responseError{
 			ID:  "BuildInWrongState",
 			Msg: fmt.Sprintf("Build %s has not started yet. No logs to view.", uuidString),
@@ -1673,7 +1682,7 @@ func (api *API) composeLogHandler(writer http.ResponseWriter, request *http.Requ
 		return
 	}
 
-	if compose.QueueStatus == "RUNNING" {
+	if compose.GetState() == common.CRunning {
 		fmt.Fprintf(writer, "Running...\n")
 		return
 	}
@@ -1717,7 +1726,7 @@ func (api *API) composeFinishedHandler(writer http.ResponseWriter, request *http
 	composes := api.store.GetAllComposes()
 	for _, entry := range composesToComposeEntries(composes, nil, isRequestVersionAtLeast(params, 1)) {
 		switch entry.QueueStatus {
-		case "FINISHED":
+		case common.IBFinished:
 			reply.Finished = append(reply.Finished, entry)
 		}
 	}
@@ -1737,7 +1746,7 @@ func (api *API) composeFailedHandler(writer http.ResponseWriter, request *http.R
 	composes := api.store.GetAllComposes()
 	for _, entry := range composesToComposeEntries(composes, nil, isRequestVersionAtLeast(params, 1)) {
 		switch entry.QueueStatus {
-		case "FAILED":
+		case common.IBFailed:
 			reply.Failed = append(reply.Failed, entry)
 		}
 	}
diff --git a/internal/weldr/compose.go b/internal/weldr/compose.go
index a4355bab3..404b846d4 100644
--- a/internal/weldr/compose.go
+++ b/internal/weldr/compose.go
@@ -2,61 +2,62 @@ package weldr
 
 import (
 	"github.com/google/uuid"
-	"github.com/osbuild/osbuild-composer/internal/store"
+	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/compose"
 	"log"
 	"sort"
 )
 
 type ComposeEntry struct {
-	ID          uuid.UUID        `json:"id"`
-	Blueprint   string           `json:"blueprint"`
-	Version     string           `json:"version"`
-	ComposeType string           `json:"compose_type"`
-	ImageSize   uint64           `json:"image_size"`
-	QueueStatus string           `json:"queue_status"`
-	JobCreated  float64          `json:"job_created"`
-	JobStarted  float64          `json:"job_started,omitempty"`
-	JobFinished float64          `json:"job_finished,omitempty"`
-	Uploads     []UploadResponse `json:"uploads,omitempty"`
+	ID          uuid.UUID              `json:"id"`
+	Blueprint   string                 `json:"blueprint"`
+	Version     string                 `json:"version"`
+	ComposeType common.ImageType       `json:"compose_type"`
+	ImageSize   uint64                 `json:"image_size"`
+	QueueStatus common.ImageBuildState `json:"queue_status"`
+	JobCreated  float64                `json:"job_created"`
+	JobStarted  float64                `json:"job_started,omitempty"`
+	JobFinished float64                `json:"job_finished,omitempty"`
+	Uploads     []UploadResponse       `json:"uploads,omitempty"`
 }
 
-func composeToComposeEntry(id uuid.UUID, compose store.Compose, includeUploads bool) *ComposeEntry {
+func composeToComposeEntry(id uuid.UUID, compose compose.Compose, includeUploads bool) *ComposeEntry {
 	var composeEntry ComposeEntry
 
 	composeEntry.ID = id
 	composeEntry.Blueprint = compose.Blueprint.Name
 	composeEntry.Version = compose.Blueprint.Version
-	composeEntry.ComposeType = compose.OutputType
-	composeEntry.QueueStatus = compose.QueueStatus
+	composeEntry.ComposeType = compose.ImageBuilds[0].ImageType
+	composeEntry.QueueStatus = compose.ImageBuilds[0].QueueStatus
 
 	if includeUploads {
-		composeEntry.Uploads = TargetsToUploadResponses(compose.Targets)
+		composeEntry.Uploads = TargetsToUploadResponses(compose.ImageBuilds[0].Targets)
 	}
 
-	switch compose.QueueStatus {
-	case "WAITING":
-		composeEntry.JobCreated = float64(compose.JobCreated.UnixNano()) / 1000000000
+	switch compose.ImageBuilds[0].QueueStatus {
+	case common.IBWaiting:
+		composeEntry.JobCreated = float64(compose.ImageBuilds[0].JobCreated.UnixNano()) / 1000000000
 
-	case "RUNNING":
-		composeEntry.JobCreated = float64(compose.JobCreated.UnixNano()) / 1000000000
-		composeEntry.JobStarted = float64(compose.JobStarted.UnixNano()) / 1000000000
+	case common.IBRunning:
+		composeEntry.JobCreated = float64(compose.ImageBuilds[0].JobCreated.UnixNano()) / 1000000000
+		composeEntry.JobStarted = float64(compose.ImageBuilds[0].JobStarted.UnixNano()) / 1000000000
 
-	case "FINISHED":
-		if compose.Image != nil {
-			composeEntry.ImageSize = compose.Size
+	case common.IBFinished:
+		if compose.ImageBuilds[0].Image != nil {
+			composeEntry.ImageSize = compose.ImageBuilds[0].Size
 		} else {
 			log.Printf("finished compose with id %s has nil image\n", id.String())
 			composeEntry.ImageSize = 0
 		}
 
-		composeEntry.JobCreated = float64(compose.JobCreated.UnixNano()) / 1000000000
-		composeEntry.JobStarted = float64(compose.JobStarted.UnixNano()) / 1000000000
-		composeEntry.JobFinished = float64(compose.JobFinished.UnixNano()) / 1000000000
+		composeEntry.JobCreated = float64(compose.ImageBuilds[0].JobCreated.UnixNano()) / 1000000000
+		composeEntry.JobStarted = float64(compose.ImageBuilds[0].JobStarted.UnixNano()) / 1000000000
+		composeEntry.JobFinished = float64(compose.ImageBuilds[0].JobFinished.UnixNano()) / 1000000000
 
-	case "FAILED":
-		composeEntry.JobCreated = float64(compose.JobCreated.UnixNano()) / 1000000000
-		composeEntry.JobStarted = float64(compose.JobStarted.UnixNano()) / 1000000000
-		composeEntry.JobFinished = float64(compose.JobFinished.UnixNano()) / 1000000000
+	case common.IBFailed:
+		composeEntry.JobCreated = float64(compose.ImageBuilds[0].JobCreated.UnixNano()) / 1000000000
+		composeEntry.JobStarted = float64(compose.ImageBuilds[0].JobStarted.UnixNano()) / 1000000000
+		composeEntry.JobFinished = float64(compose.ImageBuilds[0].JobFinished.UnixNano()) / 1000000000
 	default:
 		panic("invalid compose state")
 	}
@@ -64,7 +65,7 @@ func composeToComposeEntry(id uuid.UUID, compose store.Compose, includeUploads b
 	return &composeEntry
 }
 
-func composesToComposeEntries(composes map[uuid.UUID]store.Compose, uuids []uuid.UUID, includeUploads bool) []*ComposeEntry {
+func composesToComposeEntries(composes map[uuid.UUID]compose.Compose, uuids []uuid.UUID, includeUploads bool) []*ComposeEntry {
 	var composeEntries []*ComposeEntry
 	if uuids == nil {
 		composeEntries = make([]*ComposeEntry, 0, len(composes))
diff --git a/internal/weldr/upload.go b/internal/weldr/upload.go
index 44ef52457..6c3c31b7b 100644
--- a/internal/weldr/upload.go
+++ b/internal/weldr/upload.go
@@ -3,6 +3,7 @@ package weldr
 import (
 	"encoding/json"
 	"errors"
+	"github.com/osbuild/osbuild-composer/internal/common"
 	"time"
 
 	"github.com/google/uuid"
@@ -10,12 +11,12 @@ import (
 )
 
 type UploadResponse struct {
-	Uuid         uuid.UUID            `json:"uuid"`
-	Status       string               `json:"status"`
-	ProviderName string               `json:"provider_name"`
-	ImageName    string               `json:"image_name"`
-	CreationTime float64              `json:"creation_time"`
-	Settings     target.TargetOptions `json:"settings"`
+	Uuid         uuid.UUID              `json:"uuid"`
+	Status       common.ImageBuildState `json:"status"`
+	ProviderName string                 `json:"provider_name"`
+	ImageName    string                 `json:"image_name"`
+	CreationTime float64                `json:"creation_time"`
+	Settings     target.TargetOptions   `json:"settings"`
 }
 
 type UploadRequest struct {
@@ -103,7 +104,7 @@ func UploadRequestToTarget(u UploadRequest) (*target.Target, error) {
 	t.ImageName = u.ImageName
 	t.Options = u.Settings
 	t.Name = targetName
-	t.Status = "WAITING"
+	t.Status = common.IBWaiting
 	t.Created = time.Now()
 
 	return &t, nil


Test patch:
diff --git a/internal/common/states_test.go b/internal/common/states_test.go
new file mode 100644
index 000000000..d5f7057eb
--- /dev/null
+++ b/internal/common/states_test.go
@@ -0,0 +1,59 @@
+package common
+
+import (
+	"encoding/json"
+	"reflect"
+	"testing"
+)
+
+func TestJSONConversions(t *testing.T) {
+	type TestJson struct {
+		Ibs ImageBuildState `json:"ibs"`
+		Cs ComposeState `json:"cs"`
+	}
+	typedCases := []TestJson{
+		{
+			Ibs: IBWaiting,
+			Cs: CWaiting,
+		},
+		{
+			Ibs: IBRunning,
+			Cs: CFailed,
+		},
+	}
+	strCases := []string{
+		`{"ibs": "WAITING", "cs": "WAITING"}`,
+		`{"ibs": "RUNNING", "cs": "FAILED"}`,
+	}
+
+	for n, c := range strCases {
+		var inputStringAsStruct *TestJson
+		err := json.Unmarshal([]byte(c), &inputStringAsStruct)
+		if err != nil {
+			t.Fatal("Failed to unmarshal:", err)
+		}
+		if reflect.DeepEqual(inputStringAsStruct, typedCases[n]) {
+			t.Error("Unmarshaled compose request is not the one expected")
+		}
+	}
+
+	var byteArrays [][]byte
+	for _, c := range typedCases {
+		data, err := json.Marshal(c)
+		if err != nil {
+			t.Fatal("Failed to marshal state:", err)
+		}
+		byteArrays = append(byteArrays, data)
+	}
+	for n, b := range byteArrays {
+		var inputStringAsStruct *TestJson
+		err := json.Unmarshal(b, &inputStringAsStruct)
+		if err != nil {
+			t.Fatal("Failed to unmarshal:", err)
+		}
+		if reflect.DeepEqual(inputStringAsStruct, typedCases[n]) {
+			t.Error("Unmarshaled compose request is not the one expected")
+		}
+	}
+	
+}
diff --git a/internal/common/types_test.go b/internal/common/types_test.go
index fb9da27ab..20a8b67e8 100644
--- a/internal/common/types_test.go
+++ b/internal/common/types_test.go
@@ -3,23 +3,32 @@ package common
 import (
 	"encoding/json"
 	"github.com/google/uuid"
+	"github.com/osbuild/osbuild-composer/internal/blueprint"
+	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 	"reflect"
 	"testing"
 )
 
 func TestJSONConversionsComposeRequest(t *testing.T) {
-	cases := []struct{
-		input string
+	cases := []struct {
+		input                    string
 		expectedConversionResult ComposeRequest
 	}{
 		// 1st case
 		{
 			`
 			{
-				"blueprint_name": "foo",
+				"blueprint": {
+					"name": "",
+					"description": "",
+					"packages": [],
+					"modules": [],
+					"groups": []
+				},
 				"uuid": "789b4d42-da1a-49c9-a20c-054da3bb6c82",
 				"distro": "fedora-31",
 				"arch": "x86_64",
+				"repositories": [],
 				"requested_images": [
 					{
 						"image_type": "AWS",
@@ -29,10 +38,19 @@ func TestJSONConversionsComposeRequest(t *testing.T) {
 			}
 			`,
 			ComposeRequest{
-				BlueprintName: "foo",
-				ComposeID:     uuid.UUID{},
-				Distro:        Fedora31,
-				Arch:          X86_64,
+				Blueprint: blueprint.Blueprint{
+					Name:           "",
+					Description:    "",
+					Version:        "",
+					Packages:       nil,
+					Modules:        nil,
+					Groups:         nil,
+					Customizations: nil,
+				},
+				ComposeID:    uuid.UUID{},
+				Distro:       Fedora31,
+				Arch:         X86_64,
+				Repositories: []rpmmd.RepoConfig{},
 				RequestedImages: []ImageRequest{{
 					ImgType:  Aws,
 					UpTarget: []UploadTarget{EC2},
@@ -43,10 +61,17 @@ func TestJSONConversionsComposeRequest(t *testing.T) {
 		{
 			`
 			{
-				"blueprint_name": "bar",
+				"blueprint": {
+					"name": "",
+					"description": "",
+					"packages": [],
+					"modules": [],
+					"groups": []
+				},
 				"uuid": "789b4d42-da1a-49c9-a20c-054da3bb6c82",
 				"distro": "rhel-8.2",
 				"arch": "aarch64",
+				"repositories": [],
 				"requested_images": [
 					{
 						"image_type": "Azure",
@@ -56,10 +81,19 @@ func TestJSONConversionsComposeRequest(t *testing.T) {
 			}
 			`,
 			ComposeRequest{
-				BlueprintName: "bar",
-				ComposeID:     uuid.UUID{},
-				Distro:        RHEL82,
-				Arch:          Aarch64,
+				Blueprint: blueprint.Blueprint{
+					Name:           "",
+					Description:    "",
+					Version:        "",
+					Packages:       nil,
+					Modules:        nil,
+					Groups:         nil,
+					Customizations: nil,
+				},
+				ComposeID:    uuid.UUID{},
+				Distro:       RHEL82,
+				Arch:         Aarch64,
+				Repositories: []rpmmd.RepoConfig{},
 				RequestedImages: []ImageRequest{{
 					ImgType:  Azure,
 					UpTarget: []UploadTarget{AzureStorage, EC2},
@@ -68,12 +102,12 @@ func TestJSONConversionsComposeRequest(t *testing.T) {
 		},
 	}
 
-	for _, c := range cases {
+	for n, c := range cases {
 		// Test unmashaling the JSON from the string above
 		var inputStringAsStruct *ComposeRequest
 		err := json.Unmarshal([]byte(c.input), &inputStringAsStruct)
 		if err != nil {
-			t.Fatal("Failed ot unmarshal ComposeRequest:", err)
+			t.Fatal("Failed to unmarshal ComposeRequest (", n, "):", err)
 		}
 		if reflect.DeepEqual(inputStringAsStruct, c.expectedConversionResult) {
 			t.Error("Unmarshaled compose request is not the one expected")
@@ -84,12 +118,12 @@ func TestJSONConversionsComposeRequest(t *testing.T) {
 		// compare the resulting structure with the input one
 		data, err := json.Marshal(c.expectedConversionResult)
 		if err != nil {
-			t.Fatal("Failed ot marshal ComposeRequest:", err)
+			t.Fatal("Failed to marshal ComposeRequest:", err)
 		}
 		var expectedResultAfterMarshaling *ComposeRequest
 		err = json.Unmarshal(data, &expectedResultAfterMarshaling)
 		if err != nil {
-			t.Fatal("Failed ot unmarshal ComposeRequest:", err, ", input:", string(data))
+			t.Fatal("Failed to unmarshal ComposeRequest:", err, ", input:", string(data))
 		}
 		if reflect.DeepEqual(expectedResultAfterMarshaling, c.expectedConversionResult) {
 			t.Error("Marshaled compose request is not the one expected")
@@ -97,5 +131,3 @@ func TestJSONConversionsComposeRequest(t *testing.T) {
 	}
 
 }
-
-
diff --git a/internal/compose/compose_test.go b/internal/compose/compose_test.go
new file mode 100644
index 000000000..2fcd5df42
--- /dev/null
+++ b/internal/compose/compose_test.go
@@ -0,0 +1,125 @@
+package compose
+
+import (
+	"github.com/osbuild/osbuild-composer/internal/common"
+	"testing"
+)
+
+func TestGetState(t *testing.T) {
+	cases := []struct{
+		compose Compose
+		expecedStatus common.ComposeState
+	}{
+		{
+			compose: Compose{
+				ImageBuilds: []ImageBuild{
+					{QueueStatus: common.IBWaiting},
+				},
+			},
+			expecedStatus: common.CWaiting,
+		},
+		{
+			compose: Compose{
+				ImageBuilds: []ImageBuild{
+					{QueueStatus: common.IBRunning},
+				},
+			},
+			expecedStatus: common.CRunning,
+		},
+		{
+			compose: Compose{
+				ImageBuilds: []ImageBuild{
+					{QueueStatus: common.IBFailed},
+				},
+			},
+			expecedStatus: common.CFailed,
+		},
+		{
+			compose: Compose{
+				ImageBuilds: []ImageBuild{
+					{QueueStatus: common.IBFinished},
+				},
+			},
+			expecedStatus: common.CFinished,
+		},
+		{
+			compose: Compose{
+				ImageBuilds: []ImageBuild{
+					{QueueStatus: common.IBWaiting},
+					{QueueStatus: common.IBWaiting},
+				},
+			},
+			expecedStatus: common.CWaiting,
+		},
+		{
+			compose: Compose{
+				ImageBuilds: []ImageBuild{
+					{QueueStatus: common.IBWaiting},
+					{QueueStatus: common.IBRunning},
+				},
+			},
+			expecedStatus: common.CRunning,
+		},
+		{
+			compose: Compose{
+				ImageBuilds: []ImageBuild{
+					{QueueStatus: common.IBRunning},
+					{QueueStatus: common.IBRunning},
+				},
+			},
+			expecedStatus: common.CRunning,
+		},
+		{
+			compose: Compose{
+				ImageBuilds: []ImageBuild{
+					{QueueStatus: common.IBRunning},
+					{QueueStatus: common.IBFailed},
+				},
+			},
+			expecedStatus: common.CRunning,
+		},
+		{
+			compose: Compose{
+				ImageBuilds: []ImageBuild{
+					{QueueStatus: common.IBWaiting},
+					{QueueStatus: common.IBFailed},
+				},
+			},
+			expecedStatus: common.CRunning,
+		},
+		{
+			compose: Compose{
+				ImageBuilds: []ImageBuild{
+					{QueueStatus: common.IBFailed},
+					{QueueStatus: common.IBFailed},
+				},
+			},
+			expecedStatus: common.CFailed,
+		},
+		{
+			compose: Compose{
+				ImageBuilds: []ImageBuild{
+					{QueueStatus: common.IBFinished},
+					{QueueStatus: common.IBFinished},
+				},
+			},
+			expecedStatus: common.CFinished,
+		},
+		{
+			compose: Compose{
+				ImageBuilds: []ImageBuild{
+					{QueueStatus: common.IBFinished},
+					{QueueStatus: common.IBFailed},
+				},
+			},
+			expecedStatus: common.CFailed,
+		},
+	}
+	for n, c := range cases {
+		got := c.compose.GetState()
+		wanted := c.expecedStatus
+		if got != wanted {
+			t.Error("Compose", n, "should be in", wanted.ToString(), "state, but it is:", got.ToString())
+		}
+	} 
+}
\ No newline at end of file
diff --git a/internal/distro/fedoratest/distro.go b/internal/distro/fedoratest/distro.go
new file mode 100644
index 000000000..43e3a01f9
--- /dev/null
+++ b/internal/distro/fedoratest/distro.go
@@ -0,0 +1,62 @@
+package fedoratest
+
+import (
+	"errors"
+	"github.com/osbuild/osbuild-composer/internal/blueprint"
+	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/pipeline"
+	"github.com/osbuild/osbuild-composer/internal/rpmmd"
+)
+
+type FedoraTestDistro struct{}
+
+func New() *FedoraTestDistro {
+	return &FedoraTestDistro{}
+}
+
+func (d *FedoraTestDistro) Name() string {
+	return "fedora-30"
+}
+
+func (d *FedoraTestDistro) Distribution() common.Distribution {
+	return common.Fedora30
+}
+
+func (d *FedoraTestDistro) Repositories(arch string) []rpmmd.RepoConfig {
+	return []rpmmd.RepoConfig{
+		{
+			Id:      "test-id",
+			Name:    "Test Name",
+			BaseURL: "http://example.com/test/os/" + arch,
+		},
+	}
+}
+
+func (d *FedoraTestDistro) ListOutputFormats() []string {
+	return []string{"qcow2"}
+}
+
+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)
+	}
+}
+
+func (r *FedoraTestDistro) GetSizeForOutputType(outputFormat string, size uint64) uint64 {
+	return 0
+}
+
+func (d *FedoraTestDistro) Pipeline(b *blueprint.Blueprint, additionalRepos []rpmmd.RepoConfig, checksums map[string]string, outputArch, outputFormat string, size uint64) (*pipeline.Pipeline, error) {
+	if outputFormat == "qcow2" && outputArch == "x86_64" {
+		return &pipeline.Pipeline{}, nil
+	} else {
+		return nil, errors.New("invalid output format or arch: " + outputFormat + " @ " + outputArch)
+	}
+}
+
+func (d *FedoraTestDistro) Runner() string {
+	return "org.osbuild.test"
+}
+
diff --git a/internal/distro/test/distro.go b/internal/distro/test/distro.go
deleted file mode 100644
index c062a8b9b..000000000
--- a/internal/distro/test/distro.go
+++ /dev/null
@@ -1,59 +0,0 @@
-package test
-
-import (
-	"errors"
-
-	"github.com/osbuild/osbuild-composer/internal/blueprint"
-	"github.com/osbuild/osbuild-composer/internal/pipeline"
-	"github.com/osbuild/osbuild-composer/internal/rpmmd"
-)
-
-type TestDistro struct{}
-
-const Name = "test-distro"
-
-func New() *TestDistro {
-	return &TestDistro{}
-}
-
-func (d *TestDistro) Name() string {
-	return Name
-}
-
-func (d *TestDistro) Repositories(arch string) []rpmmd.RepoConfig {
-	return []rpmmd.RepoConfig{
-		{
-			Id:      "test-id",
-			Name:    "Test Name",
-			BaseURL: "http://example.com/test/os/" + arch,
-		},
-	}
-}
-
-func (d *TestDistro) ListOutputFormats() []string {
-	return []string{"test_format"}
-}
-
-func (d *TestDistro) FilenameFromType(outputFormat string) (string, string, error) {
-	if outputFormat == "test_format" {
-		return "test.img", "application/x-test", nil
-	} else {
-		return "", "", errors.New("invalid output format: " + outputFormat)
-	}
-}
-
-func (r *TestDistro) GetSizeForOutputType(outputFormat string, size uint64) uint64 {
-	return 0
-}
-
-func (d *TestDistro) Pipeline(b *blueprint.Blueprint, additionalRepos []rpmmd.RepoConfig, checksums map[string]string, outputArch, outputFormat string, size uint64) (*pipeline.Pipeline, error) {
-	if outputFormat == "test_output" && outputArch == "test_arch" {
-		return &pipeline.Pipeline{}, nil
-	} else {
-		return nil, errors.New("invalid output format or arch: " + outputFormat + " @ " + outputArch)
-	}
-}
-
-func (d *TestDistro) Runner() string {
-	return "org.osbuild.test"
-}
diff --git a/internal/jobqueue/api_test.go b/internal/jobqueue/api_test.go
index 535369028..5e11fefe5 100644
--- a/internal/jobqueue/api_test.go
+++ b/internal/jobqueue/api_test.go
@@ -5,7 +5,8 @@ import (
 	"testing"
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
-	test_distro "github.com/osbuild/osbuild-composer/internal/distro/test"
+	"github.com/osbuild/osbuild-composer/internal/distro"
+	test_distro "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"
@@ -28,12 +29,13 @@ func TestBasic(t *testing.T) {
 		// Update job that does not exist, with invalid body
 		{"PATCH", "/job-queue/v1/jobs/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", ``, http.StatusBadRequest, ``},
 		// Update job that does not exist
-		{"PATCH", "/job-queue/v1/jobs/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", `{"status":"RUNNING"}`, http.StatusNotFound, ``},
+		{"PATCH", "/job-queue/v1/jobs/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", `{"image_build_id": 0, "status":"RUNNING"}`, http.StatusNotFound, ``},
 	}
 
 	for _, c := range cases {
-		distro := test_distro.New()
-		api := jobqueue.New(nil, store.New(nil, distro))
+		distroStruct := test_distro.New()
+		registry := distro.NewRegistry([]string{"."})
+		api := jobqueue.New(nil, store.New(nil, distroStruct, *registry))
 
 		test.TestRoute(t, api, false, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
 	}
@@ -41,27 +43,29 @@ func TestBasic(t *testing.T) {
 
 func TestCreate(t *testing.T) {
 	id, _ := uuid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff")
-	distro := test_distro.New()
-	store := store.New(nil, distro)
+	distroStruct := test_distro.New()
+	registry := distro.NewRegistry([]string{"."})
+	store := store.New(nil, distroStruct, *registry)
 	api := jobqueue.New(nil, store)
 
-	err := store.PushCompose(id, &blueprint.Blueprint{}, map[string]string{"test-repo": "test:foo"}, "test_arch", "test_output", 0, nil)
+	err := store.PushCompose(id, &blueprint.Blueprint{}, map[string]string{"test-repo": "test:foo"}, "x86_64", "qcow2", 0, nil)
 	if err != nil {
 		t.Fatalf("error pushing compose: %v", err)
 	}
 
 	test.TestRoute(t, api, false, "POST", "/job-queue/v1/jobs", `{}`, http.StatusCreated,
-		`{"distro":"test-distro","id":"ffffffff-ffff-ffff-ffff-ffffffffffff","output_type":"test_output","pipeline":{},"targets":[]}`, "created", "uuid")
+		`{"distro":"fedora-30","id":"ffffffff-ffff-ffff-ffff-ffffffffffff","image_build_id":0,"output_type":"qcow2","pipeline":{},"targets":[]}`, "created", "uuid")
 }
 
 func testUpdateTransition(t *testing.T, from, to string, expectedStatus int) {
 	id, _ := uuid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff")
-	distro := test_distro.New()
-	store := store.New(nil, distro)
+	distroStruct := test_distro.New()
+	registry := distro.NewRegistry([]string{"."})
+	store := store.New(nil, distroStruct, *registry)
 	api := jobqueue.New(nil, store)
 
 	if from != "VOID" {
-		err := store.PushCompose(id, &blueprint.Blueprint{}, map[string]string{"test": "test:foo"}, "test_arch", "test_output", 0, nil)
+		err := store.PushCompose(id, &blueprint.Blueprint{}, map[string]string{"test": "test:foo"}, "x86_64", "qcow2", 0, nil)
 		if err != nil {
 			t.Fatalf("error pushing compose: %v", err)
 		}
diff --git a/internal/rcm/api_test.go b/internal/rcm/api_test.go
index 8b1437714..01858cd90 100644
--- a/internal/rcm/api_test.go
+++ b/internal/rcm/api_test.go
@@ -9,7 +9,8 @@ import (
 	"testing"
 
 	"github.com/google/uuid"
-	test_distro "github.com/osbuild/osbuild-composer/internal/distro/test"
+	"github.com/osbuild/osbuild-composer/internal/distro"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedoratest"
 	"github.com/osbuild/osbuild-composer/internal/rcm"
 	"github.com/osbuild/osbuild-composer/internal/store"
 )
@@ -42,18 +43,19 @@ func TestBasicRcmAPI(t *testing.T) {
 		{"GET", "/v1/compose", ``, "", http.StatusMethodNotAllowed, ``},
 		{"POST", "/v1/compose", `{"status":"RUNNING"}`, "application/json", http.StatusBadRequest, ``},
 		{"POST", "/v1/compose", `{"status":"RUNNING"}`, "text/plain", http.StatusBadRequest, ``},
-		{"POST", "/v1/compose", `{"distribution": "fedora-30", "image_types": ["test_output"], "architectures":["test_arch"], "repositories": [{"url": "aaa", "checksum": "bbb"}]}`, "application/json", http.StatusOK, `{"compose_id":".*"}`},
+		{"POST", "/v1/compose", `{"distribution": "fedora-30", "image_types": ["qcow2"], "architectures":["x86_64"], "repositories": [{"url": "aaa"}]}`, "application/json", http.StatusOK, `{"compose_id":".*"}`},
 		{"POST", "/v1/compose/111-222-333", `{"status":"RUNNING"}`, "application/json", http.StatusMethodNotAllowed, ``},
 		{"GET", "/v1/compose/7802c476-9cd1-41b7-ba81-43c1906bce73", `{"status":"RUNNING"}`, "application/json", http.StatusBadRequest, `{"error_reason":"Compose UUID does not exist"}`},
 	}
 
-	distro := test_distro.New()
-	api := rcm.New(nil, store.New(nil, distro))
+	registry := distro.NewRegistry([]string{"."})
+	distroStruct := fedoratest.New()
+	api := rcm.New(nil, store.New(nil, distroStruct, *registry))
 
 	for _, c := range cases {
 		resp := internalRequest(api, c.Method, c.Path, c.Body, c.ContentType)
 		buf := new(bytes.Buffer)
-		buf.ReadFrom(resp.Body)
+		_, _ = buf.ReadFrom(resp.Body)
 		response_body := buf.String()
 		if resp.StatusCode != c.ExpectedStatus {
 			t.Errorf("%s request to %s should return status code %d but returns %d, response: %s", c.Method, c.Path, c.ExpectedStatus, resp.StatusCode, response_body)
@@ -70,8 +72,9 @@ func TestBasicRcmAPI(t *testing.T) {
 
 func TestSubmitCompose(t *testing.T) {
 	// Test the most basic use case: Submit a new job and get its status.
-	distro := test_distro.New()
-	api := rcm.New(nil, store.New(nil, distro))
+	registry := distro.NewRegistry([]string{"."})
+	distroStruct := fedoratest.New()
+	api := rcm.New(nil, store.New(nil, distroStruct, *registry))
 
 	var submit_reply struct {
 		UUID uuid.UUID `json:"compose_id"`
@@ -80,28 +83,46 @@ func TestSubmitCompose(t *testing.T) {
 		Status      string `json:"status,omitempty"`
 		ErrorReason string `json:"error_reason,omitempty"`
 	}
-	// Submit job
-	t.Log("RCM API submit compose")
-	resp := internalRequest(api, "POST", "/v1/compose", `{"distribution": "fedora-30", "image_types": ["test_output"], "architectures":["test_arch"], "repositories": [{"url": "aaa", "checksum": "bbb"}]}`, "application/json")
-	if resp.StatusCode != http.StatusOK {
-		t.Fatal("Failed to call /v1/compose, HTTP status code:", resp.StatusCode)
-	}
-	decoder := json.NewDecoder(resp.Body)
-	decoder.DisallowUnknownFields()
-	err := decoder.Decode(&submit_reply)
-	if err != nil {
-		t.Fatal("Failed to decode response to /v1/compose:", err)
-	}
-	// Get the status
-	t.Log("RCM API get status")
-	resp = internalRequest(api, "GET", "/v1/compose/"+submit_reply.UUID.String(), "", "")
-	decoder = json.NewDecoder(resp.Body)
-	decoder.DisallowUnknownFields()
-	err = decoder.Decode(&status_reply)
-	if err != nil {
-		t.Fatal("Failed to decode response to /v1/compose/UUID:", err)
+
+	var cases = []struct {
+		Method      string
+		Path        string
+		Body        string
+		ContentType string
+	}{
+		{
+			"POST",
+			"/v1/compose",
+			`{"distribution": "fedora-30", "image_types": ["qcow2"], "architectures":["x86_64"], "repositories": [{"url": "aaa"}]}`,
+			"application/json",
+		},
 	}
-	if status_reply.ErrorReason != "" {
-		t.Error("Failed to get compose status, reason:", status_reply.ErrorReason)
+
+	for n, c := range cases {
+		// Submit job
+		t.Logf("RCM API submit compose, case %d\n", n)
+		//resp := internalRequest(api, "POST", "/v1/compose", `{"distribution": "fedora-30", "image_types": ["test_output"], "architectures":["test_arch"], "repositories": [{"url": "aaa", "checksum": "bbb"}]}`, "application/json")
+		resp := internalRequest(api, c.Method, c.Path, c.Body, c.ContentType)
+		if resp.StatusCode != http.StatusOK {
+			t.Fatal("Failed to call /v1/compose, HTTP status code:", resp.StatusCode)
+		}
+		decoder := json.NewDecoder(resp.Body)
+		decoder.DisallowUnknownFields()
+		err := decoder.Decode(&submit_reply)
+		if err != nil {
+			t.Fatal("Failed to decode response to /v1/compose:", err)
+		}
+		// Get the status
+		t.Log("RCM API get status")
+		resp = internalRequest(api, "GET", "/v1/compose/"+submit_reply.UUID.String(), "", "")
+		decoder = json.NewDecoder(resp.Body)
+		decoder.DisallowUnknownFields()
+		err = decoder.Decode(&status_reply)
+		if err != nil {
+			t.Fatal("Failed to decode response to /v1/compose/UUID:", err)
+		}
+		if status_reply.ErrorReason != "" {
+			t.Error("Failed to get compose status, reason:", status_reply.ErrorReason)
+		}
 	}
 }
diff --git a/internal/weldr/api_test.go b/internal/weldr/api_test.go
index ad22e3d9b..b8dd065cb 100644
--- a/internal/weldr/api_test.go
+++ b/internal/weldr/api_test.go
@@ -3,6 +3,9 @@ package weldr_test
 import (
 	"archive/tar"
 	"bytes"
+	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/compose"
+	"github.com/osbuild/osbuild-composer/internal/target"
 	"io"
 	"math/rand"
 	"net/http"
@@ -13,11 +16,9 @@ import (
 	"time"
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
-	_ "github.com/osbuild/osbuild-composer/internal/distro/test"
-	test_distro "github.com/osbuild/osbuild-composer/internal/distro/test"
+	test_distro "github.com/osbuild/osbuild-composer/internal/distro/fedoratest"
 	rpmmd_mock "github.com/osbuild/osbuild-composer/internal/mocks/rpmmd"
 	"github.com/osbuild/osbuild-composer/internal/store"
-	"github.com/osbuild/osbuild-composer/internal/target"
 	"github.com/osbuild/osbuild-composer/internal/test"
 	"github.com/osbuild/osbuild-composer/internal/weldr"
 
@@ -31,7 +32,7 @@ func createWeldrAPI(fixtureGenerator rpmmd_mock.FixtureGenerator) (*weldr.API, *
 	rpm := rpmmd_mock.NewRPMMDMock(fixture)
 	d := test_distro.New()
 
-	return weldr.New(rpm, "test_arch", d, nil, fixture.Store), fixture.Store
+	return weldr.New(rpm, "x86_64", d, nil, fixture.Store), fixture.Store
 }
 
 func TestBasic(t *testing.T) {
@@ -47,8 +48,8 @@ func TestBasic(t *testing.T) {
 		{"/api/v0/projects/source/info", http.StatusNotFound, ``},
 		{"/api/v0/projects/source/info/", http.StatusNotFound, `{"errors":[{"code":404,"id":"HTTPError","msg":"Not Found"}],"status":false}`},
 		{"/api/v0/projects/source/info/foo", http.StatusOK, `{"errors":[{"id":"UnknownSource","msg":"foo is not a valid source"}],"sources":{}}`},
-		{"/api/v0/projects/source/info/test-id", http.StatusOK, `{"sources":{"test-id":{"name":"test-id","type":"yum-baseurl","url":"http://example.com/test/os/test_arch","check_gpg":true,"check_ssl":true,"system":true}},"errors":[]}`},
-		{"/api/v0/projects/source/info/*", http.StatusOK, `{"sources":{"test-id":{"name":"test-id","type":"yum-baseurl","url":"http://example.com/test/os/test_arch","check_gpg":true,"check_ssl":true,"system":true}},"errors":[]}`},
+		{"/api/v0/projects/source/info/test-id", http.StatusOK, `{"sources":{"test-id":{"name":"test-id","type":"yum-baseurl","url":"http://example.com/test/os/x86_64","check_gpg":true,"check_ssl":true,"system":true}},"errors":[]}`},
+		{"/api/v0/projects/source/info/*", http.StatusOK, `{"sources":{"test-id":{"name":"test-id","type":"yum-baseurl","url":"http://example.com/test/os/x86_64","check_gpg":true,"check_ssl":true,"system":true}},"errors":[]}`},
 
 		{"/api/v0/blueprints/list", http.StatusOK, `{"total":1,"offset":0,"limit":1,"blueprints":["test"]}`},
 		{"/api/v0/blueprints/info/", http.StatusNotFound, `{"errors":[{"code":404,"id":"HTTPError","msg":"Not Found"}],"status":false}`},
@@ -293,8 +294,7 @@ func TestBlueprintsDepsolve(t *testing.T) {
 }
 
 func TestCompose(t *testing.T) {
-	expectedComposeLocal := &store.Compose{
-		QueueStatus: "WAITING",
+	expectedComposeLocal := &compose.Compose{
 		Blueprint: &blueprint.Blueprint{
 			Name:           "test",
 			Version:        "0.0.0",
@@ -303,12 +303,16 @@ func TestCompose(t *testing.T) {
 			Groups:         []blueprint.Group{},
 			Customizations: nil,
 		},
-		OutputType: "test_output",
-		Targets:    []*target.Target{},
+		ImageBuilds: []compose.ImageBuild{
+			{
+				QueueStatus: common.IBWaiting,
+				ImageType:   common.Qcow2Generic,
+				Targets:     []*target.Target{},
+			},
+		},
 	}
-	expectedComposeLocalAndAws := &store.Compose{
-		QueueStatus: "WAITING",
-		Blueprint: &blueprint.Blueprint{
+	expectedComposeLocalAndAws := &compose.Compose{
+	Blueprint: &blueprint.Blueprint{
 			Name:           "test",
 			Version:        "0.0.0",
 			Packages:       []blueprint.Package{},
@@ -316,18 +320,23 @@ func TestCompose(t *testing.T) {
 			Groups:         []blueprint.Group{},
 			Customizations: nil,
 		},
-		OutputType: "test_output",
-		Targets: []*target.Target{
+		ImageBuilds: []compose.ImageBuild{
 			{
-				Name:      "org.osbuild.aws",
-				Status:    "WAITING",
-				ImageName: "test_upload",
-				Options: &target.AWSTargetOptions{
-					Region:          "frankfurt",
-					AccessKeyID:     "accesskey",
-					SecretAccessKey: "secretkey",
-					Bucket:          "clay",
-					Key:             "imagekey",
+				QueueStatus: common.IBWaiting,
+				ImageType:   common.Qcow2Generic,
+				Targets: []*target.Target{
+					{
+						Name:      "org.osbuild.aws",
+						Status:    common.IBWaiting,
+						ImageName: "test_upload",
+						Options: &target.AWSTargetOptions{
+							Region:          "frankfurt",
+							AccessKeyID:     "accesskey",
+							SecretAccessKey: "secretkey",
+							Bucket:          "clay",
+							Key:             "imagekey",
+						},
+					},
 				},
 			},
 		},
@@ -340,12 +349,12 @@ func TestCompose(t *testing.T) {
 		Body            string
 		ExpectedStatus  int
 		ExpectedJSON    string
-		ExpectedCompose *store.Compose
+		ExpectedCompose *compose.Compose
 		IgnoreFields    []string
 	}{
-		{true, "POST", "/api/v0/compose", `{"blueprint_name": "http-server","compose_type": "test_output","branch": "master"}`, http.StatusBadRequest, `{"status":false,"errors":[{"id":"UnknownBlueprint","msg":"Unknown blueprint name: http-server"}]}`, nil, []string{"build_id"}},
-		{false, "POST", "/api/v0/compose", `{"blueprint_name": "test","compose_type": "test_output","branch": "master"}`, http.StatusOK, `{"status": true}`, expectedComposeLocal, []string{"build_id"}},
-		{false, "POST", "/api/v1/compose", `{"blueprint_name": "test","compose_type":"test_output","branch":"master","upload":{"image_name":"test_upload","provider":"aws","settings":{"region":"frankfurt","accessKeyID":"accesskey","secretAccessKey":"secretkey","bucket":"clay","key":"imagekey"}}}`, http.StatusOK, `{"status": true}`, expectedComposeLocalAndAws, []string{"build_id"}},
+		{true, "POST", "/api/v0/compose", `{"blueprint_name": "http-server","compose_type": "qcow2","branch": "master"}`, http.StatusBadRequest, `{"status":false,"errors":[{"id":"UnknownBlueprint","msg":"Unknown blueprint name: http-server"}]}`, nil, []string{"build_id"}},
+		{false, "POST", "/api/v0/compose", `{"blueprint_name": "test","compose_type": "qcow2","branch": "master"}`, http.StatusOK, `{"status": true}`, expectedComposeLocal, []string{"build_id"}},
+		{false, "POST", "/api/v1/compose", `{"blueprint_name": "test","compose_type":"qcow2","branch":"master","upload":{"image_name":"test_upload","provider":"aws","settings":{"region":"frankfurt","accessKeyID":"accesskey","secretAccessKey":"secretkey","bucket":"clay","key":"imagekey"}}}`, http.StatusOK, `{"status": true}`, expectedComposeLocalAndAws, []string{"build_id"}},
 	}
 
 	for _, c := range cases {
@@ -361,20 +370,20 @@ func TestCompose(t *testing.T) {
 		}
 
 		// I have no idea how to get the compose in better way
-		var compose store.Compose
+		var composeStruct compose.Compose
 		for _, c := range s.Composes {
-			compose = c
+			composeStruct = c
 			break
 		}
 
-		if compose.Pipeline == nil {
+		if composeStruct.ImageBuilds[0].Pipeline == nil {
 			t.Fatalf("%s: the compose in the store did not contain a blueprint", c.Path)
 		} else {
 			// TODO: find some (reasonable) way to verify the contents of the pipeline
-			compose.Pipeline = nil
+			composeStruct.ImageBuilds[0].Pipeline = nil
 		}
 
-		if diff := cmp.Diff(compose, *c.ExpectedCompose, test.IgnoreDates(), test.IgnoreUuids(), test.Ignore("Targets.Options.Location")); diff != "" {
+		if diff := cmp.Diff(composeStruct, *c.ExpectedCompose, test.IgnoreDates(), test.IgnoreUuids(), test.Ignore("Targets.Options.Location")); diff != "" {
 			t.Errorf("%s: compose in store isn't the same as expected, diff:\n%s", c.Path, diff)
 		}
 
@@ -425,9 +434,9 @@ func TestComposeStatus(t *testing.T) {
 		ExpectedStatus int
 		ExpectedJSON   string
 	}{
-		{rpmmd_mock.BaseFixture, "GET", "/api/v0/compose/status/30000000-0000-0000-0000-000000000000,30000000-0000-0000-0000-000000000002", ``, http.StatusOK, `{"uuids":[{"id":"30000000-0000-0000-0000-000000000000","blueprint":"test","version":"0.0.0","compose_type":"test_output","image_size":0,"queue_status":"WAITING","job_created":1574857140},{"id":"30000000-0000-0000-0000-000000000002","blueprint":"test","version":"0.0.0","compose_type":"test_output","image_size":0,"queue_status":"FINISHED","job_created":1574857140,"job_started":1574857140,"job_finished":1574857140}]}`},
-		{rpmmd_mock.BaseFixture, "GET", "/api/v0/compose/status/*", ``, http.StatusOK, `{"uuids":[{"id":"30000000-0000-0000-0000-000000000000","blueprint":"test","version":"0.0.0","compose_type":"test_output","image_size":0,"queue_status":"WAITING","job_created":1574857140},{"id":"30000000-0000-0000-0000-000000000001","blueprint":"test","version":"0.0.0","compose_type":"test_output","image_size":0,"queue_status":"RUNNING","job_created":1574857140,"job_started":1574857140},{"id":"30000000-0000-0000-0000-000000000002","blueprint":"test","version":"0.0.0","compose_type":"test_output","image_size":0,"queue_status":"FINISHED","job_created":1574857140,"job_started":1574857140,"job_finished":1574857140},{"id":"30000000-0000-0000-0000-000000000003","blueprint":"test","version":"0.0.0","compose_type":"test_output","image_size":0,"queue_status":"FAILED","job_created":1574857140,"job_started":1574857140,"job_finished":1574857140}]}`},
-		{rpmmd_mock.BaseFixture, "GET", "/api/v1/compose/status/30000000-0000-0000-0000-000000000000", ``, http.StatusOK, `{"uuids":[{"id":"30000000-0000-0000-0000-000000000000","blueprint":"test","version":"0.0.0","compose_type":"test_output","image_size":0,"queue_status":"WAITING","job_created":1574857140,"uploads":[{"uuid":"10000000-0000-0000-0000-000000000000","status":"WAITING","provider_name":"aws","image_name":"awsimage","creation_time":1574857140,"settings":{"region":"frankfurt","accessKeyID":"accesskey","secretAccessKey":"secretkey","bucket":"clay","key":"imagekey"}}]}]}`},
+		{rpmmd_mock.BaseFixture, "GET", "/api/v0/compose/status/30000000-0000-0000-0000-000000000000,30000000-0000-0000-0000-000000000002", ``, http.StatusOK, `{"uuids":[{"id":"30000000-0000-0000-0000-000000000000","blueprint":"test","version":"0.0.0","compose_type":"qcow2","image_size":0,"queue_status":"WAITING","job_created":1574857140},{"id":"30000000-0000-0000-0000-000000000002","blueprint":"test","version":"0.0.0","compose_type":"qcow2","image_size":0,"queue_status":"FINISHED","job_created":1574857140,"job_started":1574857140,"job_finished":1574857140}]}`},
+		{rpmmd_mock.BaseFixture, "GET", "/api/v0/compose/status/*", ``, http.StatusOK, `{"uuids":[{"id":"30000000-0000-0000-0000-000000000000","blueprint":"test","version":"0.0.0","compose_type":"qcow2","image_size":0,"queue_status":"WAITING","job_created":1574857140},{"id":"30000000-0000-0000-0000-000000000001","blueprint":"test","version":"0.0.0","compose_type":"qcow2","image_size":0,"queue_status":"RUNNING","job_created":1574857140,"job_started":1574857140},{"id":"30000000-0000-0000-0000-000000000002","blueprint":"test","version":"0.0.0","compose_type":"qcow2","image_size":0,"queue_status":"FINISHED","job_created":1574857140,"job_started":1574857140,"job_finished":1574857140},{"id":"30000000-0000-0000-0000-000000000003","blueprint":"test","version":"0.0.0","compose_type":"qcow2","image_size":0,"queue_status":"FAILED","job_created":1574857140,"job_started":1574857140,"job_finished":1574857140}]}`},
+		{rpmmd_mock.BaseFixture, "GET", "/api/v1/compose/status/30000000-0000-0000-0000-000000000000", ``, http.StatusOK, `{"uuids":[{"id":"30000000-0000-0000-0000-000000000000","blueprint":"test","version":"0.0.0","compose_type":"qcow2","image_size":0,"queue_status":"WAITING","job_created":1574857140,"uploads":[{"uuid":"10000000-0000-0000-0000-000000000000","status":"WAITING","provider_name":"aws","image_name":"awsimage","creation_time":1574857140,"settings":{"region":"frankfurt","accessKeyID":"accesskey","secretAccessKey":"secretkey","bucket":"clay","key":"imagekey"}}]}]}`},
 	}
 
 	if len(os.Getenv("OSBUILD_COMPOSER_TEST_EXTERNAL")) > 0 {
@@ -449,8 +458,8 @@ func TestComposeInfo(t *testing.T) {
 		ExpectedStatus int
 		ExpectedJSON   string
 	}{
-		{rpmmd_mock.BaseFixture, "GET", "/api/v0/compose/info/30000000-0000-0000-0000-000000000000", ``, http.StatusOK, `{"id":"30000000-0000-0000-0000-000000000000","config":"","blueprint":{"name":"test","description":"","version":"0.0.0","packages":[],"modules":[],"groups":[]},"commit":"","deps":{"packages":[]},"compose_type":"test_output","queue_status":"WAITING","image_size":0}`},
-		{rpmmd_mock.BaseFixture, "GET", "/api/v1/compose/info/30000000-0000-0000-0000-000000000000", ``, http.StatusOK, `{"id":"30000000-0000-0000-0000-000000000000","config":"","blueprint":{"name":"test","description":"","version":"0.0.0","packages":[],"modules":[],"groups":[]},"commit":"","deps":{"packages":[]},"compose_type":"test_output","queue_status":"WAITING","image_size":0,"uploads":[{"uuid":"10000000-0000-0000-0000-000000000000","status":"WAITING","provider_name":"aws","image_name":"awsimage","creation_time":1574857140,"settings":{"region":"frankfurt","accessKeyID":"accesskey","secretAccessKey":"secretkey","bucket":"clay","key":"imagekey"}}]}`},
+		{rpmmd_mock.BaseFixture, "GET", "/api/v0/compose/info/30000000-0000-0000-0000-000000000000", ``, http.StatusOK, `{"id":"30000000-0000-0000-0000-000000000000","config":"","blueprint":{"name":"test","description":"","version":"0.0.0","packages":[],"modules":[],"groups":[]},"commit":"","deps":{"packages":[]},"compose_type":"qcow2","queue_status":"WAITING","image_size":0}`},
+		{rpmmd_mock.BaseFixture, "GET", "/api/v1/compose/info/30000000-0000-0000-0000-000000000000", ``, http.StatusOK, `{"id":"30000000-0000-0000-0000-000000000000","config":"","blueprint":{"name":"test","description":"","version":"0.0.0","packages":[],"modules":[],"groups":[]},"commit":"","deps":{"packages":[]},"compose_type":"qcow2","queue_status":"WAITING","image_size":0,"uploads":[{"uuid":"10000000-0000-0000-0000-000000000000","status":"WAITING","provider_name":"aws","image_name":"awsimage","creation_time":1574857140,"settings":{"region":"frankfurt","accessKeyID":"accesskey","secretAccessKey":"secretkey","bucket":"clay","key":"imagekey"}}]}`},
 		{rpmmd_mock.BaseFixture, "GET", "/api/v1/compose/info/30000000-0000-0000-0000", ``, http.StatusBadRequest, `{"status":false,"errors":[{"id":"UnknownUUID","msg":"30000000-0000-0000-0000 is not a valid build uuid"}]}`},
 		{rpmmd_mock.BaseFixture, "GET", "/api/v1/compose/info/42000000-0000-0000-0000-000000000000", ``, http.StatusBadRequest, `{"status":false,"errors":[{"id":"UnknownUUID","msg":"42000000-0000-0000-0000-000000000000 is not a valid build uuid"}]}`},
 	}
@@ -571,8 +580,8 @@ func TestComposeQueue(t *testing.T) {
 		ExpectedStatus int
 		ExpectedJSON   string
 	}{
-		{rpmmd_mock.BaseFixture, "GET", "/api/v0/compose/queue", ``, http.StatusOK, `{"new":[{"blueprint":"test","version":"0.0.0","compose_type":"test_output","image_size":0,"queue_status":"WAITING"}],"run":[{"blueprint":"test","version":"0.0.0","compose_type":"test_output","image_size":0,"queue_status":"RUNNING"}]}`},
-		{rpmmd_mock.BaseFixture, "GET", "/api/v1/compose/queue", ``, http.StatusOK, `{"new":[{"blueprint":"test","version":"0.0.0","compose_type":"test_output","image_size":0,"queue_status":"WAITING","uploads":[{"uuid":"10000000-0000-0000-0000-000000000000","status":"WAITING","provider_name":"aws","image_name":"awsimage","creation_time":1574857140,"settings":{"region":"frankfurt","accessKeyID":"accesskey","secretAccessKey":"secretkey","bucket":"clay","key":"imagekey"}}]}],"run":[{"blueprint":"test","version":"0.0.0","compose_type":"test_output","image_size":0,"queue_status":"RUNNING"}]}`},
+		{rpmmd_mock.BaseFixture, "GET", "/api/v0/compose/queue", ``, http.StatusOK, `{"new":[{"blueprint":"test","version":"0.0.0","compose_type":"qcow2","image_size":0,"queue_status":"WAITING"}],"run":[{"blueprint":"test","version":"0.0.0","compose_type":"qcow2","image_size":0,"queue_status":"RUNNING"}]}`},
+		{rpmmd_mock.BaseFixture, "GET", "/api/v1/compose/queue", ``, http.StatusOK, `{"new":[{"blueprint":"test","version":"0.0.0","compose_type":"qcow2","image_size":0,"queue_status":"WAITING","uploads":[{"uuid":"10000000-0000-0000-0000-000000000000","status":"WAITING","provider_name":"aws","image_name":"awsimage","creation_time":1574857140,"settings":{"region":"frankfurt","accessKeyID":"accesskey","secretAccessKey":"secretkey","bucket":"clay","key":"imagekey"}}]}],"run":[{"blueprint":"test","version":"0.0.0","compose_type":"qcow2","image_size":0,"queue_status":"RUNNING"}]}`},
 		{rpmmd_mock.NoComposesFixture, "GET", "/api/v0/compose/queue", ``, http.StatusOK, `{"new":[],"run":[]}`},
 	}
 
@@ -595,8 +604,8 @@ func TestComposeFinished(t *testing.T) {
 		ExpectedStatus int
 		ExpectedJSON   string
 	}{
-		{rpmmd_mock.BaseFixture, "GET", "/api/v0/compose/finished", ``, http.StatusOK, `{"finished":[{"id":"30000000-0000-0000-0000-000000000002","blueprint":"test","version":"0.0.0","compose_type":"test_output","image_size":0,"queue_status":"FINISHED","job_created":1574857140,"job_started":1574857140,"job_finished":1574857140}]}`},
-		{rpmmd_mock.BaseFixture, "GET", "/api/v1/compose/finished", ``, http.StatusOK, `{"finished":[{"id":"30000000-0000-0000-0000-000000000002","blueprint":"test","version":"0.0.0","compose_type":"test_output","image_size":0,"queue_status":"FINISHED","job_created":1574857140,"job_started":1574857140,"job_finished":1574857140,"uploads":[{"uuid":"10000000-0000-0000-0000-000000000000","status":"WAITING","provider_name":"aws","image_name":"awsimage","creation_time":1574857140,"settings":{"region":"frankfurt","accessKeyID":"accesskey","secretAccessKey":"secretkey","bucket":"clay","key":"imagekey"}}]}]}`},
+		{rpmmd_mock.BaseFixture, "GET", "/api/v0/compose/finished", ``, http.StatusOK, `{"finished":[{"id":"30000000-0000-0000-0000-000000000002","blueprint":"test","version":"0.0.0","compose_type":"qcow2","image_size":0,"queue_status":"FINISHED","job_created":1574857140,"job_started":1574857140,"job_finished":1574857140}]}`},
+		{rpmmd_mock.BaseFixture, "GET", "/api/v1/compose/finished", ``, http.StatusOK, `{"finished":[{"id":"30000000-0000-0000-0000-000000000002","blueprint":"test","version":"0.0.0","compose_type":"qcow2","image_size":0,"queue_status":"FINISHED","job_created":1574857140,"job_started":1574857140,"job_finished":1574857140,"uploads":[{"uuid":"10000000-0000-0000-0000-000000000000","status":"WAITING","provider_name":"aws","image_name":"awsimage","creation_time":1574857140,"settings":{"region":"frankfurt","accessKeyID":"accesskey","secretAccessKey":"secretkey","bucket":"clay","key":"imagekey"}}]}]}`},
 		{rpmmd_mock.NoComposesFixture, "GET", "/api/v0/compose/finished", ``, http.StatusOK, `{"finished":[]}`},
 	}
 
@@ -619,8 +628,8 @@ func TestComposeFailed(t *testing.T) {
 		ExpectedStatus int
 		ExpectedJSON   string
 	}{
-		{rpmmd_mock.BaseFixture, "GET", "/api/v0/compose/failed", ``, http.StatusOK, `{"failed":[{"id":"30000000-0000-0000-0000-000000000003","blueprint":"test","version":"0.0.0","compose_type":"test_output","image_size":0,"queue_status":"FAILED","job_created":1574857140,"job_started":1574857140,"job_finished":1574857140}]}`},
-		{rpmmd_mock.BaseFixture, "GET", "/api/v1/compose/failed", ``, http.StatusOK, `{"failed":[{"id":"30000000-0000-0000-0000-000000000003","blueprint":"test","version":"0.0.0","compose_type":"test_output","image_size":0,"queue_status":"FAILED","job_created":1574857140,"job_started":1574857140,"job_finished":1574857140,"uploads":[{"uuid":"10000000-0000-0000-0000-000000000000","status":"WAITING","provider_name":"aws","image_name":"awsimage","creation_time":1574857140,"settings":{"region":"frankfurt","accessKeyID":"accesskey","secretAccessKey":"secretkey","bucket":"clay","key":"imagekey"}}]}]}`},
+		{rpmmd_mock.BaseFixture, "GET", "/api/v0/compose/failed", ``, http.StatusOK, `{"failed":[{"id":"30000000-0000-0000-0000-000000000003","blueprint":"test","version":"0.0.0","compose_type":"qcow2","image_size":0,"queue_status":"FAILED","job_created":1574857140,"job_started":1574857140,"job_finished":1574857140}]}`},
+		{rpmmd_mock.BaseFixture, "GET", "/api/v1/compose/failed", ``, http.StatusOK, `{"failed":[{"id":"30000000-0000-0000-0000-000000000003","blueprint":"test","version":"0.0.0","compose_type":"qcow2","image_size":0,"queue_status":"FAILED","job_created":1574857140,"job_started":1574857140,"job_finished":1574857140,"uploads":[{"uuid":"10000000-0000-0000-0000-000000000000","status":"WAITING","provider_name":"aws","image_name":"awsimage","creation_time":1574857140,"settings":{"region":"frankfurt","accessKeyID":"accesskey","secretAccessKey":"secretkey","bucket":"clay","key":"imagekey"}}]}]}`},
 		{rpmmd_mock.NoComposesFixture, "GET", "/api/v0/compose/failed", ``, http.StatusOK, `{"failed":[]}`},
 	}