Repository: osbuild/osbuild-composer
Pull request #226 (2020-02-11 11:08:14)

api/jobqueue: return 500 error when updating job unexpectedly failed
Prior this commit these errors would just disappear.


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..faa386e31 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:
@@ -132,6 +134,8 @@ func (api *API) updateJobHandler(writer http.ResponseWriter, request *http.Reque
 			statusResponseError(writer, http.StatusBadRequest, err.Error())
 		case *store.InvalidRequestError:
 			statusResponseError(writer, http.StatusBadRequest, err.Error())
+		default:
+			statusResponseError(writer, http.StatusInternalServerError, err.Error())
 		}
 		return
 	}
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":[]}`},
 	}