Repository: osbuild/osbuild-composer
Pull request #378 (2020-03-23 11:15:07)

tests: Add sanity tests for sources. Refs #315, #356


Patch:
diff --git a/Makefile b/Makefile
index 3d6dbe6d7..2bec8fc09 100644
--- a/Makefile
+++ b/Makefile
@@ -11,7 +11,7 @@ build:
 	go test -c -tags=integration -o osbuild-weldr-tests ./internal/weldrcheck/
 	go test -c -tags=integration -o osbuild-dnf-json-tests ./cmd/osbuild-dnf-json-tests/main_test.go
 	go test -c -tags=integration -o osbuild-rcm-tests ./cmd/osbuild-rcm-tests/main_test.go
-	go test -c -tags=integration -o osbuild-image-tests ./cmd/osbuild-image-tests/main_test.go
+	go test -c -tags=integration,travis -o osbuild-image-tests ./cmd/osbuild-image-tests/
 
 .PHONY: install
 install:
diff --git a/cmd/osbuild-composer/main.go b/cmd/osbuild-composer/main.go
index 5e4b39c96..9947a9983 100644
--- a/cmd/osbuild-composer/main.go
+++ b/cmd/osbuild-composer/main.go
@@ -4,12 +4,18 @@ import (
 	"crypto/tls"
 	"crypto/x509"
 	"flag"
-	"github.com/osbuild/osbuild-composer/internal/rcm"
 	"io/ioutil"
 	"log"
 	"os"
 	"path"
 
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora30"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora31"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora32"
+	"github.com/osbuild/osbuild-composer/internal/distro/rhel81"
+	"github.com/osbuild/osbuild-composer/internal/distro/rhel82"
+	"github.com/osbuild/osbuild-composer/internal/rcm"
+
 	"github.com/osbuild/osbuild-composer/internal/common"
 	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/jobqueue"
@@ -84,7 +90,7 @@ func main() {
 
 	rpm := rpmmd.NewRPMMD(path.Join(cacheDirectory, "rpmmd"))
 
-	distros, err := distro.NewDefaultRegistry()
+	distros, err := distro.NewRegistry(fedora30.New(), fedora31.New(), fedora32.New(), rhel81.New(), rhel82.New())
 	if err != nil {
 		log.Fatalf("Error loading distros: %v", err)
 	}
@@ -94,6 +100,11 @@ func main() {
 		log.Fatalf("Could not determine distro from host: " + err.Error())
 	}
 
+	arch, err := distribution.GetArch(common.CurrentArch())
+	if err != nil {
+		log.Fatalf("Host distro does not support host architecture: " + err.Error())
+	}
+
 	repoMap, err := rpmmd.LoadRepositories([]string{"/etc/osbuild-composer", "/usr/share/osbuild-composer"}, distribution.Name())
 	if err != nil {
 		log.Fatalf("Could not load repositories for %s: %v", distribution.Name(), err)
@@ -107,7 +118,7 @@ func main() {
 	store := store.New(&stateDir)
 
 	jobAPI := jobqueue.New(logger, store)
-	weldrAPI := weldr.New(rpm, common.CurrentArch(), distribution, repoMap[common.CurrentArch()], logger, store)
+	weldrAPI := weldr.New(rpm, arch, distribution, repoMap[common.CurrentArch()], logger, store)
 
 	go func() {
 		err := jobAPI.Serve(jobListener)
diff --git a/cmd/osbuild-pipeline/main.go b/cmd/osbuild-pipeline/main.go
index 8264d3117..1b65a9412 100644
--- a/cmd/osbuild-pipeline/main.go
+++ b/cmd/osbuild-pipeline/main.go
@@ -9,7 +9,11 @@ import (
 	"os"
 	"path"
 
-	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora30"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora31"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora32"
+	"github.com/osbuild/osbuild-composer/internal/distro/rhel81"
+	"github.com/osbuild/osbuild-composer/internal/distro/rhel82"
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/distro"
@@ -23,12 +27,12 @@ type rpmMD struct {
 }
 
 func main() {
-	var imageType string
+	var imageTypeArg string
 	var blueprintArg string
 	var archArg string
 	var distroArg string
 	var rpmmdArg bool
-	flag.StringVar(&imageType, "image-type", "", "image type, e.g. qcow2 or ami")
+	flag.StringVar(&imageTypeArg, "image-type", "", "image type, e.g. qcow2 or ami")
 	flag.StringVar(&archArg, "arch", "", "architecture to create image for, e.g. x86_64")
 	flag.StringVar(&distroArg, "distro", "", "distribution to create, e.g. fedora-30")
 	flag.BoolVar(&rpmmdArg, "rpmmd", false, "output rpmmd struct instead of pipeline manifest")
@@ -38,20 +42,11 @@ func main() {
 	blueprintArg = flag.Arg(0)
 
 	// Print help usage if one of the required arguments wasn't provided
-	if imageType == "" || archArg == "" || distroArg == "" {
+	if imageTypeArg == "" || archArg == "" || distroArg == "" {
 		flag.Usage()
 		return
 	}
 
-	// Validate architecture
-	if !common.ArchitectureExists(archArg) {
-		_, _ = fmt.Fprintf(os.Stderr, "The provided architecture (%s) is not supported. Use one of these:\n", archArg)
-		for _, arch := range common.ListArchitectures() {
-			_, _ = fmt.Fprintln(os.Stderr, " *", arch)
-		}
-		return
-	}
-
 	blueprint := &blueprint.Blueprint{}
 	if blueprintArg != "" {
 		var reader io.Reader
@@ -74,7 +69,7 @@ func main() {
 		}
 	}
 
-	distros, err := distro.NewDefaultRegistry()
+	distros, err := distro.NewRegistry(fedora30.New(), fedora31.New(), fedora32.New(), rhel81.New(), rhel82.New())
 	if err != nil {
 		panic(err)
 	}
@@ -88,6 +83,18 @@ func main() {
 		return
 	}
 
+	arch, err := d.GetArch(archArg)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "The provided architecture (%s) is not supported by distro %s.: %s\n", archArg, distroArg, err.Error())
+		return
+	}
+
+	imageType, err := arch.GetImageType(imageTypeArg)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "The provided image type (%s) is not supported by architecture %s on distro %s.: %s\n", imageTypeArg, archArg, distroArg, err.Error())
+		return
+	}
+
 	repos, err := rpmmd.LoadRepositories([]string{"."}, distroArg)
 	if err != nil {
 		panic(err)
@@ -105,10 +112,7 @@ func main() {
 		}
 	}
 
-	pkgs, exclude_pkgs, err := d.BasePackages(imageType, archArg)
-	if err != nil {
-		panic("could not get base packages: " + err.Error())
-	}
+	pkgs, exclude_pkgs := imageType.BasePackages()
 	packages = append(pkgs, packages...)
 
 	home, err := os.UserHomeDir()
@@ -122,10 +126,7 @@ func main() {
 		panic("Could not depsolve: " + err.Error())
 	}
 
-	buildPkgs, err := d.BuildPackages(archArg)
-	if err != nil {
-		panic("Could not get build packages: " + err.Error())
-	}
+	buildPkgs := imageType.BuildPackages()
 	buildPackageSpecs, _, err := rpmmd.Depsolve(buildPkgs, nil, repos[archArg], d.ModulePlatformID())
 	if err != nil {
 		panic("Could not depsolve build packages: " + err.Error())
@@ -143,8 +144,7 @@ func main() {
 			panic("could not marshal rpmmd struct into JSON")
 		}
 	} else {
-		size := d.GetSizeForOutputType(imageType, 0)
-		manifest, err := d.Manifest(blueprint.Customizations, repos[archArg], packageSpecs, buildPackageSpecs, archArg, imageType, size)
+		manifest, err := imageType.Manifest(blueprint.Customizations, repos[archArg], packageSpecs, buildPackageSpecs, imageType.Size(0))
 		if err != nil {
 			panic(err.Error())
 		}
diff --git a/internal/client/blueprints.go b/internal/client/blueprints.go
index 2df7e8381..15731462b 100644
--- a/internal/client/blueprints.go
+++ b/internal/client/blueprints.go
@@ -7,6 +7,8 @@ import (
 	"fmt"
 	"net/http"
 	"strings"
+
+	"github.com/osbuild/osbuild-composer/internal/weldr"
 )
 
 // PostTOMLBlueprintV0 sends a TOML blueprint string to the API
@@ -73,7 +75,7 @@ func ListBlueprintsV0(socket *http.Client) ([]string, *APIResponse, error) {
 	if resp != nil || err != nil {
 		return nil, resp, err
 	}
-	var list BlueprintsListV0
+	var list weldr.BlueprintsListV0
 	err = json.Unmarshal(body, &list)
 	if err != nil {
 		return nil, nil, err
@@ -91,30 +93,30 @@ func GetBlueprintInfoTOMLV0(socket *http.Client, bpName string) (string, *APIRes
 }
 
 // GetBlueprintsInfoJSONV0 returns the requested blueprints and their changed state
-func GetBlueprintsInfoJSONV0(socket *http.Client, bpName string) (BlueprintsInfoV0, *APIResponse, error) {
+func GetBlueprintsInfoJSONV0(socket *http.Client, bpName string) (weldr.BlueprintsInfoV0, *APIResponse, error) {
 	body, resp, err := GetRaw(socket, "GET", "/api/v0/blueprints/info/"+bpName)
 	if resp != nil || err != nil {
-		return BlueprintsInfoV0{}, resp, err
+		return weldr.BlueprintsInfoV0{}, resp, err
 	}
-	var info BlueprintsInfoV0
+	var info weldr.BlueprintsInfoV0
 	err = json.Unmarshal(body, &info)
 	if err != nil {
-		return BlueprintsInfoV0{}, nil, err
+		return weldr.BlueprintsInfoV0{}, nil, err
 	}
 	return info, nil, nil
 }
 
 // GetBlueprintsChangesV0 returns the changes to the listed blueprints
-func GetBlueprintsChangesV0(socket *http.Client, bpNames []string) (BlueprintsChangesV0, *APIResponse, error) {
+func GetBlueprintsChangesV0(socket *http.Client, bpNames []string) (weldr.BlueprintsChangesV0, *APIResponse, error) {
 	names := strings.Join(bpNames, ",")
 	body, resp, err := GetRaw(socket, "GET", "/api/v0/blueprints/changes/"+names)
 	if resp != nil || err != nil {
-		return BlueprintsChangesV0{}, resp, err
+		return weldr.BlueprintsChangesV0{}, resp, err
 	}
-	var changes BlueprintsChangesV0
+	var changes weldr.BlueprintsChangesV0
 	err = json.Unmarshal(body, &changes)
 	if err != nil {
-		return BlueprintsChangesV0{}, nil, err
+		return weldr.BlueprintsChangesV0{}, nil, err
 	}
 	return changes, nil, nil
 }
@@ -139,30 +141,30 @@ func TagBlueprintV0(socket *http.Client, blueprint string) (*APIResponse, error)
 }
 
 // DepsolveBlueprintV0 depsolves the listed blueprint
-func DepsolveBlueprintV0(socket *http.Client, blueprint string) (BlueprintsDepsolveV0, *APIResponse, error) {
+func DepsolveBlueprintV0(socket *http.Client, blueprint string) (weldr.BlueprintsDepsolveV0, *APIResponse, error) {
 	body, resp, err := GetRaw(socket, "GET", "/api/v0/blueprints/depsolve/"+blueprint)
 	if resp != nil || err != nil {
-		return BlueprintsDepsolveV0{}, resp, err
+		return weldr.BlueprintsDepsolveV0{}, resp, err
 	}
-	var deps BlueprintsDepsolveV0
+	var deps weldr.BlueprintsDepsolveV0
 	err = json.Unmarshal(body, &deps)
 	if err != nil {
-		return BlueprintsDepsolveV0{}, nil, err
+		return weldr.BlueprintsDepsolveV0{}, nil, err
 	}
 	return deps, nil, nil
 }
 
 // FreezeBlueprintV0 depsolves the listed blueprint and returns the blueprint with frozen package
 // versions
-func FreezeBlueprintV0(socket *http.Client, blueprint string) (BlueprintsFreezeV0, *APIResponse, error) {
+func FreezeBlueprintV0(socket *http.Client, blueprint string) (weldr.BlueprintsFreezeV0, *APIResponse, error) {
 	body, resp, err := GetRaw(socket, "GET", "/api/v0/blueprints/freeze/"+blueprint)
 	if resp != nil || err != nil {
-		return BlueprintsFreezeV0{}, resp, err
+		return weldr.BlueprintsFreezeV0{}, resp, err
 	}
-	var frozen BlueprintsFreezeV0
+	var frozen weldr.BlueprintsFreezeV0
 	err = json.Unmarshal(body, &frozen)
 	if err != nil {
-		return BlueprintsFreezeV0{}, nil, err
+		return weldr.BlueprintsFreezeV0{}, nil, err
 	}
 	return frozen, nil, nil
 }
diff --git a/internal/client/source.go b/internal/client/source.go
new file mode 100644
index 000000000..c004c2692
--- /dev/null
+++ b/internal/client/source.go
@@ -0,0 +1,69 @@
+// Package client - source contains functions for the source API
+// Copyright (C) 2020 by Red Hat, Inc.
+package client
+
+import (
+	"encoding/json"
+	//	"fmt"
+	"net/http"
+	//	"strings"
+
+	"github.com/osbuild/osbuild-composer/internal/weldr"
+)
+
+// ListSourcesV0 returns a list of source names
+func ListSourcesV0(socket *http.Client) ([]string, *APIResponse, error) {
+	body, resp, err := GetRaw(socket, "GET", "/api/v0/projects/source/list")
+	if resp != nil || err != nil {
+		return nil, resp, err
+	}
+	var list weldr.SourceListV0
+	err = json.Unmarshal(body, &list)
+	if err != nil {
+		return nil, nil, err
+	}
+	return list.Sources, nil, nil
+}
+
+// GetSourceInfoV0 returns detailed information on the named sources
+func GetSourceInfoV0(socket *http.Client, sourceNames string) (map[string]weldr.SourceConfigV0, *APIResponse, error) {
+	body, resp, err := GetRaw(socket, "GET", "/api/v0/projects/source/info/"+sourceNames)
+	if resp != nil || err != nil {
+		return nil, resp, err
+	}
+	var info weldr.SourceInfoV0
+	err = json.Unmarshal(body, &info)
+	if err != nil {
+		return nil, nil, err
+	}
+	return info.Sources, nil, nil
+}
+
+// PostJSONSourceV0 sends a JSON source string to the API
+// and returns an APIResponse
+func PostJSONSourceV0(socket *http.Client, source string) (*APIResponse, error) {
+	body, resp, err := PostJSON(socket, "/api/v0/projects/source/new", source)
+	if resp != nil || err != nil {
+		return resp, err
+	}
+	return NewAPIResponse(body)
+}
+
+// PostTOMLSourceV0 sends a TOML source string to the API
+// and returns an APIResponse
+func PostTOMLSourceV0(socket *http.Client, source string) (*APIResponse, error) {
+	body, resp, err := PostTOML(socket, "/api/v0/projects/source/new", source)
+	if resp != nil || err != nil {
+		return resp, err
+	}
+	return NewAPIResponse(body)
+}
+
+// DeleteSourceV0 deletes the named source and returns an APIResponse
+func DeleteSourceV0(socket *http.Client, sourceName string) (*APIResponse, error) {
+	body, resp, err := DeleteRaw(socket, "/api/v0/projects/source/delete/"+sourceName)
+	if resp != nil || err != nil {
+		return resp, err
+	}
+	return NewAPIResponse(body)
+}
diff --git a/internal/client/weldr.go b/internal/client/weldr.go
index dcb9feede..1090ee96a 100644
--- a/internal/client/weldr.go
+++ b/internal/client/weldr.go
@@ -5,10 +5,12 @@ package client
 import (
 	"encoding/json"
 	"net/http"
+
+	"github.com/osbuild/osbuild-composer/internal/weldr"
 )
 
 // GetStatusV0 makes a GET request to /api/status and returns the v0 response as a StatusResponseV0
-func GetStatusV0(socket *http.Client) (reply StatusV0, resp *APIResponse, err error) {
+func GetStatusV0(socket *http.Client) (reply weldr.StatusV0, resp *APIResponse, err error) {
 	body, resp, err := GetRaw(socket, "GET", "/api/status")
 	if resp != nil || err != nil {
 		return reply, resp, err
diff --git a/internal/distro/distro.go b/internal/distro/distro.go
index 524c52ee2..1e02e9d59 100644
--- a/internal/distro/distro.go
+++ b/internal/distro/distro.go
@@ -9,106 +9,94 @@ import (
 	"sort"
 	"strings"
 
-	"github.com/osbuild/osbuild-composer/internal/common"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
-
-	"github.com/osbuild/osbuild-composer/internal/distro/fedora30"
-	"github.com/osbuild/osbuild-composer/internal/distro/fedora31"
-	"github.com/osbuild/osbuild-composer/internal/distro/fedora32"
-	"github.com/osbuild/osbuild-composer/internal/distro/rhel81"
-	"github.com/osbuild/osbuild-composer/internal/distro/rhel82"
 )
 
+// A Distro represents composer's notion of what a given distribution is.
 type Distro interface {
-	// Returns the name of the distro. This is the same name that was
-	// passed to New().
+	// Returns the name of the distro.
 	Name() string
 
-	// Return strong-typed distribution
-	Distribution() common.Distribution
-
 	// Returns the module platform id of the distro. This is used by DNF
 	// for modularity support.
 	ModulePlatformID() string
 
-	// Returns a sorted list of the output formats this distro supports.
-	ListOutputFormats() []string
+	// Returns an object representing the given architecture as support
+	// by this distro.
+	GetArch(arch string) (Arch, error)
 
 	// Returns the canonical filename and MIME type for a given output
 	// format. `outputFormat` must be one returned by
+	// TODO: remove when redundant
 	FilenameFromType(outputFormat string) (string, string, error)
+}
+
+// An Arch represents a given distribution's support for a given architecture.
+type Arch interface {
+	// Returns the name of the architecture.
+	Name() string
+
+	// Returns a sorted list of the names of the image types this architecture
+	// supports.
+	ListImageTypes() []string
+
+	// Returns an object representing a given image format for this architecture,
+	// on this distro.
+	GetImageType(imageType string) (ImageType, error)
+}
+
+// An ImageType represents a given distribution's support for a given Image Type
+// for a given architecture.
+type ImageType interface {
+	// Returns the name of the image type.
+	Name() string
+
+	// Returns the canonical filename for the image type.
+	Filename() string
 
-	// Returns the proper image size for a given output format. If the size
+	// Retrns the MIME-type for the image type.
+	MIMEType() string
+
+	// Returns the proper image size for a given output format. If the input size
 	// is 0 the default value for the format will be returned.
-	GetSizeForOutputType(outputFormat string, size uint64) uint64
+	Size(size uint64) uint64
 
-	// Returns the base packages for a given output type and architecture
-	BasePackages(outputFormat, outputArchitecture string) ([]string, []string, error)
+	// Returns the default packages to include and exclude when making the image
+	// type.
+	BasePackages() ([]string, []string)
 
-	// Returns the build packages for a given output architecture
-	BuildPackages(outputArchitecture string) ([]string, error)
+	// Returns the build packages for the output type.
+	BuildPackages() []string
 
 	// Returns an osbuild manifest, containing the sources and pipeline necessary
-	// to generates an image in the given output format with all packages and
-	// customizations specified in the given blueprint.
-	Manifest(b *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, imageFormat string, size uint64) (*osbuild.Manifest, error)
-
-	// Returns a osbuild runner that can be used on this distro.
-	Runner() string
+	// to build an image, given output format with all packages and customizations
+	// specified in the given blueprint.
+	Manifest(b *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, size uint64) (*osbuild.Manifest, error)
 }
 
 type Registry struct {
-	distros map[common.Distribution]Distro
+	distros map[string]Distro
 }
 
 func NewRegistry(distros ...Distro) (*Registry, error) {
 	reg := &Registry{
-		distros: make(map[common.Distribution]Distro),
+		distros: make(map[string]Distro),
 	}
 	for _, distro := range distros {
-		distroTag := distro.Distribution()
-		if _, exists := reg.distros[distroTag]; exists {
+		name := distro.Name()
+		if _, exists := reg.distros[name]; exists {
 			return nil, fmt.Errorf("NewRegistry: passed two distros with the same name: %s", distro.Name())
 		}
-		reg.distros[distroTag] = distro
+		reg.distros[name] = distro
 	}
 	return reg, nil
 }
 
-// Create a new Registry containing all known distros.
-func NewDefaultRegistry() (*Registry, error) {
-	f30, err := fedora30.New()
-	if err != nil {
-		return nil, fmt.Errorf("error loading fedora30: %v", err)
-	}
-	f31, err := fedora31.New()
-	if err != nil {
-		return nil, fmt.Errorf("error loading fedora31: %v", err)
-	}
-	f32, err := fedora32.New()
-	if err != nil {
-		return nil, fmt.Errorf("error loading fedora32: %v", err)
-	}
-	el81, err := rhel81.New()
-	if err != nil {
-		return nil, fmt.Errorf("error loading rhel81: %v", err)
-	}
-	el82, err := rhel82.New()
-	if err != nil {
-		return nil, fmt.Errorf("error loading rhel82: %v", err)
-	}
-	return NewRegistry(f30, f31, f32, el81, el82)
-}
-
 func (r *Registry) GetDistro(name string) Distro {
-	distroTag, exists := common.DistributionFromString(name)
-	if !exists {
-		return nil
-	}
-	distro, ok := r.distros[distroTag]
+	distro, ok := r.distros[name]
 	if !ok {
 		return nil
 	}
@@ -116,7 +104,7 @@ func (r *Registry) GetDistro(name string) Distro {
 	return distro
 }
 
-// Returns the names of all distros in a Registry, sorted alphabetically.
+// List returns the names of all distros in a Registry, sorted alphabetically.
 func (r *Registry) List() []string {
 	list := []string{}
 	for _, distro := range r.distros {
diff --git a/internal/distro/fedora30/distro.go b/internal/distro/fedora30/distro.go
index 8809444a7..79c18953c 100644
--- a/internal/distro/fedora30/distro.go
+++ b/internal/distro/fedora30/distro.go
@@ -6,6 +6,7 @@ import (
 	"strconv"
 
 	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
 
 	"github.com/google/uuid"
@@ -15,12 +16,6 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
 
-type Fedora30 struct {
-	arches        map[string]arch
-	outputs       map[string]output
-	buildPackages []string
-}
-
 type arch struct {
 	Name               string
 	BootloaderPackages []string
@@ -44,7 +39,104 @@ type output struct {
 const Distro = common.Fedora30
 const ModulePlatformID = "platform:f30"
 
-func New() (*Fedora30, error) {
+type Fedora30 struct {
+	arches        map[string]arch
+	outputs       map[string]output
+	buildPackages []string
+}
+
+type Fedora30Arch struct {
+	name   string
+	distro *Fedora30
+	arch   *arch
+}
+
+type Fedora30ImageType struct {
+	name   string
+	arch   *Fedora30Arch
+	output *output
+}
+
+func (d *Fedora30) GetArch(arch string) (distro.Arch, error) {
+	a, exists := d.arches[arch]
+	if !exists {
+		return nil, errors.New("invalid architecture: " + arch)
+	}
+
+	return &Fedora30Arch{
+		name:   arch,
+		distro: d,
+		arch:   &a,
+	}, nil
+}
+
+func (a *Fedora30Arch) Name() string {
+	return a.name
+}
+
+func (a *Fedora30Arch) ListImageTypes() []string {
+	return a.distro.ListOutputFormats()
+}
+
+func (a *Fedora30Arch) GetImageType(imageType string) (distro.ImageType, error) {
+	t, exists := a.distro.outputs[imageType]
+	if !exists {
+		return nil, errors.New("invalid image type: " + imageType)
+	}
+
+	return &Fedora30ImageType{
+		name:   imageType,
+		arch:   a,
+		output: &t,
+	}, nil
+}
+
+func (t *Fedora30ImageType) Name() string {
+	return t.name
+}
+
+func (t *Fedora30ImageType) Filename() string {
+	return t.output.Name
+}
+
+func (t *Fedora30ImageType) MIMEType() string {
+	return t.output.MimeType
+}
+
+func (t *Fedora30ImageType) Size(size uint64) uint64 {
+	return t.arch.distro.GetSizeForOutputType(t.name, size)
+}
+
+func (t *Fedora30ImageType) BasePackages() ([]string, []string) {
+	packages := t.output.Packages
+	if t.output.Bootable {
+		packages = append(packages, t.arch.arch.BootloaderPackages...)
+	}
+
+	return packages, t.output.ExcludedPackages
+}
+
+func (t *Fedora30ImageType) BuildPackages() []string {
+	return append(t.arch.distro.buildPackages, t.arch.arch.BuildPackages...)
+}
+
+func (t *Fedora30ImageType) Manifest(c *blueprint.Customizations,
+	repos []rpmmd.RepoConfig,
+	packageSpecs,
+	buildPackageSpecs []rpmmd.PackageSpec,
+	size uint64) (*osbuild.Manifest, error) {
+	pipeline, err := t.arch.distro.pipeline(c, repos, packageSpecs, buildPackageSpecs, t.arch.name, t.name, size)
+	if err != nil {
+		return nil, err
+	}
+
+	return &osbuild.Manifest{
+		Sources:  *t.arch.distro.sources(append(packageSpecs, buildPackageSpecs...)),
+		Pipeline: *pipeline,
+	}, nil
+}
+
+func New() *Fedora30 {
 	const GigaByte = 1024 * 1024 * 1024
 
 	r := Fedora30{
@@ -286,7 +378,7 @@ func New() (*Fedora30, error) {
 		},
 	}
 
-	return &r, nil
+	return &r
 }
 
 func (r *Fedora30) Name() string {
diff --git a/internal/distro/fedora31/distro.go b/internal/distro/fedora31/distro.go
index 4a64ef55b..449bd03fd 100644
--- a/internal/distro/fedora31/distro.go
+++ b/internal/distro/fedora31/distro.go
@@ -6,6 +6,7 @@ import (
 	"strconv"
 
 	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
 
 	"github.com/google/uuid"
@@ -15,12 +16,6 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
 
-type Fedora31 struct {
-	arches        map[string]arch
-	outputs       map[string]output
-	buildPackages []string
-}
-
 type arch struct {
 	Name               string
 	BootloaderPackages []string
@@ -44,7 +39,104 @@ type output struct {
 const Distro = common.Fedora31
 const ModulePlatformID = "platform:f31"
 
-func New() (*Fedora31, error) {
+type Fedora31 struct {
+	arches        map[string]arch
+	outputs       map[string]output
+	buildPackages []string
+}
+
+type Fedora31Arch struct {
+	name   string
+	distro *Fedora31
+	arch   *arch
+}
+
+type Fedora31ImageType struct {
+	name   string
+	arch   *Fedora31Arch
+	output *output
+}
+
+func (d *Fedora31) GetArch(arch string) (distro.Arch, error) {
+	a, exists := d.arches[arch]
+	if !exists {
+		return nil, errors.New("invalid architecture: " + arch)
+	}
+
+	return &Fedora31Arch{
+		name:   arch,
+		distro: d,
+		arch:   &a,
+	}, nil
+}
+
+func (a *Fedora31Arch) Name() string {
+	return a.name
+}
+
+func (a *Fedora31Arch) ListImageTypes() []string {
+	return a.distro.ListOutputFormats()
+}
+
+func (a *Fedora31Arch) GetImageType(imageType string) (distro.ImageType, error) {
+	t, exists := a.distro.outputs[imageType]
+	if !exists {
+		return nil, errors.New("invalid image type: " + imageType)
+	}
+
+	return &Fedora31ImageType{
+		name:   imageType,
+		arch:   a,
+		output: &t,
+	}, nil
+}
+
+func (t *Fedora31ImageType) Name() string {
+	return t.name
+}
+
+func (t *Fedora31ImageType) Filename() string {
+	return t.output.Name
+}
+
+func (t *Fedora31ImageType) MIMEType() string {
+	return t.output.MimeType
+}
+
+func (t *Fedora31ImageType) Size(size uint64) uint64 {
+	return t.arch.distro.GetSizeForOutputType(t.name, size)
+}
+
+func (t *Fedora31ImageType) BasePackages() ([]string, []string) {
+	packages := t.output.Packages
+	if t.output.Bootable {
+		packages = append(packages, t.arch.arch.BootloaderPackages...)
+	}
+
+	return packages, t.output.ExcludedPackages
+}
+
+func (t *Fedora31ImageType) BuildPackages() []string {
+	return append(t.arch.distro.buildPackages, t.arch.arch.BuildPackages...)
+}
+
+func (t *Fedora31ImageType) Manifest(c *blueprint.Customizations,
+	repos []rpmmd.RepoConfig,
+	packageSpecs,
+	buildPackageSpecs []rpmmd.PackageSpec,
+	size uint64) (*osbuild.Manifest, error) {
+	pipeline, err := t.arch.distro.pipeline(c, repos, packageSpecs, buildPackageSpecs, t.arch.name, t.name, size)
+	if err != nil {
+		return nil, err
+	}
+
+	return &osbuild.Manifest{
+		Sources:  *t.arch.distro.sources(append(packageSpecs, buildPackageSpecs...)),
+		Pipeline: *pipeline,
+	}, nil
+}
+
+func New() *Fedora31 {
 	const GigaByte = 1024 * 1024 * 1024
 
 	r := Fedora31{
@@ -286,7 +378,7 @@ func New() (*Fedora31, error) {
 		},
 	}
 
-	return &r, nil
+	return &r
 }
 
 func (r *Fedora31) Name() string {
diff --git a/internal/distro/fedora32/distro.go b/internal/distro/fedora32/distro.go
index 9510cfa52..9bfe50429 100644
--- a/internal/distro/fedora32/distro.go
+++ b/internal/distro/fedora32/distro.go
@@ -6,6 +6,7 @@ import (
 	"strconv"
 
 	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
 
 	"github.com/google/uuid"
@@ -15,12 +16,6 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
 
-type Fedora32 struct {
-	arches        map[string]arch
-	outputs       map[string]output
-	buildPackages []string
-}
-
 type arch struct {
 	Name               string
 	BootloaderPackages []string
@@ -44,7 +39,104 @@ type output struct {
 const Distro = common.Fedora32
 const ModulePlatformID = "platform:f32"
 
-func New() (*Fedora32, error) {
+type Fedora32 struct {
+	arches        map[string]arch
+	outputs       map[string]output
+	buildPackages []string
+}
+
+type Fedora32Arch struct {
+	name   string
+	distro *Fedora32
+	arch   *arch
+}
+
+type Fedora32ImageType struct {
+	name   string
+	arch   *Fedora32Arch
+	output *output
+}
+
+func (d *Fedora32) GetArch(arch string) (distro.Arch, error) {
+	a, exists := d.arches[arch]
+	if !exists {
+		return nil, errors.New("invalid architecture: " + arch)
+	}
+
+	return &Fedora32Arch{
+		name:   arch,
+		distro: d,
+		arch:   &a,
+	}, nil
+}
+
+func (a *Fedora32Arch) Name() string {
+	return a.name
+}
+
+func (a *Fedora32Arch) ListImageTypes() []string {
+	return a.distro.ListOutputFormats()
+}
+
+func (a *Fedora32Arch) GetImageType(imageType string) (distro.ImageType, error) {
+	t, exists := a.distro.outputs[imageType]
+	if !exists {
+		return nil, errors.New("invalid image type: " + imageType)
+	}
+
+	return &Fedora32ImageType{
+		name:   imageType,
+		arch:   a,
+		output: &t,
+	}, nil
+}
+
+func (t *Fedora32ImageType) Name() string {
+	return t.name
+}
+
+func (t *Fedora32ImageType) Filename() string {
+	return t.output.Name
+}
+
+func (t *Fedora32ImageType) MIMEType() string {
+	return t.output.MimeType
+}
+
+func (t *Fedora32ImageType) Size(size uint64) uint64 {
+	return t.arch.distro.GetSizeForOutputType(t.name, size)
+}
+
+func (t *Fedora32ImageType) BasePackages() ([]string, []string) {
+	packages := t.output.Packages
+	if t.output.Bootable {
+		packages = append(packages, t.arch.arch.BootloaderPackages...)
+	}
+
+	return packages, t.output.ExcludedPackages
+}
+
+func (t *Fedora32ImageType) BuildPackages() []string {
+	return append(t.arch.distro.buildPackages, t.arch.arch.BuildPackages...)
+}
+
+func (t *Fedora32ImageType) Manifest(c *blueprint.Customizations,
+	repos []rpmmd.RepoConfig,
+	packageSpecs,
+	buildPackageSpecs []rpmmd.PackageSpec,
+	size uint64) (*osbuild.Manifest, error) {
+	pipeline, err := t.arch.distro.pipeline(c, repos, packageSpecs, buildPackageSpecs, t.arch.name, t.name, size)
+	if err != nil {
+		return nil, err
+	}
+
+	return &osbuild.Manifest{
+		Sources:  *t.arch.distro.sources(append(packageSpecs, buildPackageSpecs...)),
+		Pipeline: *pipeline,
+	}, nil
+}
+
+func New() *Fedora32 {
 	const GigaByte = 1024 * 1024 * 1024
 
 	r := Fedora32{
@@ -286,7 +378,7 @@ func New() (*Fedora32, error) {
 		},
 	}
 
-	return &r, nil
+	return &r
 }
 
 func (r *Fedora32) Name() string {
diff --git a/internal/distro/rhel81/distro.go b/internal/distro/rhel81/distro.go
index c8aacfe08..ed4287b13 100644
--- a/internal/distro/rhel81/distro.go
+++ b/internal/distro/rhel81/distro.go
@@ -6,6 +6,7 @@ import (
 	"strconv"
 
 	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
 
 	"github.com/google/uuid"
@@ -15,12 +16,6 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
 
-type RHEL81 struct {
-	arches        map[string]arch
-	outputs       map[string]output
-	buildPackages []string
-}
-
 type arch struct {
 	Name               string
 	BootloaderPackages []string
@@ -45,7 +40,104 @@ type output struct {
 const Distro = common.RHEL81
 const ModulePlatformID = "platform:el8"
 
-func New() (*RHEL81, error) {
+type RHEL81 struct {
+	arches        map[string]arch
+	outputs       map[string]output
+	buildPackages []string
+}
+
+type RHEL81Arch struct {
+	name   string
+	distro *RHEL81
+	arch   *arch
+}
+
+type RHEL81ImageType struct {
+	name   string
+	arch   *RHEL81Arch
+	output *output
+}
+
+func (d *RHEL81) GetArch(arch string) (distro.Arch, error) {
+	a, exists := d.arches[arch]
+	if !exists {
+		return nil, errors.New("invalid architecture: " + arch)
+	}
+
+	return &RHEL81Arch{
+		name:   arch,
+		distro: d,
+		arch:   &a,
+	}, nil
+}
+
+func (a *RHEL81Arch) Name() string {
+	return a.name
+}
+
+func (a *RHEL81Arch) ListImageTypes() []string {
+	return a.distro.ListOutputFormats()
+}
+
+func (a *RHEL81Arch) GetImageType(imageType string) (distro.ImageType, error) {
+	t, exists := a.distro.outputs[imageType]
+	if !exists {
+		return nil, errors.New("invalid image type: " + imageType)
+	}
+
+	return &RHEL81ImageType{
+		name:   imageType,
+		arch:   a,
+		output: &t,
+	}, nil
+}
+
+func (t *RHEL81ImageType) Name() string {
+	return t.name
+}
+
+func (t *RHEL81ImageType) Filename() string {
+	return t.output.Name
+}
+
+func (t *RHEL81ImageType) MIMEType() string {
+	return t.output.MimeType
+}
+
+func (t *RHEL81ImageType) Size(size uint64) uint64 {
+	return t.arch.distro.GetSizeForOutputType(t.name, size)
+}
+
+func (t *RHEL81ImageType) BasePackages() ([]string, []string) {
+	packages := t.output.Packages
+	if t.output.Bootable {
+		packages = append(packages, t.arch.arch.BootloaderPackages...)
+	}
+
+	return packages, t.output.ExcludedPackages
+}
+
+func (t *RHEL81ImageType) BuildPackages() []string {
+	return append(t.arch.distro.buildPackages, t.arch.arch.BuildPackages...)
+}
+
+func (t *RHEL81ImageType) Manifest(c *blueprint.Customizations,
+	repos []rpmmd.RepoConfig,
+	packageSpecs,
+	buildPackageSpecs []rpmmd.PackageSpec,
+	size uint64) (*osbuild.Manifest, error) {
+	pipeline, err := t.arch.distro.pipeline(c, repos, packageSpecs, buildPackageSpecs, t.arch.name, t.name, size)
+	if err != nil {
+		return nil, err
+	}
+
+	return &osbuild.Manifest{
+		Sources:  *t.arch.distro.sources(append(packageSpecs, buildPackageSpecs...)),
+		Pipeline: *pipeline,
+	}, nil
+}
+
+func New() *RHEL81 {
 	const GigaByte = 1024 * 1024 * 1024
 
 	r := RHEL81{
@@ -425,7 +517,7 @@ func New() (*RHEL81, error) {
 		},
 	}
 
-	return &r, nil
+	return &r
 }
 
 func (r *RHEL81) Name() string {
diff --git a/internal/distro/rhel82/distro.go b/internal/distro/rhel82/distro.go
index dc2a53fdb..e97e7fefe 100644
--- a/internal/distro/rhel82/distro.go
+++ b/internal/distro/rhel82/distro.go
@@ -6,6 +6,7 @@ import (
 	"strconv"
 
 	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
 
 	"github.com/google/uuid"
@@ -15,12 +16,6 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
 
-type RHEL82 struct {
-	arches        map[string]arch
-	outputs       map[string]output
-	buildPackages []string
-}
-
 type arch struct {
 	Name               string
 	BootloaderPackages []string
@@ -45,7 +40,104 @@ type output struct {
 const Distro = common.RHEL82
 const ModulePlatformID = "platform:el8"
 
-func New() (*RHEL82, error) {
+type RHEL82 struct {
+	arches        map[string]arch
+	outputs       map[string]output
+	buildPackages []string
+}
+
+type RHEL82Arch struct {
+	name   string
+	distro *RHEL82
+	arch   *arch
+}
+
+type RHEL82ImageType struct {
+	name   string
+	arch   *RHEL82Arch
+	output *output
+}
+
+func (d *RHEL82) GetArch(arch string) (distro.Arch, error) {
+	a, exists := d.arches[arch]
+	if !exists {
+		return nil, errors.New("invalid architecture: " + arch)
+	}
+
+	return &RHEL82Arch{
+		name:   arch,
+		distro: d,
+		arch:   &a,
+	}, nil
+}
+
+func (a *RHEL82Arch) Name() string {
+	return a.name
+}
+
+func (a *RHEL82Arch) ListImageTypes() []string {
+	return a.distro.ListOutputFormats()
+}
+
+func (a *RHEL82Arch) GetImageType(imageType string) (distro.ImageType, error) {
+	t, exists := a.distro.outputs[imageType]
+	if !exists {
+		return nil, errors.New("invalid image type: " + imageType)
+	}
+
+	return &RHEL82ImageType{
+		name:   imageType,
+		arch:   a,
+		output: &t,
+	}, nil
+}
+
+func (t *RHEL82ImageType) Name() string {
+	return t.name
+}
+
+func (t *RHEL82ImageType) Filename() string {
+	return t.output.Name
+}
+
+func (t *RHEL82ImageType) MIMEType() string {
+	return t.output.MimeType
+}
+
+func (t *RHEL82ImageType) Size(size uint64) uint64 {
+	return t.arch.distro.GetSizeForOutputType(t.name, size)
+}
+
+func (t *RHEL82ImageType) BasePackages() ([]string, []string) {
+	packages := t.output.Packages
+	if t.output.Bootable {
+		packages = append(packages, t.arch.arch.BootloaderPackages...)
+	}
+
+	return packages, t.output.ExcludedPackages
+}
+
+func (t *RHEL82ImageType) BuildPackages() []string {
+	return append(t.arch.distro.buildPackages, t.arch.arch.BuildPackages...)
+}
+
+func (t *RHEL82ImageType) Manifest(c *blueprint.Customizations,
+	repos []rpmmd.RepoConfig,
+	packageSpecs,
+	buildPackageSpecs []rpmmd.PackageSpec,
+	size uint64) (*osbuild.Manifest, error) {
+	pipeline, err := t.arch.distro.pipeline(c, repos, packageSpecs, buildPackageSpecs, t.arch.name, t.name, size)
+	if err != nil {
+		return nil, err
+	}
+
+	return &osbuild.Manifest{
+		Sources:  *t.arch.distro.sources(append(packageSpecs, buildPackageSpecs...)),
+		Pipeline: *pipeline,
+	}, nil
+}
+
+func New() *RHEL82 {
 	const GigaByte = 1024 * 1024 * 1024
 
 	r := RHEL82{
@@ -425,7 +517,7 @@ func New() (*RHEL82, error) {
 		},
 	}
 
-	return &r, nil
+	return &r
 }
 
 func (r *RHEL82) Name() string {
diff --git a/internal/rcm/api.go b/internal/rcm/api.go
index a02bc48a8..106811468 100644
--- a/internal/rcm/api.go
+++ b/internal/rcm/api.go
@@ -10,7 +10,6 @@ import (
 	"net/http"
 	"strings"
 
-	"github.com/osbuild/osbuild-composer/internal/common"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 
 	"github.com/google/uuid"
@@ -84,22 +83,14 @@ func notFoundHandler(writer http.ResponseWriter, request *http.Request) {
 
 // Depsolves packages and build packages for building an image for a given
 // distro, in the given architecture
-func depsolve(rpmmd rpmmd.RPMMD, distro distro.Distro, repos []rpmmd.RepoConfig, imageType, arch string) ([]rpmmd.PackageSpec, []rpmmd.PackageSpec, error) {
-	specs, excludeSpecs, err := distro.BasePackages(imageType, arch)
-	if err != nil {
-		return nil, nil, fmt.Errorf("Distro.BasePackages: %v", err)
-	}
-
+func depsolve(rpmmd rpmmd.RPMMD, distro distro.Distro, imageType distro.ImageType, repos []rpmmd.RepoConfig) ([]rpmmd.PackageSpec, []rpmmd.PackageSpec, error) {
+	specs, excludeSpecs := imageType.BasePackages()
 	packages, _, err := rpmmd.Depsolve(specs, excludeSpecs, repos, distro.ModulePlatformID())
 	if err != nil {
 		return nil, nil, fmt.Errorf("RPMMD.Depsolve: %v", err)
 	}
 
-	specs, err = distro.BuildPackages(arch)
-	if err != nil {
-		return nil, nil, fmt.Errorf("Distro.BuildPackages: %v", err)
-	}
-
+	specs = imageType.BuildPackages()
 	buildPackages, _, err := rpmmd.Depsolve(specs, nil, repos, distro.ModulePlatformID())
 	if err != nil {
 		return nil, nil, fmt.Errorf("RPMMD.Depsolve: %v", err)
@@ -122,10 +113,10 @@ func (api *API) submit(writer http.ResponseWriter, request *http.Request, _ http
 
 	// JSON structure expected from the client
 	var composeRequest struct {
-		Distribution  string                `json:"distribution"`
-		ImageTypes    []common.ImageType    `json:"image_types"`
-		Architectures []common.Architecture `json:"architectures"`
-		Repositories  []Repository          `json:"repositories"`
+		Distribution  string       `json:"distribution"`
+		ImageTypes    []string     `json:"image_types"`
+		Architectures []string     `json:"architectures"`
+		Repositories  []Repository `json:"repositories"`
 	}
 	// JSON structure with error message
 	var errorReason struct {
@@ -135,7 +126,7 @@ func (api *API) submit(writer http.ResponseWriter, request *http.Request, _ http
 	decoder := json.NewDecoder(request.Body)
 	decoder.DisallowUnknownFields()
 	err := decoder.Decode(&composeRequest)
-	if err != nil || len(composeRequest.Architectures) == 0 || len(composeRequest.Repositories) == 0 || len(composeRequest.ImageTypes) == 0 {
+	if err != nil || len(composeRequest.Architectures) != 1 || len(composeRequest.ImageTypes) != 1 || len(composeRequest.Repositories) == 0 {
 		writer.WriteHeader(http.StatusBadRequest)
 		errors := []string{}
 		if err != nil {
@@ -163,17 +154,6 @@ func (api *API) submit(writer http.ResponseWriter, request *http.Request, _ http
 		return
 	}
 
-	// Create repo configurations from the URLs in the request. Use made up repo id and name, because
-	// we don't want to bother clients of this API with details like this
-	repoConfigs := []rpmmd.RepoConfig{}
-	for n, repo := range composeRequest.Repositories {
-		repoConfigs = append(repoConfigs, rpmmd.RepoConfig{
-			Id:        fmt.Sprintf("repo-%d", n),
-			BaseURL:   repo.URL,
-			IgnoreSSL: false,
-		})
-	}
-
 	distro := api.distros.GetDistro(composeRequest.Distribution)
 	if distro == nil {
 		writer.WriteHeader(http.StatusBadRequest)
@@ -181,31 +161,54 @@ func (api *API) submit(writer http.ResponseWriter, request *http.Request, _ http
 		if err != nil {
 			panic("Failed to write response")
 		}
+		return
+	}
+
+	arch, err := distro.GetArch(composeRequest.Architectures[0])
+	if err != nil {
+		writer.WriteHeader(http.StatusBadRequest)
+		_, err := writer.Write([]byte("unknown architecture for distro"))
+		if err != nil {
+			panic("Failed to write response")
+		}
+		return
 	}
 
-	arch, ok := composeRequest.Architectures[0].ToString()
-	if !ok {
-		panic("architecture was validated before")
+	imageType, err := arch.GetImageType(composeRequest.ImageTypes[0])
+	if err != nil {
+		writer.WriteHeader(http.StatusBadRequest)
+		_, err := writer.Write([]byte("unknown image type for distro and architecture"))
+		if err != nil {
+			panic("Failed to write response")
+		}
+		return
 	}
 
-	imageType, ok := composeRequest.ImageTypes[0].ToString()
-	if !ok {
-		panic("image type was validated before")
+	// Create repo configurations from the URLs in the request. Use made up repo id and name, because
+	// we don't want to bother clients of this API with details like this
+	repoConfigs := []rpmmd.RepoConfig{}
+	for n, repo := range composeRequest.Repositories {
+		repoConfigs = append(repoConfigs, rpmmd.RepoConfig{
+			Id:        fmt.Sprintf("repo-%d", n),
+			BaseURL:   repo.URL,
+			IgnoreSSL: false,
+		})
 	}
 
-	packages, buildPackages, err := depsolve(api.rpmMetadata, distro, repoConfigs, imageType, arch)
+	packages, buildPackages, err := depsolve(api.rpmMetadata, distro, imageType, repoConfigs)
 	if err != nil {
 		writer.WriteHeader(http.StatusBadRequest)
 		_, err := writer.Write([]byte(err.Error()))
 		if err != nil {
 			panic("Failed to write response")
 		}
+		return
 	}
 
 	// Push the requested compose to the store
 	composeUUID := uuid.New()
 	// nil is used as an upload target, because LocalTarget is already used in the PushCompose function
-	err = api.store.PushCompose(distro, composeUUID, &blueprint.Blueprint{}, repoConfigs, packages, buildPackages, arch, imageType, 0, nil)
+	err = api.store.PushCompose(distro, arch, imageType, composeUUID, &blueprint.Blueprint{}, repoConfigs, packages, buildPackages, 0, nil)
 	if err != nil {
 		if api.logger != nil {
 			api.logger.Println("RCM API failed to push compose:", err)
diff --git a/internal/store/store.go b/internal/store/store.go
index 98539575d..22a713659 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -560,11 +560,11 @@ func (s *Store) getImageBuildDirectory(composeID uuid.UUID, imageBuildID int) st
 	return fmt.Sprintf("%s/%d", s.getComposeDirectory(composeID), imageBuildID)
 }
 
-func (s *Store) PushCompose(distro distro.Distro, composeID uuid.UUID, bp *blueprint.Blueprint, repos []rpmmd.RepoConfig, packages, buildPackages []rpmmd.PackageSpec, arch, composeType string, size uint64, uploadTarget *target.Target) error {
+func (s *Store) PushCompose(distro distro.Distro, arch distro.Arch, imageType distro.ImageType, composeID uuid.UUID, bp *blueprint.Blueprint, repos []rpmmd.RepoConfig, packages, buildPackages []rpmmd.PackageSpec, size uint64, uploadTarget *target.Target) error {
 	targets := []*target.Target{}
 
 	// Compatibility layer for image types in Weldr API v0
-	imageType, exists := common.ImageTypeFromCompatString(composeType)
+	imageTypeCommon, exists := common.ImageTypeFromCompatString(imageType.Name())
 	if !exists {
 		panic("fatal error, compose type does not exist")
 	}
@@ -577,20 +577,13 @@ func (s *Store) PushCompose(distro distro.Distro, composeID uuid.UUID, bp *bluep
 			return fmt.Errorf("cannot create output directory for job %v: %#v", composeID, err)
 		}
 
-		filename, _, err := distro.FilenameFromType(composeType)
-		if err != nil {
-			return fmt.Errorf("cannot query filename from image type %s: %#v", composeType, err)
-		}
-
 		targets = append(targets, target.NewLocalTarget(
 			&target.LocalTargetOptions{
-				Filename: filename,
+				Filename: imageType.Filename(),
 			},
 		))
 	}
 
-	size = distro.GetSizeForOutputType(composeType, size)
-
 	if uploadTarget != nil {
 		targets = append(targets, uploadTarget)
 	}
@@ -600,7 +593,7 @@ func (s *Store) PushCompose(distro distro.Distro, composeID uuid.UUID, bp *bluep
 		allRepos = append(allRepos, source.RepoConfig())
 	}
 
-	manifestStruct, err := distro.Manifest(bp.Customizations, allRepos, packages, buildPackages, arch, composeType, size)
+	manifestStruct, err := imageType.Manifest(bp.Customizations, allRepos, packages, buildPackages, imageType.Size(size))
 	if err != nil {
 		return err
 	}
@@ -612,7 +605,7 @@ func (s *Store) PushCompose(distro distro.Distro, composeID uuid.UUID, bp *bluep
 				{
 					QueueStatus: common.IBWaiting,
 					Manifest:    manifestStruct,
-					ImageType:   imageType,
+					ImageType:   imageTypeCommon,
 					Targets:     targets,
 					JobCreated:  time.Now(),
 					Size:        size,
@@ -626,7 +619,7 @@ func (s *Store) PushCompose(distro distro.Distro, composeID uuid.UUID, bp *bluep
 		ImageBuildID: 0,
 		Manifest:     manifestStruct,
 		Targets:      targets,
-		ImageType:    composeType,
+		ImageType:    imageType.Name(),
 	}
 
 	return nil
diff --git a/internal/weldr/api.go b/internal/weldr/api.go
index 9a317a03b..becdba2af 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -31,7 +31,7 @@ type API struct {
 	store *store.Store
 
 	rpmmd  rpmmd.RPMMD
-	arch   string
+	arch   distro.Arch
 	distro distro.Distro
 	repos  []rpmmd.RepoConfig
 
@@ -39,7 +39,7 @@ type API struct {
 	router *httprouter.Router
 }
 
-func New(rpmmd rpmmd.RPMMD, arch string, distro distro.Distro, repos []rpmmd.RepoConfig, logger *log.Logger, store *store.Store) *API {
+func New(rpmmd rpmmd.RPMMD, arch distro.Arch, distro distro.Distro, repos []rpmmd.RepoConfig, logger *log.Logger, store *store.Store) *API {
 	// This needs to be shared with the worker API so that they can communicate with each other
 	// builds := make(chan queue.Build, 200)
 	api := &API{
@@ -354,7 +354,16 @@ func (api *API) sourceNewHandler(writer http.ResponseWriter, request *http.Reque
 		return
 	}
 
-	var source store.SourceConfig
+	if request.ContentLength == 0 {
+		errors := responseError{
+			ID:  "ProjectsError",
+			Msg: "Missing source",
+		}
+		statusResponseError(writer, http.StatusBadRequest, errors)
+		return
+	}
+
+	var source SourceConfigV0
 	var err error
 	if contentType[0] == "application/json" {
 		err = json.NewDecoder(request.Body).Decode(&source)
@@ -366,15 +375,14 @@ func (api *API) sourceNewHandler(writer http.ResponseWriter, request *http.Reque
 
 	if err != nil {
 		errors := responseError{
-			Code: http.StatusBadRequest,
-			ID:   "HTTPError",
-			Msg:  "Bad Request",
+			ID:  "ProjectsError",
+			Msg: "Problem parsing POST body: " + err.Error(),
 		}
 		statusResponseError(writer, http.StatusBadRequest, errors)
 		return
 	}
 
-	api.store.PushSource(source)
+	api.store.PushSource(source.SourceConfig())
 
 	statusResponseOK(writer)
 }
@@ -845,7 +853,7 @@ func (api *API) blueprintsDepsolveHandler(writer http.ResponseWriter, request *h
 			continue
 		}
 
-		dependencies, _, err := api.depsolveBlueprint(blueprint, "", api.arch)
+		dependencies, _, err := api.depsolveBlueprint(blueprint, nil)
 
 		if err != nil {
 			errors := responseError{
@@ -941,7 +949,7 @@ func (api *API) blueprintsFreezeHandler(writer http.ResponseWriter, request *htt
 			break
 		}
 
-		dependencies, _, err := api.depsolveBlueprint(&blueprint, "", api.arch)
+		dependencies, _, err := api.depsolveBlueprint(&blueprint, nil)
 		if err != nil {
 			rerr := responseError{
 				ID:  "BlueprintsError",
@@ -1389,14 +1397,19 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 		return
 	}
 
-	reply := ComposeReply{
-		BuildID: uuid.New(),
-		Status:  true,
+	imageType, err := api.arch.GetImageType(cr.ComposeType)
+	if err != nil {
+		errors := responseError{
+			ID:  "UnknownComposeType",
+			Msg: fmt.Sprintf("Unknown compose type for architecture: %s", cr.ComposeType),
+		}
+		statusResponseError(writer, http.StatusBadRequest, errors)
+		return
 	}
 
 	var uploadTarget *target.Target
 	if isRequestVersionAtLeast(params, 1) && cr.Upload != nil {
-		uploadTarget, err = uploadRequestToTarget(*cr.Upload, api.distro, cr.ComposeType)
+		uploadTarget, err = uploadRequestToTarget(*cr.Upload, imageType)
 		if err != nil {
 			errors := responseError{
 				ID:  "UploadError",
@@ -1409,31 +1422,7 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 	}
 
 	bp := api.store.GetBlueprintCommitted(cr.BlueprintName)
-	if bp != nil {
-		packages, buildPackages, err := api.depsolveBlueprint(bp, cr.ComposeType, api.arch)
-		if err != nil {
-			errors := responseError{
-				ID:  "DepsolveError",
-				Msg: err.Error(),
-			}
-			statusResponseError(writer, http.StatusInternalServerError, errors)
-			return
-		}
-
-		err = api.store.PushCompose(api.distro, reply.BuildID, bp, api.repos, packages, buildPackages, api.arch, cr.ComposeType, cr.Size, uploadTarget)
-
-		// TODO: we should probably do some kind of blueprint validation in future
-		// for now, let's just 500 and bail out
-		if err != nil {
-			log.Println("error when pushing new compose: ", err.Error())
-			errors := responseError{
-				ID:  "ComposePushErrored",
-				Msg: err.Error(),
-			}
-			statusResponseError(writer, http.StatusInternalServerError, errors)
-			return
-		}
-	} else {
+	if bp == nil {
 		errors := responseError{
 			ID:  "UnknownBlueprint",
 			Msg: fmt.Sprintf("Unknown blueprint name: %s", cr.BlueprintName),
@@ -1442,6 +1431,35 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 		return
 	}
 
+	packages, buildPackages, err := api.depsolveBlueprint(bp, imageType)
+	if err != nil {
+		errors := responseError{
+			ID:  "DepsolveError",
+			Msg: err.Error(),
+		}
+		statusResponseError(writer, http.StatusInternalServerError, errors)
+		return
+	}
+
+	reply := ComposeReply{
+		BuildID: uuid.New(),
+		Status:  true,
+	}
+
+	err = api.store.PushCompose(api.distro, api.arch, imageType, reply.BuildID, bp, api.repos, packages, buildPackages, cr.Size, uploadTarget)
+
+	// TODO: we should probably do some kind of blueprint validation in future
+	// for now, let's just 500 and bail out
+	if err != nil {
+		log.Println("error when pushing new compose: ", err.Error())
+		errors := responseError{
+			ID:  "ComposePushErrored",
+			Msg: err.Error(),
+		}
+		statusResponseError(writer, http.StatusInternalServerError, errors)
+		return
+	}
+
 	err = json.NewEncoder(writer).Encode(reply)
 	common.PanicOnError(err)
 }
@@ -1522,7 +1540,7 @@ func (api *API) composeTypesHandler(writer http.ResponseWriter, request *http.Re
 		Types []composeType `json:"types"`
 	}
 
-	for _, format := range api.distro.ListOutputFormats() {
+	for _, format := range api.arch.ListImageTypes() {
 		reply.Types = append(reply.Types, composeType{format, true})
 	}
 
@@ -1969,7 +1987,7 @@ func getPkgNameGlob(pkg blueprint.Package) string {
 	return pkg.Name
 }
 
-func (api *API) depsolveBlueprint(bp *blueprint.Blueprint, outputType, arch string) ([]rpmmd.PackageSpec, []rpmmd.PackageSpec, error) {
+func (api *API) depsolveBlueprint(bp *blueprint.Blueprint, imageType distro.ImageType) ([]rpmmd.PackageSpec, []rpmmd.PackageSpec, error) {
 	repos := append([]rpmmd.RepoConfig{}, api.repos...)
 	for _, source := range api.store.GetAllSources() {
 		repos = append(repos, source.RepoConfig())
@@ -1982,13 +2000,10 @@ func (api *API) depsolveBlueprint(bp *blueprint.Blueprint, outputType, arch stri
 		specs = append(specs, getPkgNameGlob(mod))
 	}
 	excludeSpecs := []string{}
-	if outputType != "" {
+	if imageType != nil {
 		// When the output type is known, include the base packages in the depsolve
 		// transaction.
-		packages, excludePackages, err := api.distro.BasePackages(outputType, arch)
-		if err != nil {
-			return nil, nil, err
-		}
+		packages, excludePackages := imageType.BasePackages()
 		specs = append(specs, packages...)
 		excludeSpecs = append(excludePackages, excludeSpecs...)
 	}
@@ -1999,11 +2014,8 @@ func (api *API) depsolveBlueprint(bp *blueprint.Blueprint, outputType, arch stri
 	}
 
 	buildPackages := []rpmmd.PackageSpec{}
-	if outputType != "" {
-		buildSpecs, err := api.distro.BuildPackages(arch)
-		if err != nil {
-			return nil, nil, err
-		}
+	if imageType != nil {
+		buildSpecs := imageType.BuildPackages()
 		buildPackages, _, err = api.rpmmd.Depsolve(buildSpecs, nil, repos, api.distro.ModulePlatformID())
 		if err != nil {
 			return nil, nil, err
diff --git a/internal/client/json.go b/internal/weldr/json.go
similarity index 62%
rename from internal/client/json.go
rename to internal/weldr/json.go
index 4cf955342..db11cc995 100644
--- a/internal/client/json.go
+++ b/internal/weldr/json.go
@@ -1,10 +1,11 @@
-// Package client - json contains Exported API response structures
+// Package weldr - json contains Exported API request/response structures
 // Copyright (C) 2020 by Red Hat, Inc.
-package client
+package weldr
 
 import (
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
+	"github.com/osbuild/osbuild-composer/internal/store"
 )
 
 // StatusV0 is the response to /api/status from a v0+ server
@@ -75,3 +76,48 @@ type BlueprintsFreezeV0 struct {
 type blueprintFrozen struct {
 	Blueprint blueprint.Blueprint `json:"blueprint"`
 }
+
+// SourceListV0 is the response to /source/list request
+type SourceListV0 struct {
+	Sources []string `json:"sources"`
+}
+
+// SourceInfoV0 is the response to a /source/info request
+type SourceInfoV0 struct {
+	Sources map[string]SourceConfigV0 `json:"sources"`
+	Errors  []ResponseError           `json:"errors"`
+}
+
+// SourceConfig returns a SourceConfig struct populated with the supported variables
+func (s *SourceInfoV0) SourceConfig(sourceName string) (ssc store.SourceConfig, ok bool) {
+	si, ok := s.Sources[sourceName]
+	if !ok {
+		return ssc, false
+	}
+
+	return si.SourceConfig(), true
+}
+
+// SourceConfigV0 holds the source repository information
+type SourceConfigV0 struct {
+	Name     string   `json:"name"`
+	Type     string   `json:"type"`
+	URL      string   `json:"url"`
+	CheckGPG bool     `json:"check_gpg"`
+	CheckSSL bool     `json:"check_ssl"`
+	System   bool     `json:"system"`
+	Proxy    string   `json:"proxy"`
+	GPGUrls  []string `json:"gpgkey_urls"`
+}
+
+// SourceConfig returns a SourceConfig struct populated with the supported variables
+// The store does not support proxy and gpgkey_urls
+func (s *SourceConfigV0) SourceConfig() (ssc store.SourceConfig) {
+	ssc.Name = s.Name
+	ssc.Type = s.Type
+	ssc.URL = s.URL
+	ssc.CheckGPG = s.CheckGPG
+	ssc.CheckSSL = s.CheckSSL
+
+	return ssc
+}
diff --git a/internal/weldr/upload.go b/internal/weldr/upload.go
index 5e9029d6b..8e2d7356b 100644
--- a/internal/weldr/upload.go
+++ b/internal/weldr/upload.go
@@ -118,14 +118,9 @@ func targetsToUploadResponses(targets []*target.Target) []uploadResponse {
 	return uploads
 }
 
-func uploadRequestToTarget(u uploadRequest, d distro.Distro, imageType string) (*target.Target, error) {
+func uploadRequestToTarget(u uploadRequest, imageType distro.ImageType) (*target.Target, error) {
 	var t target.Target
 
-	filename, _, err := d.FilenameFromType(imageType)
-	if err != nil {
-		return nil, err
-	}
-
 	t.Uuid = uuid.New()
 	t.ImageName = u.ImageName
 	t.Status = common.IBWaiting
@@ -135,7 +130,7 @@ func uploadRequestToTarget(u uploadRequest, d distro.Distro, imageType string) (
 	case *awsUploadSettings:
 		t.Name = "org.osbuild.aws"
 		t.Options = &target.AWSTargetOptions{
-			Filename:        filename,
+			Filename:        imageType.Filename(),
 			Region:          options.Region,
 			AccessKeyID:     options.AccessKeyID,
 			SecretAccessKey: options.SecretAccessKey,
@@ -145,7 +140,7 @@ func uploadRequestToTarget(u uploadRequest, d distro.Distro, imageType string) (
 	case *azureUploadSettings:
 		t.Name = "org.osbuild.azure"
 		t.Options = &target.AzureTargetOptions{
-			Filename:  filename,
+			Filename:  imageType.Filename(),
 			Account:   options.Account,
 			AccessKey: options.AccessKey,
 			Container: options.Container,
diff --git a/internal/weldrcheck/utils.go b/internal/weldrcheck/utils.go
index 9fec45263..a5a51e6b0 100644
--- a/internal/weldrcheck/utils.go
+++ b/internal/weldrcheck/utils.go
@@ -21,6 +21,7 @@ import (
 type TestState struct {
 	socket     *http.Client
 	apiVersion int
+	repoDir    string
 }
 
 // isStringInSlice returns true if the string is present, false if not
@@ -34,7 +35,7 @@ func isStringInSlice(slice []string, s string) bool {
 	return false
 }
 
-func setupTestState(socketPath string, timeout time.Duration) (*TestState, error) {
+func setUpTestState(socketPath string, timeout time.Duration) (*TestState, error) {
 	var state TestState
 	state.socket = &http.Client{
 		// TODO This may be too short/simple for downloading images


Test patch:
diff --git a/cmd/osbuild-dnf-json-tests/main_test.go b/cmd/osbuild-dnf-json-tests/main_test.go
index 44624825f..13595af35 100644
--- a/cmd/osbuild-dnf-json-tests/main_test.go
+++ b/cmd/osbuild-dnf-json-tests/main_test.go
@@ -6,40 +6,18 @@ package main
 
 import (
 	"fmt"
-	"github.com/osbuild/osbuild-composer/internal/rpmmd"
-	"io/ioutil"
-	"os"
-	"os/exec"
-	"testing"
 	"github.com/stretchr/testify/assert"
 	"path"
-)
-
-func setUpTemporaryRepository() (string, error) {
-	dir, err := ioutil.TempDir("/tmp", "osbuild-composer-test-")
-	if err != nil {
-		return "", err
-	}
-	cmd := exec.Command("createrepo_c", path.Join(dir))
-	err = cmd.Start()
-	if err != nil {
-		return "", err
-	}
-	err = cmd.Wait()
-	if err != nil {
-		return "", err
-	}
-	return dir, nil
-}
+	"testing"
 
-func tearDownTemporaryRepository(dir string) error {
-	return os.RemoveAll(dir)
-}
+	"github.com/osbuild/osbuild-composer/internal/rpmmd"
+	"github.com/osbuild/osbuild-composer/internal/test"
+)
 
 func TestFetchChecksum(t *testing.T) {
-	dir, err := setUpTemporaryRepository()
+	dir, err := test.SetUpTemporaryRepository()
 	defer func(dir string) {
-		err := tearDownTemporaryRepository(dir)
+		err := test.TearDownTemporaryRepository(dir)
 		assert.Nil(t, err, "Failed to clean up temporary repository.")
 	}(dir)
 	assert.Nilf(t, err, "Failed to set up temporary repository: %v", err)
diff --git a/cmd/osbuild-tests/main_test.go b/cmd/osbuild-tests/main_test.go
index 056df9b38..a3b5ed82d 100644
--- a/cmd/osbuild-tests/main_test.go
+++ b/cmd/osbuild-tests/main_test.go
@@ -18,7 +18,7 @@ import (
 	"testing"
 )
 
-func TestComposeIntegration(t *testing.T) {
+func TestEverything(t *testing.T) {
 	// Smoke tests, until functionality tested fully
 	// that the calls succeed and return valid output
 	runComposerCLI(t, false, "compose", "types")
@@ -55,17 +55,36 @@ func TestComposeIntegration(t *testing.T) {
 	runComposerCLI(t, false, "projects", "info", "filesystem")
 	runComposerCLI(t, false, "projects", "info", "filesystem", "kernel")
 	runComposerCLI(t, false, "sources", "list")
-	// runCommand(false, "sources", "info", "fedora")
-	// runCommand(false, "sources", "info", "fedora", "fedora-updates")
-	// runCommand(false, "sources", "add" SOURCES.TOML)
-	// runCommand(false, "sources", "change" SOURCES.TOML)
-	// runCommand(false, "sources", "delete" SOURCES.TOML)
 	runComposerCLI(t, false, "status", "show")
 
 	// Full integration tests
 	testCompose(t, "ami")
 }
 
+func TestSources(t *testing.T) {
+	sources_toml, err := ioutil.TempFile("", "SOURCES-*.TOML")
+	require.NoErrorf(t, err, "Could not create temporary file: %v", err)
+	defer os.Remove(sources_toml.Name())
+
+	_, err = sources_toml.Write([]byte(`name = "osbuild-test-addon-source"
+url = "file://REPO-PATH"
+type = "yum-baseurl"
+proxy = "https://proxy-url/"
+check_ssl = true
+check_gpg = true
+gpgkey_urls = ["https://url/path/to/gpg-key"]
+`))
+	require.NoError(t, err)
+
+	runComposerCLI(t, false, "sources", "list")
+	// todo: this will fail on RHEL
+	runComposerCLI(t, false, "sources", "info", "fedora")
+
+	runComposerCLI(t, false, "sources", "add", sources_toml.Name())
+	runComposerCLI(t, false, "sources", "change", sources_toml.Name())
+	runComposerCLI(t, false, "sources", "delete", "osbuild-test-addon-source")
+}
+
 func testCompose(t *testing.T, outputType string) {
 	tmpdir := NewTemporaryWorkDir(t, "osbuild-tests-")
 	defer tmpdir.Close(t)
diff --git a/internal/client/client_test.go b/internal/client/client_test.go
index 32fe5cab2..0358cb64d 100644
--- a/internal/client/client_test.go
+++ b/internal/client/client_test.go
@@ -13,7 +13,7 @@ import (
 	"testing"
 	"time"
 
-	test_distro "github.com/osbuild/osbuild-composer/internal/distro/fedoratest"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedoratest"
 	rpmmd_mock "github.com/osbuild/osbuild-composer/internal/mocks/rpmmd"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 	"github.com/osbuild/osbuild-composer/internal/weldr"
@@ -209,10 +209,14 @@ func TestMain(m *testing.M) {
 	// Create a mock API server listening on the temporary socket
 	fixture := rpmmd_mock.BaseFixture()
 	rpm := rpmmd_mock.NewRPMMDMock(fixture)
-	distro := test_distro.New()
+	distro := fedoratest.New()
+	arch, err := distro.GetArch("x86_64")
+	if err != nil {
+		panic(err)
+	}
 	repos := []rpmmd.RepoConfig{{Id: "test-id", BaseURL: "http://example.com/test/os/test_arch"}}
 	logger := log.New(os.Stdout, "", 0)
-	api := weldr.New(rpm, "test_arch", distro, repos, logger, fixture.Store)
+	api := weldr.New(rpm, arch, distro, repos, logger, fixture.Store)
 	server := http.Server{Handler: api}
 	defer server.Close()
 
diff --git a/internal/distro/distro_test.go b/internal/distro/distro_test.go
index 56c4f10b6..e664ec2c4 100644
--- a/internal/distro/distro_test.go
+++ b/internal/distro/distro_test.go
@@ -9,6 +9,11 @@ import (
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/distro"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora30"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora31"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora32"
+	"github.com/osbuild/osbuild-composer/internal/distro/rhel81"
+	"github.com/osbuild/osbuild-composer/internal/distro/rhel82"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
@@ -52,7 +57,7 @@ func TestDistro_Manifest(t *testing.T) {
 			t.Fatalf("rpmmd.LoadRepositories: %v", err)
 		}
 		t.Run(tt.ComposeRequest.OutputFormat, func(t *testing.T) {
-			distros, err := distro.NewDefaultRegistry()
+			distros, err := distro.NewRegistry(fedora30.New(), fedora31.New(), fedora32.New(), rhel81.New(), rhel82.New())
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -61,14 +66,21 @@ func TestDistro_Manifest(t *testing.T) {
 				t.Errorf("unknown distro: %v", tt.ComposeRequest.Distro)
 				return
 			}
-			size := d.GetSizeForOutputType(tt.ComposeRequest.OutputFormat, 0)
-			got, err := d.Manifest(tt.ComposeRequest.Blueprint.Customizations,
+			arch, err := d.GetArch(tt.ComposeRequest.Arch)
+			if err != nil {
+				t.Errorf("unknown arch: %v", tt.ComposeRequest.Arch)
+				return
+			}
+			imageType, err := arch.GetImageType(tt.ComposeRequest.OutputFormat)
+			if err != nil {
+				t.Errorf("unknown image type: %v", tt.ComposeRequest.OutputFormat)
+				return
+			}
+			got, err := imageType.Manifest(tt.ComposeRequest.Blueprint.Customizations,
 				repoMap[tt.ComposeRequest.Arch],
 				tt.RpmMD.Packages,
 				tt.RpmMD.BuildPackages,
-				tt.ComposeRequest.Arch,
-				tt.ComposeRequest.OutputFormat,
-				size)
+				imageType.Size(0))
 			if (err != nil) != (tt.Manifest == nil) {
 				t.Errorf("distro.Manifest() error = %v", err)
 				return
@@ -92,7 +104,7 @@ func TestDistro_RegistryList(t *testing.T) {
 		"rhel-8.2",
 	}
 
-	distros, err := distro.NewDefaultRegistry()
+	distros, err := distro.NewRegistry(fedora30.New(), fedora31.New(), fedora32.New(), rhel81.New(), rhel82.New())
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/internal/distro/fedora30/distro_test.go b/internal/distro/fedora30/distro_test.go
index fc5e258eb..ac8e89144 100644
--- a/internal/distro/fedora30/distro_test.go
+++ b/internal/distro/fedora30/distro_test.go
@@ -19,10 +19,7 @@ func TestListOutputFormats(t *testing.T) {
 		"vmdk",
 	}
 
-	f30, err := fedora30.New()
-	if err != nil {
-		t.Fatal(err)
-	}
+	f30 := fedora30.New()
 
 	if got := f30.ListOutputFormats(); !reflect.DeepEqual(got, want) {
 		t.Errorf("ListOutputFormats() = %v, want %v", got, want)
@@ -96,10 +93,7 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			f30, err := fedora30.New()
-			if err != nil {
-				t.Fatal(err)
-			}
+			f30 := fedora30.New()
 			got, got1, err := f30.FilenameFromType(tt.args.outputFormat)
 			if (err != nil) != tt.wantErr {
 				t.Errorf("FilenameFromType() error = %v, wantErr %v", err, tt.wantErr)
diff --git a/internal/distro/fedora31/distro_test.go b/internal/distro/fedora31/distro_test.go
index 104442bec..0bddb1020 100644
--- a/internal/distro/fedora31/distro_test.go
+++ b/internal/distro/fedora31/distro_test.go
@@ -19,10 +19,7 @@ func TestListOutputFormats(t *testing.T) {
 		"vmdk",
 	}
 
-	f31, err := fedora31.New()
-	if err != nil {
-		t.Fatal(err)
-	}
+	f31 := fedora31.New()
 
 	if got := f31.ListOutputFormats(); !reflect.DeepEqual(got, want) {
 		t.Errorf("ListOutputFormats() = %v, want %v", got, want)
@@ -96,10 +93,7 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			f31, err := fedora31.New()
-			if err != nil {
-				t.Fatal(err)
-			}
+			f31 := fedora31.New()
 			got, got1, err := f31.FilenameFromType(tt.args.outputFormat)
 			if (err != nil) != tt.wantErr {
 				t.Errorf("FilenameFromType() error = %v, wantErr %v", err, tt.wantErr)
diff --git a/internal/distro/fedora32/distro_test.go b/internal/distro/fedora32/distro_test.go
index 9d750a491..e01b049db 100644
--- a/internal/distro/fedora32/distro_test.go
+++ b/internal/distro/fedora32/distro_test.go
@@ -19,10 +19,7 @@ func TestListOutputFormats(t *testing.T) {
 		"vmdk",
 	}
 
-	f32, err := fedora32.New()
-	if err != nil {
-		t.Fatal(err)
-	}
+	f32 := fedora32.New()
 
 	if got := f32.ListOutputFormats(); !reflect.DeepEqual(got, want) {
 		t.Errorf("ListOutputFormats() = %v, want %v", got, want)
@@ -96,10 +93,8 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			f32, err := fedora32.New()
-			if err != nil {
-				t.Fatal(err)
-			}
+			f32 := fedora32.New()
+
 			got, got1, err := f32.FilenameFromType(tt.args.outputFormat)
 			if (err != nil) != tt.wantErr {
 				t.Errorf("FilenameFromType() error = %v, wantErr %v", err, tt.wantErr)
diff --git a/internal/distro/fedoratest/distro.go b/internal/distro/fedoratest/distro.go
index 5ac971036..158c33ae4 100644
--- a/internal/distro/fedoratest/distro.go
+++ b/internal/distro/fedoratest/distro.go
@@ -5,13 +5,89 @@ import (
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
 
+const ModulePlatformID = "platform:f30"
+
 type FedoraTestDistro struct{}
 
-const ModulePlatformID = "platform:f30"
+type FedoraTestDistroArch struct {
+	name   string
+	distro *FedoraTestDistro
+}
+
+type FedoraTestDistroImageType struct {
+	name string
+	arch *FedoraTestDistroArch
+}
+
+func (d *FedoraTestDistro) GetArch(arch string) (distro.Arch, error) {
+	if arch != "x86_64" {
+		return nil, errors.New("invalid architecture: " + arch)
+	}
+
+	return &FedoraTestDistroArch{
+		name:   arch,
+		distro: d,
+	}, nil
+}
+
+func (a *FedoraTestDistroArch) Name() string {
+	return a.name
+}
+
+func (a *FedoraTestDistroArch) ListImageTypes() []string {
+	return a.distro.ListOutputFormats()
+}
+
+func (a *FedoraTestDistroArch) GetImageType(imageType string) (distro.ImageType, error) {
+	if imageType != "qcow2" {
+		return nil, errors.New("invalid image type: " + imageType)
+	}
+
+	return &FedoraTestDistroImageType{
+		name: imageType,
+		arch: a,
+	}, nil
+}
+
+func (t *FedoraTestDistroImageType) Name() string {
+	return t.name
+}
+
+func (t *FedoraTestDistroImageType) Filename() string {
+	return "test.img"
+}
+
+func (t *FedoraTestDistroImageType) MIMEType() string {
+	return "application/x-test"
+}
+
+func (t *FedoraTestDistroImageType) Size(size uint64) uint64 {
+	return t.arch.distro.GetSizeForOutputType(t.name, size)
+}
+
+func (t *FedoraTestDistroImageType) BasePackages() ([]string, []string) {
+	return nil, nil
+}
+
+func (t *FedoraTestDistroImageType) BuildPackages() []string {
+	return nil
+}
+
+func (t *FedoraTestDistroImageType) Manifest(c *blueprint.Customizations,
+	repos []rpmmd.RepoConfig,
+	packageSpecs,
+	buildPackageSpecs []rpmmd.PackageSpec,
+	size uint64) (*osbuild.Manifest, error) {
+	return &osbuild.Manifest{
+		Pipeline: osbuild.Pipeline{},
+		Sources:  osbuild.Sources{},
+	}, nil
+}
 
 func New() *FedoraTestDistro {
 	return &FedoraTestDistro{}
diff --git a/internal/distro/rhel81/distro_test.go b/internal/distro/rhel81/distro_test.go
index 5cb02c4de..2cecf93f9 100644
--- a/internal/distro/rhel81/distro_test.go
+++ b/internal/distro/rhel81/distro_test.go
@@ -19,10 +19,7 @@ func TestListOutputFormats(t *testing.T) {
 		"vmdk",
 	}
 
-	el81, err := rhel81.New()
-	if err != nil {
-		t.Fatal(err)
-	}
+	el81 := rhel81.New()
 
 	if got := el81.ListOutputFormats(); !reflect.DeepEqual(got, want) {
 		t.Errorf("ListOutputFormats() = %v, want %v", got, want)
@@ -96,10 +93,7 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			el81, err := rhel81.New()
-			if err != nil {
-				t.Fatal(err)
-			}
+			el81 := rhel81.New()
 			got, got1, err := el81.FilenameFromType(tt.args.outputFormat)
 			if (err != nil) != tt.wantErr {
 				t.Errorf("FilenameFromType() error = %v, wantErr %v", err, tt.wantErr)
diff --git a/internal/distro/rhel82/distro_test.go b/internal/distro/rhel82/distro_test.go
index 444812fb3..6066eeac4 100644
--- a/internal/distro/rhel82/distro_test.go
+++ b/internal/distro/rhel82/distro_test.go
@@ -19,10 +19,7 @@ func TestListOutputFormats(t *testing.T) {
 		"vmdk",
 	}
 
-	el82, err := rhel82.New()
-	if err != nil {
-		t.Fatal(err)
-	}
+	el82 := rhel82.New()
 
 	if got := el82.ListOutputFormats(); !reflect.DeepEqual(got, want) {
 		t.Errorf("ListOutputFormats() = %v, want %v", got, want)
@@ -96,10 +93,7 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			el82, err := rhel82.New()
-			if err != nil {
-				t.Fatal(err)
-			}
+			el82 := rhel82.New()
 			got, got1, err := el82.FilenameFromType(tt.args.outputFormat)
 			if (err != nil) != tt.wantErr {
 				t.Errorf("FilenameFromType() error = %v, wantErr %v", err, tt.wantErr)
diff --git a/internal/distro/test/distro.go b/internal/distro/test/distro.go
index 5893570c0..6cf56a3a2 100644
--- a/internal/distro/test/distro.go
+++ b/internal/distro/test/distro.go
@@ -4,15 +4,68 @@ import (
 	"errors"
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
+	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/osbuild"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
 
 type TestDistro struct{}
+type TestArch struct{}
+type TestImageType struct{}
 
 const Name = "test-distro"
 const ModulePlatformID = "platform:test"
 
+func (d *TestDistro) GetArch(arch string) (distro.Arch, error) {
+	if arch != "test_arch" {
+		return nil, errors.New("invalid arch: " + arch)
+	}
+	return &TestArch{}, nil
+}
+
+func (a *TestArch) Name() string {
+	return Name
+}
+
+func (a *TestArch) ListImageTypes() []string {
+	return []string{"test-format"}
+}
+
+func (a *TestArch) GetImageType(imageType string) (distro.ImageType, error) {
+	if imageType != "test_output" {
+		return nil, errors.New("invalid image type: " + imageType)
+	}
+	return &TestImageType{}, nil
+}
+
+func (t *TestImageType) Name() string {
+	return "test-format"
+}
+
+func (t *TestImageType) Filename() string {
+	return "test.img"
+}
+
+func (t *TestImageType) MIMEType() string {
+	return "application/x-test"
+}
+
+func (t *TestImageType) Size(size uint64) uint64 {
+	return 0
+}
+
+func (t *TestImageType) BasePackages() ([]string, []string) {
+	return nil, nil
+}
+
+func (t *TestImageType) BuildPackages() []string {
+	return nil
+}
+
+func (t *TestImageType) Manifest(b *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, size uint64) (*osbuild.Manifest, error) {
+	return &osbuild.Manifest{}, nil
+}
+
 func New() *TestDistro {
 	return &TestDistro{}
 }
diff --git a/internal/jobqueue/api_test.go b/internal/jobqueue/api_test.go
index be86055c5..ab88fc6c6 100644
--- a/internal/jobqueue/api_test.go
+++ b/internal/jobqueue/api_test.go
@@ -5,7 +5,7 @@ import (
 	"testing"
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
-	test_distro "github.com/osbuild/osbuild-composer/internal/distro/fedoratest"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedoratest"
 	"github.com/osbuild/osbuild-composer/internal/jobqueue"
 	"github.com/osbuild/osbuild-composer/internal/store"
 	"github.com/osbuild/osbuild-composer/internal/test"
@@ -40,11 +40,19 @@ func TestBasic(t *testing.T) {
 
 func TestCreate(t *testing.T) {
 	id, _ := uuid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff")
-	distroStruct := test_distro.New()
+	distroStruct := fedoratest.New()
+	arch, err := distroStruct.GetArch("x86_64")
+	if err != nil {
+		t.Fatalf("error getting arch from distro")
+	}
+	imageType, err := arch.GetImageType("qcow2")
+	if err != nil {
+		t.Fatalf("error getting image type from arch")
+	}
 	store := store.New(nil)
 	api := jobqueue.New(nil, store)
 
-	err := store.PushCompose(distroStruct, id, &blueprint.Blueprint{}, nil, nil, nil, "x86_64", "qcow2", 0, nil)
+	err = store.PushCompose(distroStruct, arch, imageType, id, &blueprint.Blueprint{}, nil, nil, nil, 0, nil)
 	if err != nil {
 		t.Fatalf("error pushing compose: %v", err)
 	}
@@ -55,12 +63,20 @@ func TestCreate(t *testing.T) {
 
 func testUpdateTransition(t *testing.T, from, to string, expectedStatus int, expectedResponse string) {
 	id, _ := uuid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff")
-	distroStruct := test_distro.New()
+	distroStruct := fedoratest.New()
+	arch, err := distroStruct.GetArch("x86_64")
+	if err != nil {
+		t.Fatalf("error getting arch from distro")
+	}
+	imageType, err := arch.GetImageType("qcow2")
+	if err != nil {
+		t.Fatalf("error getting image type from arch")
+	}
 	store := store.New(nil)
 	api := jobqueue.New(nil, store)
 
 	if from != "VOID" {
-		err := store.PushCompose(distroStruct, id, &blueprint.Blueprint{}, nil, nil, nil, "x86_64", "qcow2", 0, nil)
+		err := store.PushCompose(distroStruct, arch, imageType, id, &blueprint.Blueprint{}, nil, nil, nil, 0, nil)
 		if err != nil {
 			t.Fatalf("error pushing compose: %v", err)
 		}
diff --git a/internal/test/helpers.go b/internal/test/helpers.go
index cd6360e89..de69a713b 100644
--- a/internal/test/helpers.go
+++ b/internal/test/helpers.go
@@ -9,6 +9,8 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"os"
+	"os/exec"
+	"path"
 	"strings"
 	"testing"
 	"time"
@@ -176,3 +178,26 @@ func IgnoreUuids() cmp.Option {
 func Ignore(what string) cmp.Option {
 	return cmp.FilterPath(func(p cmp.Path) bool { return p.String() == what }, cmp.Ignore())
 }
+
+// Create a temporary repository
+func SetUpTemporaryRepository() (string, error) {
+	dir, err := ioutil.TempDir("/tmp", "osbuild-composer-test-")
+	if err != nil {
+		return "", err
+	}
+	cmd := exec.Command("createrepo_c", path.Join(dir))
+	err = cmd.Start()
+	if err != nil {
+		return "", err
+	}
+	err = cmd.Wait()
+	if err != nil {
+		return "", err
+	}
+	return dir, nil
+}
+
+// Remove the temporary repository
+func TearDownTemporaryRepository(dir string) error {
+	return os.RemoveAll(dir)
+}
diff --git a/internal/weldr/api_test.go b/internal/weldr/api_test.go
index d6b8ad7ca..682c076ec 100644
--- a/internal/weldr/api_test.go
+++ b/internal/weldr/api_test.go
@@ -33,8 +33,12 @@ func createWeldrAPI(fixtureGenerator rpmmd_mock.FixtureGenerator) (*API, *store.
 	rpm := rpmmd_mock.NewRPMMDMock(fixture)
 	repos := []rpmmd.RepoConfig{{Id: "test-id", BaseURL: "http://example.com/test/os/x86_64"}}
 	d := test_distro.New()
+	arch, err := d.GetArch("x86_64")
+	if err != nil {
+		panic(err)
+	}
 
-	return New(rpm, "x86_64", d, repos, nil, fixture.Store), fixture.Store
+	return New(rpm, arch, d, repos, nil, fixture.Store), fixture.Store
 }
 
 func TestBasic(t *testing.T) {
@@ -823,7 +827,9 @@ func TestSourcesNew(t *testing.T) {
 		ExpectedStatus int
 		ExpectedJSON   string
 	}{
-		{"POST", "/api/v0/projects/source/new", ``, http.StatusBadRequest, `{"errors":[{"code":400,"id":"HTTPError","msg":"Bad Request"}],"status":false}`},
+		{"POST", "/api/v0/projects/source/new", ``, http.StatusBadRequest, `{"errors": [{"id": "ProjectsError","msg": "Missing source"}],"status":false}`},
+		// Bad JSON, missing quote after name
+		{"POST", "/api/v0/projects/source/new", `{"name: "fish","url": "https://download.opensuse.org/repositories/shells:/fish:/release:/3/Fedora_29/","type": "yum-baseurl","check_ssl": false,"check_gpg": false}`, http.StatusBadRequest, `{"errors": [{"id": "ProjectsError","msg": "Problem parsing POST body: invalid character 'f' after object key"}],"status":false}`},
 		{"POST", "/api/v0/projects/source/new", `{"name": "fish","url": "https://download.opensuse.org/repositories/shells:/fish:/release:/3/Fedora_29/","type": "yum-baseurl","check_ssl": false,"check_gpg": false}`, http.StatusOK, `{"status":true}`},
 	}
 
diff --git a/internal/weldrcheck/blueprints_test.go b/internal/weldrcheck/blueprints_test.go
index 890eb0994..8dc9086e3 100644
--- a/internal/weldrcheck/blueprints_test.go
+++ b/internal/weldrcheck/blueprints_test.go
@@ -24,6 +24,7 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/osbuild/osbuild-composer/internal/client"
+	"github.com/osbuild/osbuild-composer/internal/test"
 )
 
 // Hold test state to share between tests
@@ -33,12 +34,29 @@ var testState *TestState
 // Also makes sure there is a running server to test against
 func TestMain(m *testing.M) {
 	var err error
-	testState, err = setupTestState("/run/weldr/api.socket", 60*time.Second)
+	testState, err = setUpTestState("/run/weldr/api.socket", 60*time.Second)
 	if err != nil {
-		fmt.Printf("ERROR: Test setup failed: %s", err)
+		fmt.Printf("ERROR: Test setup failed: %s\n", err)
 		os.Exit(1)
 	}
-	os.Exit(m.Run())
+
+	// Setup the test repo
+	dir, err := test.SetUpTemporaryRepository()
+	if err != nil {
+		fmt.Printf("ERROR: Test repo setup failed: %s\n", err)
+		os.Exit(1)
+	}
+	testState.repoDir = dir
+
+	// Run the tests
+	rc := m.Run()
+
+	// Cleanup after the tests
+	err = test.TearDownTemporaryRepository(dir)
+	if err != nil {
+		fmt.Printf("ERROR: Failed to clean up temporary repository: %s\n", err)
+	}
+	os.Exit(rc)
 }
 
 // POST a new TOML blueprint
diff --git a/internal/weldrcheck/source_test.go b/internal/weldrcheck/source_test.go
new file mode 100644
index 000000000..72138be3b
--- /dev/null
+++ b/internal/weldrcheck/source_test.go
@@ -0,0 +1,190 @@
+// Package weldrcheck - source contains functions to check the source API
+// Copyright (C) 2020 by Red Hat, Inc.
+
+// Tests should be self-contained and not depend on the state of the server
+// They should use their own blueprints, not the default blueprints
+// They should not assume version numbers for packages will match
+// They should run tests that depend on previous results from the same function
+// not from other functions.
+
+// +build integration
+
+package weldrcheck
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/osbuild/osbuild-composer/internal/client"
+)
+
+// POST a new JSON source
+func TestPOSTJSONSourceV0(t *testing.T) {
+	source := `{
+		"name": "package-repo-json-v0",
+		"url": "file://REPO-PATH",
+		"type": "yum-baseurl",
+		"proxy": "https://proxy-url/",
+		"check_ssl": true,
+		"check_gpg": true,
+		"gpgkey_urls": ["https://url/path/to/gpg-key"]
+	}`
+	source = strings.Replace(source, "REPO-PATH", testState.repoDir, 1)
+
+	resp, err := client.PostJSONSourceV0(testState.socket, source)
+	require.NoError(t, err, "POST source failed with a client error")
+	require.True(t, resp.Status, "POST source failed: %#v", resp)
+
+	resp, err = client.DeleteSourceV0(testState.socket, "package-repo-json-v0")
+	require.NoError(t, err, "DELETE source failed with a client error")
+	require.True(t, resp.Status, "DELETE source failed: %#v", resp)
+}
+
+// POST an empty JSON source
+func TestPOSTEmptyJSONSourceV0(t *testing.T) {
+	resp, err := client.PostJSONSourceV0(testState.socket, "")
+	require.NoError(t, err, "POST source failed with a client error")
+	require.False(t, resp.Status, "did not return an error")
+}
+
+// POST an invalid JSON source
+func TestPOSTInvalidJSONSourceV0(t *testing.T) {
+	// Missing quote in url
+	source := `{
+		"name": "package-repo-json-v0",
+		"url": "file://REPO-PATH,
+		"type": "yum-baseurl",
+		"proxy": "https://proxy-url/",
+		"check_ssl": true,
+		"check_gpg": true,
+		"gpgkey_urls": ["https://url/path/to/gpg-key"]
+	}`
+
+	resp, err := client.PostJSONSourceV0(testState.socket, source)
+	require.NoError(t, err, "POST source failed with a client error")
+	require.False(t, resp.Status, "did not return an error")
+}
+
+// POST a new TOML source
+func TestPOSTTOMLSourceV0(t *testing.T) {
+	source := `
+		name = "package-repo-toml-v0"
+		url = "file://REPO-PATH"
+		type = "yum-baseurl"
+		proxy = "https://proxy-url/"
+		check_ssl = true
+		check_gpg = true
+		gpgkey_urls = ["https://url/path/to/gpg-key"]
+	`
+	source = strings.Replace(source, "REPO-PATH", testState.repoDir, 1)
+
+	resp, err := client.PostTOMLSourceV0(testState.socket, source)
+	require.NoError(t, err, "POST source failed with a client error")
+	require.True(t, resp.Status, "POST source failed: %#v", resp)
+
+	resp, err = client.DeleteSourceV0(testState.socket, "package-repo-toml-v0")
+	require.NoError(t, err, "DELETE source failed with a client error")
+	require.True(t, resp.Status, "DELETE source failed: %#v", resp)
+}
+
+// POST an empty TOML source
+func TestPOSTEmptyTOMLSourceV0(t *testing.T) {
+	resp, err := client.PostTOMLSourceV0(testState.socket, "")
+	require.NoError(t, err, "POST source failed with a client error")
+	require.False(t, resp.Status, "did not return an error")
+}
+
+// POST an invalid TOML source
+func TestPOSTInvalidTOMLSourceV0(t *testing.T) {
+	// Missing quote in url
+	source := `
+		name = "package-repo-toml-v0"
+		url = "file://REPO-PATH
+		type = "yum-baseurl"
+		proxy = "https://proxy-url/"
+		check_ssl = true
+		check_gpg = true
+		gpgkey_urls = ["https://url/path/to/gpg-key"]
+	`
+
+	resp, err := client.PostTOMLSourceV0(testState.socket, source)
+	require.NoError(t, err, "POST source failed with a client error")
+	require.False(t, resp.Status, "did not return an error")
+}
+
+// list sources
+func TestListSourcesV0(t *testing.T) {
+	sources := []string{`{
+		"name": "package-repo-1",
+		"url": "file://REPO-PATH",
+		"type": "yum-baseurl",
+		"proxy": "https://proxy-url/",
+		"check_ssl": true,
+		"check_gpg": true,
+		"gpgkey_urls": ["https://url/path/to/gpg-key"]
+	}`,
+		`{
+		"name": "package-repo-2",
+		"url": "file://REPO-PATH",
+		"type": "yum-baseurl",
+		"proxy": "https://proxy-url/",
+		"check_ssl": true,
+		"check_gpg": true,
+		"gpgkey_urls": ["https://url/path/to/gpg-key"]
+	}`}
+
+	for i := range sources {
+		source := strings.Replace(sources[i], "REPO-PATH", testState.repoDir, 1)
+		resp, err := client.PostJSONSourceV0(testState.socket, source)
+		require.NoError(t, err, "POST source failed with a client error")
+		require.True(t, resp.Status, "POST source failed: %#v", resp)
+	}
+
+	// Remove the test sources, ignoring any errors
+	defer func() {
+		for _, n := range []string{"package-repo-1", "package-repo-2"} {
+			resp, err := client.DeleteSourceV0(testState.socket, n)
+			require.NoError(t, err, "DELETE source failed with a client error")
+			require.True(t, resp.Status, "DELETE source failed: %#v", resp)
+		}
+	}()
+
+	// Get the list of sources
+	list, api, err := client.ListSourcesV0(testState.socket)
+	require.NoError(t, err, "GET source failed with a client error")
+	require.Nil(t, api, "ListSources failed: %#v", api)
+	require.True(t, len(list) > 1, "Not enough sources returned")
+	require.Contains(t, list, "package-repo-1")
+	require.Contains(t, list, "package-repo-2")
+}
+
+// Get the source info
+func TestGetSourceInfoV0(t *testing.T) {
+	source := `
+		name = "package-repo-info-v0"
+		url = "file://REPO-PATH"
+		type = "yum-baseurl"
+		proxy = "https://proxy-url/"
+		check_ssl = true
+		check_gpg = true
+		gpgkey_urls = ["https://url/path/to/gpg-key"]
+	`
+	source = strings.Replace(source, "REPO-PATH", testState.repoDir, 1)
+
+	resp, err := client.PostTOMLSourceV0(testState.socket, source)
+	require.NoError(t, err, "POST source failed with a client error")
+	require.True(t, resp.Status, "POST source failed: %#v", resp)
+
+	info, resp, err := client.GetSourceInfoV0(testState.socket, "package-repo-info-v0")
+	require.NoError(t, err, "GET source failed with a client error")
+	require.Nil(t, resp, "GET source failed: %#v", resp)
+	require.Contains(t, info, "package-repo-info-v0", "No source info returned")
+	require.Equal(t, "package-repo-info-v0", info["package-repo-info-v0"].Name)
+	require.Equal(t, "file://"+testState.repoDir, info["package-repo-info-v0"].URL)
+
+	resp, err = client.DeleteSourceV0(testState.socket, "package-repo-info-v0")
+	require.NoError(t, err, "DELETE source failed with a client error")
+	require.True(t, resp.Status, "DELETE source failed: %#v", resp)
+}