Repository: osbuild/osbuild-composer
Pull request #352 (2020-03-18 10:17:47)

Use testing module syntax and asserts in osbuild-tests
This is a WIP (or RFC :) commit covers issue #312 - aiming at using the integrated golang test functionality, together with using asserts instead of loggin functionality for errors. I'd appreciate any comments, as I guess this can be done better, although it looks fine from functional point of view. As this PR is in a WIP state right now, I didn't remove parts of the old code, but just left them commented out for a quick reference, and will remove them once the code is polished.

One of the possible downsides I'm aware of is that I'm not sure if there is any simpler/more elegant way to pass the `*testing.T` struct to all of the subsequently called functions or if I should take some different direction.

Another potential issue I dealt with is when I replaced some condition blocks that logged some text together with content of `err.Error()` when an error occurred, with 'relevant' asserts. However, I didn't manage to find a way to implement the functionality in the exactly same way as before, as in this case I can't use the `err.Error()` value in the formatted assert message anymore, since the test segfaults as there is no existing error when everything behaves fine. Therefore I had to use just `err` together with `%v` in the formatted text, which is different from what we had before. Is there a better way to do that, or my implementation is sufficient for such a use?

Many thanks for all suggestions and comments!

atodorov: Commit log message:
> This covers issue #312.

-> Fixes #312 and GH will close the related issue automatically upon merge.
atodorov: @jikortus I'm not sure if you did `git mv` when renaming the file or GitHub isn't handling that correctly. On some PRs the renames are shown nicely but here GH is showing the changes as deleted + newly added files. Makes it harder to compare.

/end rant
jikortus: Sorry for the mess with file renaming - indeed I used `git mv` for renaming, but either it's a GitHub problem or something else went wrong in the process...

Patch:
diff --git a/Makefile b/Makefile
index 60e810c66..90cc0ef9c 100644
--- a/Makefile
+++ b/Makefile
@@ -7,7 +7,7 @@ build:
 	go build -o osbuild-pipeline ./cmd/osbuild-pipeline/
 	go build -o osbuild-upload-azure ./cmd/osbuild-upload-azure/
 	go build -o osbuild-upload-aws ./cmd/osbuild-upload-aws/
-	go build -o osbuild-tests ./cmd/osbuild-tests/
+	go test -c -tags=integration -o osbuild-tests ./cmd/osbuild-tests/main_test.go
 	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
diff --git a/cmd/osbuild-composer/main.go b/cmd/osbuild-composer/main.go
index 24ca655ef..1083e5d44 100644
--- a/cmd/osbuild-composer/main.go
+++ b/cmd/osbuild-composer/main.go
@@ -84,7 +84,7 @@ func main() {
 
 	rpm := rpmmd.NewRPMMD(path.Join(cacheDirectory, "rpmmd"))
 
-	distros, err := distro.NewDefaultRegistry([]string{"/etc/osbuild-composer", "/usr/share/osbuild-composer"})
+	distros, err := distro.NewDefaultRegistry()
 	if err != nil {
 		log.Fatalf("Error loading distros: %v", err)
 	}
@@ -94,6 +94,11 @@ func main() {
 		log.Fatalf("Could not determine distro from host: " + 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)
+	}
+
 	var logger *log.Logger
 	if verbose {
 		logger = log.New(os.Stdout, "", 0)
@@ -102,7 +107,7 @@ func main() {
 	store := store.New(&stateDir, *distros)
 
 	jobAPI := jobqueue.New(logger, store)
-	weldrAPI := weldr.New(rpm, common.CurrentArch(), distribution, logger, store)
+	weldrAPI := weldr.New(rpm, common.CurrentArch(), 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 0b5d24d98..8264d3117 100644
--- a/cmd/osbuild-pipeline/main.go
+++ b/cmd/osbuild-pipeline/main.go
@@ -74,7 +74,7 @@ func main() {
 		}
 	}
 
-	distros, err := distro.NewDefaultRegistry([]string{"."})
+	distros, err := distro.NewDefaultRegistry()
 	if err != nil {
 		panic(err)
 	}
@@ -88,6 +88,11 @@ func main() {
 		return
 	}
 
+	repos, err := rpmmd.LoadRepositories([]string{"."}, distroArg)
+	if err != nil {
+		panic(err)
+	}
+
 	packages := make([]string, len(blueprint.Packages))
 	for i, pkg := range blueprint.Packages {
 		packages[i] = pkg.Name
@@ -112,7 +117,7 @@ func main() {
 	}
 
 	rpmmd := rpmmd.NewRPMMD(path.Join(home, ".cache/osbuild-composer/rpmmd"))
-	packageSpecs, checksums, err := rpmmd.Depsolve(packages, exclude_pkgs, d.Repositories(archArg), d.ModulePlatformID())
+	packageSpecs, checksums, err := rpmmd.Depsolve(packages, exclude_pkgs, repos[archArg], d.ModulePlatformID())
 	if err != nil {
 		panic("Could not depsolve: " + err.Error())
 	}
@@ -121,7 +126,7 @@ func main() {
 	if err != nil {
 		panic("Could not get build packages: " + err.Error())
 	}
-	buildPackageSpecs, _, err := rpmmd.Depsolve(buildPkgs, nil, d.Repositories(archArg), d.ModulePlatformID())
+	buildPackageSpecs, _, err := rpmmd.Depsolve(buildPkgs, nil, repos[archArg], d.ModulePlatformID())
 	if err != nil {
 		panic("Could not depsolve build packages: " + err.Error())
 	}
@@ -139,7 +144,7 @@ func main() {
 		}
 	} else {
 		size := d.GetSizeForOutputType(imageType, 0)
-		manifest, err := d.Manifest(blueprint.Customizations, nil, packageSpecs, buildPackageSpecs, archArg, imageType, size)
+		manifest, err := d.Manifest(blueprint.Customizations, repos[archArg], packageSpecs, buildPackageSpecs, archArg, imageType, size)
 		if err != nil {
 			panic(err.Error())
 		}
diff --git a/dnf-json b/dnf-json
index 30c4f7a4b..c3536a883 100755
--- a/dnf-json
+++ b/dnf-json
@@ -88,7 +88,10 @@ cachedir = arguments["cachedir"]
 module_platform_id = arguments["module_platform_id"]
 
 with tempfile.TemporaryDirectory() as persistdir:
-    base = create_base(repos, module_platform_id, persistdir, cachedir)
+    try:
+        base = create_base(repos, module_platform_id, persistdir, cachedir)
+    except dnf.exceptions.RepoError as e:
+        exit_with_dnf_error("RepoError", f"Error occurred when setting up repo: {e}")
 
     if command == "dump":
         packages = []
diff --git a/golang-github-osbuild-composer.spec b/golang-github-osbuild-composer.spec
index d08596751..9604f46c4 100644
--- a/golang-github-osbuild-composer.spec
+++ b/golang-github-osbuild-composer.spec
@@ -1,6 +1,6 @@
 %global goipath         github.com/osbuild/osbuild-composer
 
-Version:        7
+Version:        8
 
 %gometa
 
@@ -66,7 +66,6 @@ export GOFLAGS=-mod=vendor
 %endif
 %gobuild -o _bin/osbuild-composer %{goipath}/cmd/osbuild-composer
 %gobuild -o _bin/osbuild-worker %{goipath}/cmd/osbuild-worker
-%gobuild -o _bin/osbuild-tests %{goipath}/cmd/osbuild-tests
 %gobuild -o _bin/osbuild-image-tests %{goipath}/cmd/osbuild-image-tests
 
 # Build test binaries with `go test -c`, so that they can take advantage of
@@ -75,6 +74,7 @@ export GOFLAGS=-mod=vendor
 
 TEST_LDFLAGS="${LDFLAGS:-} -B 0x$(od -N 20 -An -tx1 -w100 /dev/urandom | tr -d ' ')"
 
+go test -c -tags=integration -ldflags="${TEST_LDFLAGS}" -o _bin/osbuild-tests %{goipath}/cmd/osbuild-tests
 go test -c -tags=integration -ldflags="${TEST_LDFLAGS}" -o _bin/osbuild-dnf-json-tests %{goipath}/cmd/osbuild-dnf-json-tests
 go test -c -tags=integration -ldflags="${TEST_LDFLAGS}" -o _bin/osbuild-weldr-tests %{goipath}/internal/weldrcheck/
 go test -c -tags=integration -ldflags="${TEST_LDFLAGS}" -o _bin/osbuild-rcm-tests %{goipath}/cmd/osbuild-rcm-tests
diff --git a/internal/distro/distro.go b/internal/distro/distro.go
index 008355103..524c52ee2 100644
--- a/internal/distro/distro.go
+++ b/internal/distro/distro.go
@@ -34,10 +34,6 @@ type Distro interface {
 	// for modularity support.
 	ModulePlatformID() string
 
-	// Returns a list of repositories from which this distribution gets its
-	// content.
-	Repositories(arch string) []rpmmd.RepoConfig
-
 	// Returns a sorted list of the output formats this distro supports.
 	ListOutputFormats() []string
 
@@ -58,7 +54,7 @@ type Distro interface {
 	// 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, additionalRepos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, imageFormat string, size uint64) (*osbuild.Manifest, error)
+	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
@@ -83,24 +79,24 @@ func NewRegistry(distros ...Distro) (*Registry, error) {
 }
 
 // Create a new Registry containing all known distros.
-func NewDefaultRegistry(confPaths []string) (*Registry, error) {
-	f30, err := fedora30.New(confPaths)
+func NewDefaultRegistry() (*Registry, error) {
+	f30, err := fedora30.New()
 	if err != nil {
 		return nil, fmt.Errorf("error loading fedora30: %v", err)
 	}
-	f31, err := fedora31.New(confPaths)
+	f31, err := fedora31.New()
 	if err != nil {
 		return nil, fmt.Errorf("error loading fedora31: %v", err)
 	}
-	f32, err := fedora32.New(confPaths)
+	f32, err := fedora32.New()
 	if err != nil {
 		return nil, fmt.Errorf("error loading fedora32: %v", err)
 	}
-	el81, err := rhel81.New(confPaths)
+	el81, err := rhel81.New()
 	if err != nil {
 		return nil, fmt.Errorf("error loading rhel81: %v", err)
 	}
-	el82, err := rhel82.New(confPaths)
+	el82, err := rhel82.New()
 	if err != nil {
 		return nil, fmt.Errorf("error loading rhel82: %v", err)
 	}
diff --git a/internal/distro/fedora30/distro.go b/internal/distro/fedora30/distro.go
index 362f804f6..8809444a7 100644
--- a/internal/distro/fedora30/distro.go
+++ b/internal/distro/fedora30/distro.go
@@ -2,8 +2,6 @@ package fedora30
 
 import (
 	"errors"
-	"fmt"
-	"log"
 	"sort"
 	"strconv"
 
@@ -28,7 +26,6 @@ type arch struct {
 	BootloaderPackages []string
 	BuildPackages      []string
 	UEFI               bool
-	Repositories       []rpmmd.RepoConfig
 }
 
 type output struct {
@@ -47,11 +44,10 @@ type output struct {
 const Distro = common.Fedora30
 const ModulePlatformID = "platform:f30"
 
-func New(confPaths []string) (*Fedora30, error) {
+func New() (*Fedora30, error) {
 	const GigaByte = 1024 * 1024 * 1024
 
 	r := Fedora30{
-		arches:  map[string]arch{},
 		outputs: map[string]output{},
 		buildPackages: []string{
 			"dnf",
@@ -62,45 +58,28 @@ func New(confPaths []string) (*Fedora30, error) {
 			"systemd",
 			"tar",
 		},
-	}
-
-	repoMap, err := rpmmd.LoadRepositories(confPaths, r.Name())
-	if err != nil {
-		return nil, fmt.Errorf("Could not load repository data for %s: %s", r.Name(), err.Error())
-	}
-
-	repos, exists := repoMap["x86_64"]
-	if !exists {
-		log.Printf("Could not load architecture-specific repository data for x86_64 (%s)", r.Name())
-	} else {
-		r.arches["x86_64"] = arch{
-			Name: "x86_64",
-			BootloaderPackages: []string{
-				"grub2-pc",
-			},
-			BuildPackages: []string{
-				"grub2-pc",
+		arches: map[string]arch{
+			"x86_64": arch{
+				Name: "x86_64",
+				BootloaderPackages: []string{
+					"grub2-pc",
+				},
+				BuildPackages: []string{
+					"grub2-pc",
+				},
 			},
-			Repositories: repos,
-		}
-	}
-
-	repos, exists = repoMap["aarch64"]
-	if !exists {
-		log.Printf("Could not load architecture-specific repository data for aarch64 (%s)", r.Name())
-	} else {
-		r.arches["aarch64"] = arch{
-			Name: "aarch64",
-			BootloaderPackages: []string{
-				"dracut-config-generic",
-				"efibootmgr",
-				"grub2-efi-aa64",
-				"grub2-tools",
-				"shim-aa64",
+			"aarch64": arch{
+				Name: "aarch64",
+				BootloaderPackages: []string{
+					"dracut-config-generic",
+					"efibootmgr",
+					"grub2-efi-aa64",
+					"grub2-tools",
+					"shim-aa64",
+				},
+				UEFI: true,
 			},
-			UEFI:         true,
-			Repositories: repos,
-		}
+		},
 	}
 
 	r.outputs["ami"] = output{
@@ -326,10 +305,6 @@ func (r *Fedora30) ModulePlatformID() string {
 	return ModulePlatformID
 }
 
-func (r *Fedora30) Repositories(arch string) []rpmmd.RepoConfig {
-	return r.arches[arch].Repositories
-}
-
 func (r *Fedora30) ListOutputFormats() []string {
 	formats := make([]string, 0, len(r.outputs))
 	for name := range r.outputs {
@@ -386,7 +361,7 @@ func (r *Fedora30) BuildPackages(outputArchitecture string) ([]string, error) {
 	return append(r.buildPackages, arch.BuildPackages...), nil
 }
 
-func (r *Fedora30) pipeline(c *blueprint.Customizations, additionalRepos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Pipeline, error) {
+func (r *Fedora30) pipeline(c *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Pipeline, error) {
 	output, exists := r.outputs[outputFormat]
 	if !exists {
 		return nil, errors.New("invalid output format: " + outputFormat)
@@ -398,9 +373,9 @@ func (r *Fedora30) pipeline(c *blueprint.Customizations, additionalRepos []rpmmd
 	}
 
 	p := &osbuild.Pipeline{}
-	p.SetBuild(r.buildPipeline(arch, buildPackageSpecs), "org.osbuild.fedora30")
+	p.SetBuild(r.buildPipeline(repos, arch, buildPackageSpecs), "org.osbuild.fedora30")
 
-	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, additionalRepos, packageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, repos, packageSpecs)))
 	p.AddStage(osbuild.NewFixBLSStage())
 
 	// TODO support setting all languages and install corresponding langpack-* package
@@ -475,8 +450,8 @@ func (r *Fedora30) sources(packages []rpmmd.PackageSpec) *osbuild.Sources {
 	}
 }
 
-func (r *Fedora30) Manifest(c *blueprint.Customizations, additionalRepos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Manifest, error) {
-	pipeline, err := r.pipeline(c, additionalRepos, packageSpecs, buildPackageSpecs, outputArchitecture, outputFormat, size)
+func (r *Fedora30) Manifest(c *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Manifest, error) {
+	pipeline, err := r.pipeline(c, repos, packageSpecs, buildPackageSpecs, outputArchitecture, outputFormat, size)
 	if err != nil {
 		return nil, err
 	}
@@ -491,15 +466,14 @@ func (r *Fedora30) Runner() string {
 	return "org.osbuild.fedora30"
 }
 
-func (r *Fedora30) buildPipeline(arch arch, packageSpecs []rpmmd.PackageSpec) *osbuild.Pipeline {
+func (r *Fedora30) buildPipeline(repos []rpmmd.RepoConfig, arch arch, packageSpecs []rpmmd.PackageSpec) *osbuild.Pipeline {
 	p := &osbuild.Pipeline{}
-	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, nil, packageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, repos, packageSpecs)))
 	return p
 }
 
-func (r *Fedora30) rpmStageOptions(arch arch, additionalRepos []rpmmd.RepoConfig, specs []rpmmd.PackageSpec) *osbuild.RPMStageOptions {
+func (r *Fedora30) rpmStageOptions(arch arch, repos []rpmmd.RepoConfig, specs []rpmmd.PackageSpec) *osbuild.RPMStageOptions {
 	var gpgKeys []string
-	repos := append(arch.Repositories, additionalRepos...)
 	for _, repo := range repos {
 		if repo.GPGKey == "" {
 			continue
diff --git a/internal/distro/fedora31/distro.go b/internal/distro/fedora31/distro.go
index 9b7204182..4a64ef55b 100644
--- a/internal/distro/fedora31/distro.go
+++ b/internal/distro/fedora31/distro.go
@@ -2,8 +2,6 @@ package fedora31
 
 import (
 	"errors"
-	"fmt"
-	"log"
 	"sort"
 	"strconv"
 
@@ -28,7 +26,6 @@ type arch struct {
 	BootloaderPackages []string
 	BuildPackages      []string
 	UEFI               bool
-	Repositories       []rpmmd.RepoConfig
 }
 
 type output struct {
@@ -47,11 +44,10 @@ type output struct {
 const Distro = common.Fedora31
 const ModulePlatformID = "platform:f31"
 
-func New(confPaths []string) (*Fedora31, error) {
+func New() (*Fedora31, error) {
 	const GigaByte = 1024 * 1024 * 1024
 
 	r := Fedora31{
-		arches:  map[string]arch{},
 		outputs: map[string]output{},
 		buildPackages: []string{
 			"dnf",
@@ -62,45 +58,28 @@ func New(confPaths []string) (*Fedora31, error) {
 			"systemd",
 			"tar",
 		},
-	}
-
-	repoMap, err := rpmmd.LoadRepositories(confPaths, r.Name())
-	if err != nil {
-		return nil, fmt.Errorf("Could not load repository data for %s: %s", r.Name(), err.Error())
-	}
-
-	repos, exists := repoMap["x86_64"]
-	if !exists {
-		log.Printf("Could not load architecture-specific repository data for x86_64 (%s)", r.Name())
-	} else {
-		r.arches["x86_64"] = arch{
-			Name: "x86_64",
-			BootloaderPackages: []string{
-				"grub2-pc",
-			},
-			BuildPackages: []string{
-				"grub2-pc",
+		arches: map[string]arch{
+			"x86_64": arch{
+				Name: "x86_64",
+				BootloaderPackages: []string{
+					"grub2-pc",
+				},
+				BuildPackages: []string{
+					"grub2-pc",
+				},
 			},
-			Repositories: repos,
-		}
-	}
-
-	repos, exists = repoMap["aarch64"]
-	if !exists {
-		log.Printf("Could not load architecture-specific repository data for aarch64 (%s)", r.Name())
-	} else {
-		r.arches["aarch64"] = arch{
-			Name: "aarch64",
-			BootloaderPackages: []string{
-				"dracut-config-generic",
-				"efibootmgr",
-				"grub2-efi-aa64",
-				"grub2-tools",
-				"shim-aa64",
+			"aarch64": arch{
+				Name: "aarch64",
+				BootloaderPackages: []string{
+					"dracut-config-generic",
+					"efibootmgr",
+					"grub2-efi-aa64",
+					"grub2-tools",
+					"shim-aa64",
+				},
+				UEFI: true,
 			},
-			UEFI:         true,
-			Repositories: repos,
-		}
+		},
 	}
 
 	r.outputs["ami"] = output{
@@ -326,10 +305,6 @@ func (r *Fedora31) ModulePlatformID() string {
 	return ModulePlatformID
 }
 
-func (r *Fedora31) Repositories(arch string) []rpmmd.RepoConfig {
-	return r.arches[arch].Repositories
-}
-
 func (r *Fedora31) ListOutputFormats() []string {
 	formats := make([]string, 0, len(r.outputs))
 	for name := range r.outputs {
@@ -386,7 +361,7 @@ func (r *Fedora31) BuildPackages(outputArchitecture string) ([]string, error) {
 	return append(r.buildPackages, arch.BuildPackages...), nil
 }
 
-func (r *Fedora31) pipeline(c *blueprint.Customizations, additionalRepos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Pipeline, error) {
+func (r *Fedora31) pipeline(c *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Pipeline, error) {
 	output, exists := r.outputs[outputFormat]
 	if !exists {
 		return nil, errors.New("invalid output format: " + outputFormat)
@@ -398,9 +373,9 @@ func (r *Fedora31) pipeline(c *blueprint.Customizations, additionalRepos []rpmmd
 	}
 
 	p := &osbuild.Pipeline{}
-	p.SetBuild(r.buildPipeline(arch, buildPackageSpecs), "org.osbuild.fedora31")
+	p.SetBuild(r.buildPipeline(repos, arch, buildPackageSpecs), "org.osbuild.fedora31")
 
-	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, additionalRepos, packageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, repos, packageSpecs)))
 	p.AddStage(osbuild.NewFixBLSStage())
 
 	// TODO support setting all languages and install corresponding langpack-* package
@@ -475,8 +450,8 @@ func (r *Fedora31) sources(packages []rpmmd.PackageSpec) *osbuild.Sources {
 	}
 }
 
-func (r *Fedora31) Manifest(c *blueprint.Customizations, additionalRepos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Manifest, error) {
-	pipeline, err := r.pipeline(c, additionalRepos, packageSpecs, buildPackageSpecs, outputArchitecture, outputFormat, size)
+func (r *Fedora31) Manifest(c *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Manifest, error) {
+	pipeline, err := r.pipeline(c, repos, packageSpecs, buildPackageSpecs, outputArchitecture, outputFormat, size)
 	if err != nil {
 		return nil, err
 	}
@@ -491,15 +466,14 @@ func (r *Fedora31) Runner() string {
 	return "org.osbuild.fedora31"
 }
 
-func (r *Fedora31) buildPipeline(arch arch, buildPackageSpecs []rpmmd.PackageSpec) *osbuild.Pipeline {
+func (r *Fedora31) buildPipeline(repos []rpmmd.RepoConfig, arch arch, buildPackageSpecs []rpmmd.PackageSpec) *osbuild.Pipeline {
 	p := &osbuild.Pipeline{}
-	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, nil, buildPackageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, repos, buildPackageSpecs)))
 	return p
 }
 
-func (r *Fedora31) rpmStageOptions(arch arch, additionalRepos []rpmmd.RepoConfig, specs []rpmmd.PackageSpec) *osbuild.RPMStageOptions {
+func (r *Fedora31) rpmStageOptions(arch arch, repos []rpmmd.RepoConfig, specs []rpmmd.PackageSpec) *osbuild.RPMStageOptions {
 	var gpgKeys []string
-	repos := append(arch.Repositories, additionalRepos...)
 	for _, repo := range repos {
 		if repo.GPGKey == "" {
 			continue
diff --git a/internal/distro/fedora32/distro.go b/internal/distro/fedora32/distro.go
index d7b732c30..9510cfa52 100644
--- a/internal/distro/fedora32/distro.go
+++ b/internal/distro/fedora32/distro.go
@@ -2,8 +2,6 @@ package fedora32
 
 import (
 	"errors"
-	"fmt"
-	"log"
 	"sort"
 	"strconv"
 
@@ -28,7 +26,6 @@ type arch struct {
 	BootloaderPackages []string
 	BuildPackages      []string
 	UEFI               bool
-	Repositories       []rpmmd.RepoConfig
 }
 
 type output struct {
@@ -47,11 +44,10 @@ type output struct {
 const Distro = common.Fedora32
 const ModulePlatformID = "platform:f32"
 
-func New(confPaths []string) (*Fedora32, error) {
+func New() (*Fedora32, error) {
 	const GigaByte = 1024 * 1024 * 1024
 
 	r := Fedora32{
-		arches:  map[string]arch{},
 		outputs: map[string]output{},
 		buildPackages: []string{
 			"dnf",
@@ -62,45 +58,28 @@ func New(confPaths []string) (*Fedora32, error) {
 			"systemd",
 			"tar",
 		},
-	}
-
-	repoMap, err := rpmmd.LoadRepositories(confPaths, r.Name())
-	if err != nil {
-		return nil, fmt.Errorf("Could not load repository data for %s: %s", r.Name(), err.Error())
-	}
-
-	repos, exists := repoMap["x86_64"]
-	if !exists {
-		log.Printf("Could not load architecture-specific repository data for x86_64 (%s)", r.Name())
-	} else {
-		r.arches["x86_64"] = arch{
-			Name: "x86_64",
-			BootloaderPackages: []string{
-				"grub2-pc",
-			},
-			BuildPackages: []string{
-				"grub2-pc",
+		arches: map[string]arch{
+			"x86_64": arch{
+				Name: "x86_64",
+				BootloaderPackages: []string{
+					"grub2-pc",
+				},
+				BuildPackages: []string{
+					"grub2-pc",
+				},
 			},
-			Repositories: repos,
-		}
-	}
-
-	repos, exists = repoMap["aarch64"]
-	if !exists {
-		log.Printf("Could not load architecture-specific repository data for aarch64 (%s)", r.Name())
-	} else {
-		r.arches["aarch64"] = arch{
-			Name: "aarch64",
-			BootloaderPackages: []string{
-				"dracut-config-generic",
-				"efibootmgr",
-				"grub2-efi-aa64",
-				"grub2-tools",
-				"shim-aa64",
+			"aarch64": arch{
+				Name: "aarch64",
+				BootloaderPackages: []string{
+					"dracut-config-generic",
+					"efibootmgr",
+					"grub2-efi-aa64",
+					"grub2-tools",
+					"shim-aa64",
+				},
+				UEFI: true,
 			},
-			UEFI:         true,
-			Repositories: repos,
-		}
+		},
 	}
 
 	r.outputs["ami"] = output{
@@ -326,10 +305,6 @@ func (r *Fedora32) ModulePlatformID() string {
 	return ModulePlatformID
 }
 
-func (r *Fedora32) Repositories(arch string) []rpmmd.RepoConfig {
-	return r.arches[arch].Repositories
-}
-
 func (r *Fedora32) ListOutputFormats() []string {
 	formats := make([]string, 0, len(r.outputs))
 	for name := range r.outputs {
@@ -386,7 +361,7 @@ func (r *Fedora32) BuildPackages(outputArchitecture string) ([]string, error) {
 	return append(r.buildPackages, arch.BuildPackages...), nil
 }
 
-func (r *Fedora32) pipeline(c *blueprint.Customizations, additionalRepos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Pipeline, error) {
+func (r *Fedora32) pipeline(c *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Pipeline, error) {
 	output, exists := r.outputs[outputFormat]
 	if !exists {
 		return nil, errors.New("invalid output format: " + outputFormat)
@@ -398,9 +373,9 @@ func (r *Fedora32) pipeline(c *blueprint.Customizations, additionalRepos []rpmmd
 	}
 
 	p := &osbuild.Pipeline{}
-	p.SetBuild(r.buildPipeline(arch, buildPackageSpecs), "org.osbuild.fedora32")
+	p.SetBuild(r.buildPipeline(repos, arch, buildPackageSpecs), "org.osbuild.fedora32")
 
-	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, additionalRepos, packageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, repos, packageSpecs)))
 	p.AddStage(osbuild.NewFixBLSStage())
 
 	// TODO support setting all languages and install corresponding langpack-* package
@@ -475,8 +450,8 @@ func (r *Fedora32) sources(packages []rpmmd.PackageSpec) *osbuild.Sources {
 	}
 }
 
-func (r *Fedora32) Manifest(c *blueprint.Customizations, additionalRepos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Manifest, error) {
-	pipeline, err := r.pipeline(c, additionalRepos, packageSpecs, buildPackageSpecs, outputArchitecture, outputFormat, size)
+func (r *Fedora32) Manifest(c *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Manifest, error) {
+	pipeline, err := r.pipeline(c, repos, packageSpecs, buildPackageSpecs, outputArchitecture, outputFormat, size)
 	if err != nil {
 		return nil, err
 	}
@@ -491,15 +466,14 @@ func (r *Fedora32) Runner() string {
 	return "org.osbuild.fedora32"
 }
 
-func (r *Fedora32) buildPipeline(arch arch, buildPackageSpecs []rpmmd.PackageSpec) *osbuild.Pipeline {
+func (r *Fedora32) buildPipeline(repos []rpmmd.RepoConfig, arch arch, buildPackageSpecs []rpmmd.PackageSpec) *osbuild.Pipeline {
 	p := &osbuild.Pipeline{}
-	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, nil, buildPackageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, repos, buildPackageSpecs)))
 	return p
 }
 
-func (r *Fedora32) rpmStageOptions(arch arch, additionalRepos []rpmmd.RepoConfig, specs []rpmmd.PackageSpec) *osbuild.RPMStageOptions {
+func (r *Fedora32) rpmStageOptions(arch arch, repos []rpmmd.RepoConfig, specs []rpmmd.PackageSpec) *osbuild.RPMStageOptions {
 	var gpgKeys []string
-	repos := append(arch.Repositories, additionalRepos...)
 	for _, repo := range repos {
 		if repo.GPGKey == "" {
 			continue
diff --git a/internal/distro/rhel81/distro.go b/internal/distro/rhel81/distro.go
index 4fe242b93..c8aacfe08 100644
--- a/internal/distro/rhel81/distro.go
+++ b/internal/distro/rhel81/distro.go
@@ -2,8 +2,6 @@ package rhel81
 
 import (
 	"errors"
-	"fmt"
-	"log"
 	"sort"
 	"strconv"
 
@@ -28,7 +26,6 @@ type arch struct {
 	BootloaderPackages []string
 	BuildPackages      []string
 	UEFI               bool
-	Repositories       []rpmmd.RepoConfig
 }
 
 type output struct {
@@ -48,11 +45,10 @@ type output struct {
 const Distro = common.RHEL81
 const ModulePlatformID = "platform:el8"
 
-func New(confPaths []string) (*RHEL81, error) {
+func New() (*RHEL81, error) {
 	const GigaByte = 1024 * 1024 * 1024
 
 	r := RHEL81{
-		arches:  map[string]arch{},
 		outputs: map[string]output{},
 		buildPackages: []string{
 			"dnf",
@@ -67,45 +63,28 @@ func New(confPaths []string) (*RHEL81, error) {
 			"tar",
 			"xfsprogs",
 		},
-	}
-
-	repoMap, err := rpmmd.LoadRepositories(confPaths, r.Name())
-	if err != nil {
-		return nil, fmt.Errorf("Could not load repository data for %s: %s", r.Name(), err.Error())
-	}
-
-	repos, exists := repoMap["x86_64"]
-	if !exists {
-		log.Printf("Could not load architecture-specific repository data for x86_64 (%s)", r.Name())
-	} else {
-		r.arches["x86_64"] = arch{
-			Name: "x86_64",
-			BootloaderPackages: []string{
-				"grub2-pc",
-			},
-			BuildPackages: []string{
-				"grub2-pc",
+		arches: map[string]arch{
+			"x86_64": arch{
+				Name: "x86_64",
+				BootloaderPackages: []string{
+					"grub2-pc",
+				},
+				BuildPackages: []string{
+					"grub2-pc",
+				},
 			},
-			Repositories: repos,
-		}
-	}
-
-	repos, exists = repoMap["aarch64"]
-	if !exists {
-		log.Printf("Could not load architecture-specific repository data for aarch64 (%s)", r.Name())
-	} else {
-		r.arches["aarch64"] = arch{
-			Name: "aarch64",
-			BootloaderPackages: []string{
-				"dracut-config-generic",
-				"efibootmgr",
-				"grub2-efi-aa64",
-				"grub2-tools",
-				"shim-aa64",
+			"aarch64": arch{
+				Name: "aarch64",
+				BootloaderPackages: []string{
+					"dracut-config-generic",
+					"efibootmgr",
+					"grub2-efi-aa64",
+					"grub2-tools",
+					"shim-aa64",
+				},
+				UEFI: true,
 			},
-			UEFI:         true,
-			Repositories: repos,
-		}
+		},
 	}
 
 	r.outputs["ami"] = output{
@@ -465,10 +444,6 @@ func (r *RHEL81) ModulePlatformID() string {
 	return ModulePlatformID
 }
 
-func (r *RHEL81) Repositories(arch string) []rpmmd.RepoConfig {
-	return r.arches[arch].Repositories
-}
-
 func (r *RHEL81) ListOutputFormats() []string {
 	formats := make([]string, 0, len(r.outputs))
 	for name := range r.outputs {
@@ -525,7 +500,7 @@ func (r *RHEL81) BuildPackages(outputArchitecture string) ([]string, error) {
 	return append(r.buildPackages, arch.BuildPackages...), nil
 }
 
-func (r *RHEL81) pipeline(c *blueprint.Customizations, additionalRepos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Pipeline, error) {
+func (r *RHEL81) pipeline(c *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Pipeline, error) {
 	output, exists := r.outputs[outputFormat]
 	if !exists {
 		return nil, errors.New("invalid output format: " + outputFormat)
@@ -537,9 +512,9 @@ func (r *RHEL81) pipeline(c *blueprint.Customizations, additionalRepos []rpmmd.R
 	}
 
 	p := &osbuild.Pipeline{}
-	p.SetBuild(r.buildPipeline(arch, buildPackageSpecs), "org.osbuild.rhel81")
+	p.SetBuild(r.buildPipeline(repos, arch, buildPackageSpecs), "org.osbuild.rhel81")
 
-	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, additionalRepos, packageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, repos, packageSpecs)))
 	p.AddStage(osbuild.NewFixBLSStage())
 
 	if output.Bootable {
@@ -619,8 +594,8 @@ func (r *RHEL81) sources(packages []rpmmd.PackageSpec) *osbuild.Sources {
 	}
 }
 
-func (r *RHEL81) Manifest(c *blueprint.Customizations, additionalRepos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Manifest, error) {
-	pipeline, err := r.pipeline(c, additionalRepos, packageSpecs, buildPackageSpecs, outputArchitecture, outputFormat, size)
+func (r *RHEL81) Manifest(c *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Manifest, error) {
+	pipeline, err := r.pipeline(c, repos, packageSpecs, buildPackageSpecs, outputArchitecture, outputFormat, size)
 	if err != nil {
 		return nil, err
 	}
@@ -635,15 +610,14 @@ func (r *RHEL81) Runner() string {
 	return "org.osbuild.rhel81"
 }
 
-func (r *RHEL81) buildPipeline(arch arch, buildPackageSpecs []rpmmd.PackageSpec) *osbuild.Pipeline {
+func (r *RHEL81) buildPipeline(repos []rpmmd.RepoConfig, arch arch, buildPackageSpecs []rpmmd.PackageSpec) *osbuild.Pipeline {
 	p := &osbuild.Pipeline{}
-	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, nil, buildPackageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, repos, buildPackageSpecs)))
 	return p
 }
 
-func (r *RHEL81) rpmStageOptions(arch arch, additionalRepos []rpmmd.RepoConfig, specs []rpmmd.PackageSpec) *osbuild.RPMStageOptions {
+func (r *RHEL81) rpmStageOptions(arch arch, repos []rpmmd.RepoConfig, specs []rpmmd.PackageSpec) *osbuild.RPMStageOptions {
 	var gpgKeys []string
-	repos := append(arch.Repositories, additionalRepos...)
 	for _, repo := range repos {
 		if repo.GPGKey == "" {
 			continue
diff --git a/internal/distro/rhel82/distro.go b/internal/distro/rhel82/distro.go
index 069bfe347..dc2a53fdb 100644
--- a/internal/distro/rhel82/distro.go
+++ b/internal/distro/rhel82/distro.go
@@ -2,8 +2,6 @@ package rhel82
 
 import (
 	"errors"
-	"fmt"
-	"log"
 	"sort"
 	"strconv"
 
@@ -28,7 +26,6 @@ type arch struct {
 	BootloaderPackages []string
 	BuildPackages      []string
 	UEFI               bool
-	Repositories       []rpmmd.RepoConfig
 }
 
 type output struct {
@@ -48,11 +45,10 @@ type output struct {
 const Distro = common.RHEL82
 const ModulePlatformID = "platform:el8"
 
-func New(confPaths []string) (*RHEL82, error) {
+func New() (*RHEL82, error) {
 	const GigaByte = 1024 * 1024 * 1024
 
 	r := RHEL82{
-		arches:  map[string]arch{},
 		outputs: map[string]output{},
 		buildPackages: []string{
 			"dnf",
@@ -67,45 +63,28 @@ func New(confPaths []string) (*RHEL82, error) {
 			"tar",
 			"xfsprogs",
 		},
-	}
-
-	repoMap, err := rpmmd.LoadRepositories(confPaths, r.Name())
-	if err != nil {
-		return nil, fmt.Errorf("Could not load repository data for %s: %s", r.Name(), err.Error())
-	}
-
-	repos, exists := repoMap["x86_64"]
-	if !exists {
-		log.Printf("Could not load architecture-specific repository data for x86_64 (%s)", r.Name())
-	} else {
-		r.arches["x86_64"] = arch{
-			Name: "x86_64",
-			BootloaderPackages: []string{
-				"grub2-pc",
-			},
-			BuildPackages: []string{
-				"grub2-pc",
+		arches: map[string]arch{
+			"x86_64": arch{
+				Name: "x86_64",
+				BootloaderPackages: []string{
+					"grub2-pc",
+				},
+				BuildPackages: []string{
+					"grub2-pc",
+				},
 			},
-			Repositories: repos,
-		}
-	}
-
-	repos, exists = repoMap["aarch64"]
-	if !exists {
-		log.Printf("Could not load architecture-specific repository data for aarch64 (%s)", r.Name())
-	} else {
-		r.arches["aarch64"] = arch{
-			Name: "aarch64",
-			BootloaderPackages: []string{
-				"dracut-config-generic",
-				"efibootmgr",
-				"grub2-efi-aa64",
-				"grub2-tools",
-				"shim-aa64",
+			"aarch64": arch{
+				Name: "aarch64",
+				BootloaderPackages: []string{
+					"dracut-config-generic",
+					"efibootmgr",
+					"grub2-efi-aa64",
+					"grub2-tools",
+					"shim-aa64",
+				},
+				UEFI: true,
 			},
-			UEFI:         true,
-			Repositories: repos,
-		}
+		},
 	}
 
 	r.outputs["ami"] = output{
@@ -465,10 +444,6 @@ func (r *RHEL82) ModulePlatformID() string {
 	return ModulePlatformID
 }
 
-func (r *RHEL82) Repositories(arch string) []rpmmd.RepoConfig {
-	return r.arches[arch].Repositories
-}
-
 func (r *RHEL82) ListOutputFormats() []string {
 	formats := make([]string, 0, len(r.outputs))
 	for name := range r.outputs {
@@ -525,7 +500,7 @@ func (r *RHEL82) BuildPackages(outputArchitecture string) ([]string, error) {
 	return append(r.buildPackages, arch.BuildPackages...), nil
 }
 
-func (r *RHEL82) pipeline(c *blueprint.Customizations, additionalRepos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Pipeline, error) {
+func (r *RHEL82) pipeline(c *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Pipeline, error) {
 	output, exists := r.outputs[outputFormat]
 	if !exists {
 		return nil, errors.New("invalid output format: " + outputFormat)
@@ -537,9 +512,9 @@ func (r *RHEL82) pipeline(c *blueprint.Customizations, additionalRepos []rpmmd.R
 	}
 
 	p := &osbuild.Pipeline{}
-	p.SetBuild(r.buildPipeline(arch, buildPackageSpecs), "org.osbuild.rhel82")
+	p.SetBuild(r.buildPipeline(repos, arch, buildPackageSpecs), "org.osbuild.rhel82")
 
-	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, additionalRepos, packageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, repos, packageSpecs)))
 	p.AddStage(osbuild.NewFixBLSStage())
 
 	if output.Bootable {
@@ -619,8 +594,8 @@ func (r *RHEL82) sources(packages []rpmmd.PackageSpec) *osbuild.Sources {
 	}
 }
 
-func (r *RHEL82) Manifest(c *blueprint.Customizations, additionalRepos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Manifest, error) {
-	pipeline, err := r.pipeline(c, additionalRepos, packageSpecs, buildPackageSpecs, outputArchitecture, outputFormat, size)
+func (r *RHEL82) Manifest(c *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Manifest, error) {
+	pipeline, err := r.pipeline(c, repos, packageSpecs, buildPackageSpecs, outputArchitecture, outputFormat, size)
 	if err != nil {
 		return nil, err
 	}
@@ -635,15 +610,14 @@ func (r *RHEL82) Runner() string {
 	return "org.osbuild.rhel82"
 }
 
-func (r *RHEL82) buildPipeline(arch arch, buildPackageSpecs []rpmmd.PackageSpec) *osbuild.Pipeline {
+func (r *RHEL82) buildPipeline(repos []rpmmd.RepoConfig, arch arch, buildPackageSpecs []rpmmd.PackageSpec) *osbuild.Pipeline {
 	p := &osbuild.Pipeline{}
-	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, nil, buildPackageSpecs)))
+	p.AddStage(osbuild.NewRPMStage(r.rpmStageOptions(arch, repos, buildPackageSpecs)))
 	return p
 }
 
-func (r *RHEL82) rpmStageOptions(arch arch, additionalRepos []rpmmd.RepoConfig, specs []rpmmd.PackageSpec) *osbuild.RPMStageOptions {
+func (r *RHEL82) rpmStageOptions(arch arch, repos []rpmmd.RepoConfig, specs []rpmmd.PackageSpec) *osbuild.RPMStageOptions {
 	var gpgKeys []string
-	repos := append(arch.Repositories, additionalRepos...)
 	for _, repo := range repos {
 		if repo.GPGKey == "" {
 			continue
diff --git a/internal/weldr/api.go b/internal/weldr/api.go
index 19d1c8ba0..08a0576f5 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -33,12 +33,13 @@ type API struct {
 	rpmmd  rpmmd.RPMMD
 	arch   string
 	distro distro.Distro
+	repos  []rpmmd.RepoConfig
 
 	logger *log.Logger
 	router *httprouter.Router
 }
 
-func New(rpmmd rpmmd.RPMMD, arch string, distro distro.Distro, logger *log.Logger, store *store.Store) *API {
+func New(rpmmd rpmmd.RPMMD, arch string, 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{
@@ -46,6 +47,7 @@ func New(rpmmd rpmmd.RPMMD, arch string, distro distro.Distro, logger *log.Logge
 		rpmmd:  rpmmd,
 		arch:   arch,
 		distro: distro,
+		repos:  repos,
 		logger: logger,
 	}
 
@@ -232,7 +234,7 @@ func (api *API) sourceListHandler(writer http.ResponseWriter, request *http.Requ
 
 	names := api.store.ListSources()
 
-	for _, repo := range api.distro.Repositories(api.arch) {
+	for _, repo := range api.repos {
 		names = append(names, repo.Id)
 	}
 
@@ -275,14 +277,14 @@ func (api *API) sourceInfoHandler(writer http.ResponseWriter, request *http.Requ
 	// if names is "*" we want all sources
 	if names == "*" {
 		sources = api.store.GetAllSources()
-		for _, repo := range api.distro.Repositories(api.arch) {
+		for _, repo := range api.repos {
 			sources[repo.Id] = store.NewSourceConfig(repo, true)
 		}
 	} else {
 		for _, name := range strings.Split(names, ",") {
 			// check if the source is one of the base repos
 			found := false
-			for _, repo := range api.distro.Repositories(api.arch) {
+			for _, repo := range api.repos {
 				if name == repo.Id {
 					sources[repo.Id] = store.NewSourceConfig(repo, true)
 					found = true
@@ -622,7 +624,7 @@ func (api *API) modulesInfoHandler(writer http.ResponseWriter, request *http.Req
 
 	if modulesRequested {
 		for i := range packageInfos {
-			err := packageInfos[i].FillDependencies(api.rpmmd, api.distro.Repositories(api.arch), api.distro.ModulePlatformID())
+			err := packageInfos[i].FillDependencies(api.rpmmd, api.repos, api.distro.ModulePlatformID())
 			if err != nil {
 				errors := responseError{
 					ID:  errorId,
@@ -653,7 +655,7 @@ func (api *API) projectsDepsolveHandler(writer http.ResponseWriter, request *htt
 
 	names := strings.Split(params.ByName("projects"), ",")
 
-	packages, _, err := api.rpmmd.Depsolve(names, nil, api.distro.Repositories(api.arch), api.distro.ModulePlatformID())
+	packages, _, err := api.rpmmd.Depsolve(names, nil, api.repos, api.distro.ModulePlatformID())
 
 	if err != nil {
 		errors := responseError{
@@ -1948,7 +1950,7 @@ func (api *API) composeFailedHandler(writer http.ResponseWriter, request *http.R
 }
 
 func (api *API) fetchPackageList() (rpmmd.PackageList, error) {
-	repos := api.distro.Repositories(api.arch)
+	repos := append([]rpmmd.RepoConfig{}, api.repos...)
 	for _, source := range api.store.GetAllSources() {
 		repos = append(repos, source.RepoConfig())
 	}
@@ -1969,7 +1971,7 @@ func getPkgNameGlob(pkg blueprint.Package) string {
 }
 
 func (api *API) depsolveBlueprint(bp *blueprint.Blueprint, outputType, arch string) ([]rpmmd.PackageSpec, []rpmmd.PackageSpec, error) {
-	repos := api.distro.Repositories(api.arch)
+	repos := append([]rpmmd.RepoConfig{}, api.repos...)
 	for _, source := range api.store.GetAllSources() {
 		repos = append(repos, source.RepoConfig())
 	}
diff --git a/repositories/fedora-32.json b/repositories/fedora-32.json
index 8ad151a79..d1405967f 100644
--- a/repositories/fedora-32.json
+++ b/repositories/fedora-32.json
@@ -2,14 +2,14 @@
     "x86_64": [
         {
             "id": "fedora",
-            "baseurl": "http://download.fedoraproject.org/pub/fedora/linux/releases/32/Everything/x86_64/os/",
+            "baseurl": "http://download.fedoraproject.org/pub/fedora/linux/development/32/Everything/x86_64/os/",
             "gpgkey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBF1RVqsBEADWMBqYv/G1r4PwyiPQCfg5fXFGXV1FCZ32qMi9gLUTv1CX7rYy\nH4Inj93oic+lt1kQ0kQCkINOwQczOkm6XDkEekmMrHknJpFLwrTK4AS28bYF2RjL\nM+QJ/dGXDMPYsP0tkLvoxaHr9WTRq89A+AmONcUAQIMJg3JxXAAafBi2UszUUEPI\nU35MyufFt2ePd1k/6hVAO8S2VT72TxXSY7Ha4X2J0pGzbqQ6Dq3AVzogsnoIi09A\n7fYutYZPVVAEGRUqavl0th8LyuZShASZ38CdAHBMvWV4bVZghd/wDV5ev3LXUE0o\nitLAqNSeiDJ3grKWN6v0qdU0l3Ya60sugABd3xaE+ROe8kDCy3WmAaO51Q880ZA2\niXOTJFObqkBTP9j9+ZeQ+KNE8SBoiH1EybKtBU8HmygZvu8ZC1TKUyL5gwGUJt8v\nergy5Bw3Q7av520sNGD3cIWr4fBAVYwdBoZT8RcsnU1PP67NmOGFcwSFJ/LpiOMC\npZ1IBvjOC7KyKEZY2/63kjW73mB7OHOd18BHtGVkA3QAdVlcSule/z68VOAy6bih\nE6mdxP28D4INsts8w6yr4G+3aEIN8u0qRQq66Ri5mOXTyle+ONudtfGg3U9lgicg\nz6oVk17RT0jV9uL6K41sGZ1sH/6yTXQKagdAYr3w1ix2L46JgzC+/+6SSwARAQAB\ntDFGZWRvcmEgKDMyKSA8ZmVkb3JhLTMyLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJdUVarAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRBsEwJtEslE0LdAD/wKdAMtfzr7O2y06/sOPnrb3D39Y2DXbB8y0iEmRdBL29Bq\n5btxwmAka7JZRJVFxPsOVqZ6KARjS0/oCBmJc0jCRANFCtM4UjVHTSsxrJfuPkel\nvrlNE9tcR6OCRpuj/PZgUa39iifF/FTUfDgh4Q91xiQoLqfBxOJzravQHoK9VzrM\nNTOu6J6l4zeGzY/ocj6DpT+5fdUO/3HgGFNiNYPC6GVzeiA3AAVR0sCyGENuqqdg\nwUxV3BIht05M5Wcdvxg1U9x5I3yjkLQw+idvX4pevTiCh9/0u+4g80cT/21Cxsdx\n7+DVHaewXbF87QQIcOAing0S5QE67r2uPVxmWy/56TKUqDoyP8SNsV62lT2jutsj\nLevNxUky011g5w3bc61UeaeKrrurFdRs+RwBVkXmtqm/i6g0ZTWZyWGO6gJd+HWA\nqY1NYiq4+cMvNLatmA2sOoCsRNmE9q6jM/ESVgaH8hSp8GcLuzt9/r4PZZGl5CvU\neldOiD221u8rzuHmLs4dsgwJJ9pgLT0cUAsOpbMPI0JpGIPQ2SG6yK7LmO6HFOxb\nAkz7IGUt0gy1MzPTyBvnB+WgD1I+IQXXsJbhP5+d+d3mOnqsd6oDM/grKBzrhoUe\noNadc9uzjqKlOrmrdIR3Bz38SSiWlde5fu6xPqJdmGZRNjXtcyJlbSPVDIloxw==\n=QWRO\n-----END PGP PUBLIC KEY BLOCK-----\n"
         }
     ],
     "aarch64": [
         {
             "id": "fedora",
-            "baseurl": "http://download.fedoraproject.org/pub/fedora/linux/releases/32/Everything/aarch64/os/",
+            "baseurl": "http://download.fedoraproject.org/pub/fedora/linux/development/32/Everything/aarch64/os/",
             "gpgkey": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBF1RVqsBEADWMBqYv/G1r4PwyiPQCfg5fXFGXV1FCZ32qMi9gLUTv1CX7rYy\nH4Inj93oic+lt1kQ0kQCkINOwQczOkm6XDkEekmMrHknJpFLwrTK4AS28bYF2RjL\nM+QJ/dGXDMPYsP0tkLvoxaHr9WTRq89A+AmONcUAQIMJg3JxXAAafBi2UszUUEPI\nU35MyufFt2ePd1k/6hVAO8S2VT72TxXSY7Ha4X2J0pGzbqQ6Dq3AVzogsnoIi09A\n7fYutYZPVVAEGRUqavl0th8LyuZShASZ38CdAHBMvWV4bVZghd/wDV5ev3LXUE0o\nitLAqNSeiDJ3grKWN6v0qdU0l3Ya60sugABd3xaE+ROe8kDCy3WmAaO51Q880ZA2\niXOTJFObqkBTP9j9+ZeQ+KNE8SBoiH1EybKtBU8HmygZvu8ZC1TKUyL5gwGUJt8v\nergy5Bw3Q7av520sNGD3cIWr4fBAVYwdBoZT8RcsnU1PP67NmOGFcwSFJ/LpiOMC\npZ1IBvjOC7KyKEZY2/63kjW73mB7OHOd18BHtGVkA3QAdVlcSule/z68VOAy6bih\nE6mdxP28D4INsts8w6yr4G+3aEIN8u0qRQq66Ri5mOXTyle+ONudtfGg3U9lgicg\nz6oVk17RT0jV9uL6K41sGZ1sH/6yTXQKagdAYr3w1ix2L46JgzC+/+6SSwARAQAB\ntDFGZWRvcmEgKDMyKSA8ZmVkb3JhLTMyLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJdUVarAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRBsEwJtEslE0LdAD/wKdAMtfzr7O2y06/sOPnrb3D39Y2DXbB8y0iEmRdBL29Bq\n5btxwmAka7JZRJVFxPsOVqZ6KARjS0/oCBmJc0jCRANFCtM4UjVHTSsxrJfuPkel\nvrlNE9tcR6OCRpuj/PZgUa39iifF/FTUfDgh4Q91xiQoLqfBxOJzravQHoK9VzrM\nNTOu6J6l4zeGzY/ocj6DpT+5fdUO/3HgGFNiNYPC6GVzeiA3AAVR0sCyGENuqqdg\nwUxV3BIht05M5Wcdvxg1U9x5I3yjkLQw+idvX4pevTiCh9/0u+4g80cT/21Cxsdx\n7+DVHaewXbF87QQIcOAing0S5QE67r2uPVxmWy/56TKUqDoyP8SNsV62lT2jutsj\nLevNxUky011g5w3bc61UeaeKrrurFdRs+RwBVkXmtqm/i6g0ZTWZyWGO6gJd+HWA\nqY1NYiq4+cMvNLatmA2sOoCsRNmE9q6jM/ESVgaH8hSp8GcLuzt9/r4PZZGl5CvU\neldOiD221u8rzuHmLs4dsgwJJ9pgLT0cUAsOpbMPI0JpGIPQ2SG6yK7LmO6HFOxb\nAkz7IGUt0gy1MzPTyBvnB+WgD1I+IQXXsJbhP5+d+d3mOnqsd6oDM/grKBzrhoUe\noNadc9uzjqKlOrmrdIR3Bz38SSiWlde5fu6xPqJdmGZRNjXtcyJlbSPVDIloxw==\n=QWRO\n-----END PGP PUBLIC KEY BLOCK-----\n"
         }
     ]


Test patch:
diff --git a/cmd/osbuild-tests/main.go b/cmd/osbuild-tests/main.go
deleted file mode 100644
index 2f7ebf32a..000000000
--- a/cmd/osbuild-tests/main.go
+++ /dev/null
@@ -1,311 +0,0 @@
-package main
-
-import (
-	"bytes"
-	"encoding/json"
-	"io/ioutil"
-	"log"
-	"os"
-	"os/exec"
-	"strings"
-	"time"
-
-	"github.com/BurntSushi/toml"
-	"github.com/google/uuid"
-	"github.com/osbuild/osbuild-composer/internal/blueprint"
-)
-
-func main() {
-	// Smoke tests, until functionality tested fully
-	// that the calls succeed and return valid output
-	runComposerCLI(false, "compose", "types")
-	runComposerCLI(false, "compose", "status")
-	runComposerCLI(false, "compose", "list")
-	runComposerCLI(false, "compose", "list", "waiting")
-	runComposerCLI(false, "compose", "list", "running")
-	runComposerCLI(false, "compose", "list", "finished")
-	runComposerCLI(false, "compose", "list", "failed")
-	// runCommand(false, "compose", "log", UUID, [<SIZE>])
-	// runCommand(false, "compose", "cancel", UUID)
-	// runCommand(false, "compose", "delete", UUID)
-	// runCommand(false, "compose", "info", UUID)
-	// runCommand(false, "compose", "metadata", UUID)
-	// runCommand(false, "compose", "logs", UUID)
-	// runCommand(false, "compose", "results", UUID)
-	// runCommand(false, "compose", "image", UUID)
-	runComposerCLI(false, "blueprints", "list")
-	// runCommand(false, "blueprints", "show", BLUEPRINT,....)
-	// runCommand(false, "blueprints", "changes", BLUEPRINT,....)
-	// runCommand(false, "blueprints", "diff", BLUEPRINT, FROM/NEWEST, TO/NEWEST/WORKSPACE)
-	// runCommand(false, "blueprints", "save", BLUEPRINT,...)
-	// runCommand(false, "blueprints", "delete", BLUEPRINT)
-	// runCommand(false, "blueprints", "depsolve", BLUEPRINT,...)
-	// runCommand(false, "blueprints", "push", BLUEPRINT.TOML)
-	// runCommand(false, "blueprints", "freeze", BLUEPRINT,...)
-	// runCommand(false, "blueprints", "freeze", "show", BLUEPRINT,...)
-	// runCommand(false, "blueprints", "freeze", "save", BLUEPRINT,...)
-	// runCommand(false, "blueprints", "tag", BLUEPRINT)
-	// runCommand(false, "blueprints", "undo", BLUEPRINT, COMMIT)
-	// runCommand(false, "blueprints", "workspace", BLUEPRINT)
-	runComposerCLI(false, "modules", "list")
-	runComposerCLI(false, "projects", "list")
-	runComposerCLI(false, "projects", "info", "filesystem")
-	runComposerCLI(false, "projects", "info", "filesystem", "kernel")
-	runComposerCLI(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(false, "status", "show")
-
-	// Full integration tests
-	testCompose("ami")
-}
-
-func testCompose(outputType string) {
-	tmpdir := NewTemporaryWorkDir("osbuild-tests-")
-	log.Println(tmpdir.Path)
-	defer tmpdir.Close()
-
-	bp := blueprint.Blueprint{
-		Name:        "empty",
-		Description: "Test empty blueprint in toml format",
-	}
-	pushBlueprint(&bp)
-	defer deleteBlueprint(&bp)
-
-	runComposerCLI(false, "blueprints", "save", "empty")
-	if _, err := os.Stat("empty.toml"); os.IsNotExist(err) {
-		log.Fatalf("empty.toml doesn not exist: %v", err)
-	}
-
-	uuid := startCompose("empty", outputType)
-	defer deleteCompose(uuid)
-	status := waitForCompose(uuid)
-	if status != "FINISHED" {
-		log.Fatalf("Unexpected compose result: %s", status)
-	}
-
-	runComposerCLI(false, "compose", "image", uuid.String())
-}
-
-func startCompose(name, outputType string) uuid.UUID {
-	var reply struct {
-		BuildID uuid.UUID `json:"build_id"`
-		Status  bool      `json:"status"`
-	}
-	rawReply := runComposerCLI(false, "compose", "start", name, outputType)
-	err := json.Unmarshal(rawReply, &reply)
-	if err != nil {
-		log.Fatalf("Unexpected reply: " + err.Error())
-	}
-	if !reply.Status {
-		log.Fatalf("Unexpected status %v", reply.Status)
-	}
-
-	return reply.BuildID
-}
-
-func deleteCompose(id uuid.UUID) {
-	type deleteUUID struct {
-		ID     uuid.UUID `json:"uuid"`
-		Status bool      `json:"status"`
-	}
-	var reply struct {
-		IDs    []deleteUUID  `json:"uuids"`
-		Errors []interface{} `json:"errors"`
-	}
-	rawReply := runComposerCLI(false, "compose", "delete", id.String())
-	err := json.Unmarshal(rawReply, &reply)
-	if err != nil {
-		log.Fatalf("Unexpected reply: " + err.Error())
-	}
-	if len(reply.Errors) != 0 {
-		log.Fatalf("Unexpected errors")
-	}
-	if len(reply.IDs) != 1 {
-		log.Fatalf("Unexpected number of UUIDs returned: %d", len(reply.IDs))
-	}
-	if !reply.IDs[0].Status {
-		log.Fatalf("Unexpected status %v", reply.IDs[0].Status)
-	}
-}
-
-func waitForCompose(uuid uuid.UUID) string {
-	for {
-		status := getComposeStatus(true, uuid)
-		if status == "FINISHED" || status == "FAILED" {
-			return status
-		}
-		time.Sleep(time.Second)
-	}
-}
-
-func getComposeStatus(quiet bool, uuid uuid.UUID) string {
-	var reply struct {
-		QueueStatus string `json:"queue_status"`
-	}
-	rawReply := runComposerCLI(quiet, "compose", "info", uuid.String())
-	err := json.Unmarshal(rawReply, &reply)
-	if err != nil {
-		log.Fatalf("Unexpected reply: " + err.Error())
-	}
-
-	return reply.QueueStatus
-}
-
-func pushBlueprint(bp *blueprint.Blueprint) {
-	tmpfile, err := ioutil.TempFile("", "osbuild-test-")
-	if err != nil {
-		log.Fatalf("Could not create temporary file: " + err.Error())
-	}
-	defer os.Remove(tmpfile.Name())
-
-	err = toml.NewEncoder(tmpfile).Encode(bp)
-	if err != nil {
-		log.Fatalf("Could not marshapl blueprint TOML: " + err.Error())
-	}
-	if err := tmpfile.Close(); err != nil {
-		log.Fatalf("Could not close toml file: " + err.Error())
-	}
-
-	var reply struct {
-		Status bool `json:"status"`
-	}
-	rawReply := runComposerCLI(false, "blueprints", "push", tmpfile.Name())
-	err = json.Unmarshal(rawReply, &reply)
-	if err != nil {
-		log.Fatalf("Unexpected reply: " + err.Error())
-	}
-	if !reply.Status {
-		log.Fatalf("Unexpected status %v", reply.Status)
-	}
-}
-
-func deleteBlueprint(bp *blueprint.Blueprint) {
-	var reply struct {
-		Status bool `json:"status"`
-	}
-	rawReply := runComposerCLI(false, "blueprints", "delete", bp.Name)
-	err := json.Unmarshal(rawReply, &reply)
-	if err != nil {
-		log.Fatalf("Unexpected reply: " + err.Error())
-	}
-	if !reply.Status {
-		log.Fatalf("Unexpected status %v", reply.Status)
-	}
-}
-
-func runComposerCLI(quiet bool, command ...string) json.RawMessage {
-	command = append([]string{"--json"}, command...)
-	cmd := exec.Command("composer-cli", command...)
-	stdout, err := cmd.StdoutPipe()
-	if err != nil {
-		log.Fatalf("Could not create command: " + err.Error())
-	}
-
-	if !quiet {
-		log.Printf("$ composer-cli %s\n", strings.Join(command, " "))
-	}
-
-	err = cmd.Start()
-	if err != nil {
-		log.Fatalf("Could not start command: %v", err)
-	}
-
-	var result json.RawMessage
-
-	contents, err := ioutil.ReadAll(stdout)
-	if err != nil {
-		log.Fatalf("Could not read stdout from command: " + err.Error())
-	}
-
-	if len(contents) != 0 {
-		err = json.Unmarshal(contents, &result)
-		if err != nil {
-			// We did not get JSON, try interpreting it as TOML
-			var data interface{}
-			err = toml.Unmarshal(contents, &data)
-			if err != nil {
-				log.Fatalf("Could not parse output: %s", err.Error())
-			}
-			buffer := bytes.Buffer{}
-			err = json.NewEncoder(&buffer).Encode(data)
-			if err != nil {
-				log.Fatalf("Could not remarshal TOML to JSON: %s", err.Error())
-			}
-			err = json.NewDecoder(&buffer).Decode(&result)
-			if err != nil {
-				log.Fatalf("Could not decode the remarshalled JSON: %s", err.Error())
-			}
-		}
-	}
-
-	err = cmd.Wait()
-	if err != nil {
-		log.Fatalf("Command failed: " + err.Error())
-	}
-
-	buffer := bytes.Buffer{}
-	encoder := json.NewEncoder(&buffer)
-	encoder.SetIndent("", "  ")
-	err = encoder.Encode(result)
-	if err != nil {
-		log.Fatalf("Could not remarshal the recevied JSON: " + err.Error())
-	}
-
-	if !quiet {
-		log.Printf("Return:\n%s\n", buffer.String())
-	}
-
-	return result
-}
-
-type TemporaryWorkDir struct {
-	OldWorkDir string
-	Path       string
-}
-
-// Creates a new temporary directory based on pattern and changes the current
-// working directory to it.
-//
-// Example:
-//   d := NewTemporaryWorkDir("foo-*")
-//   defer d.Close()
-func NewTemporaryWorkDir(pattern string) TemporaryWorkDir {
-	var d TemporaryWorkDir
-	var err error
-
-	d.OldWorkDir, err = os.Getwd()
-	if err != nil {
-		log.Fatalf("os.GetWd: %v", err)
-	}
-
-	d.Path, err = ioutil.TempDir("", pattern)
-	if err != nil {
-		log.Fatalf("ioutil.TempDir: %v", err)
-	}
-
-	err = os.Chdir(d.Path)
-	if err != nil {
-		log.Fatalf("os.ChDir: %v", err)
-	}
-
-	return d
-}
-
-// Change back to the previous working directory and removes the temporary one.
-func (d *TemporaryWorkDir) Close() {
-	var err error
-
-	err = os.Chdir(d.OldWorkDir)
-	if err != nil {
-		log.Fatalf("os.ChDir: %v", err)
-	}
-
-	err = os.RemoveAll(d.Path)
-	if err != nil {
-		log.Fatalf("os.RemoveAll: %v", err)
-	}
-}
diff --git a/cmd/osbuild-tests/main_test.go b/cmd/osbuild-tests/main_test.go
new file mode 100644
index 000000000..056df9b38
--- /dev/null
+++ b/cmd/osbuild-tests/main_test.go
@@ -0,0 +1,249 @@
+// +build integration
+
+package main
+
+import (
+	"bytes"
+	"encoding/json"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"time"
+
+	"github.com/BurntSushi/toml"
+	"github.com/google/uuid"
+	"github.com/osbuild/osbuild-composer/internal/blueprint"
+
+	"github.com/stretchr/testify/require"
+	"testing"
+)
+
+func TestComposeIntegration(t *testing.T) {
+	// Smoke tests, until functionality tested fully
+	// that the calls succeed and return valid output
+	runComposerCLI(t, false, "compose", "types")
+	runComposerCLI(t, false, "compose", "status")
+	runComposerCLI(t, false, "compose", "list")
+	runComposerCLI(t, false, "compose", "list", "waiting")
+	runComposerCLI(t, false, "compose", "list", "running")
+	runComposerCLI(t, false, "compose", "list", "finished")
+	runComposerCLI(t, false, "compose", "list", "failed")
+	// runCommand(false, "compose", "log", UUID, [<SIZE>])
+	// runCommand(false, "compose", "cancel", UUID)
+	// runCommand(false, "compose", "delete", UUID)
+	// runCommand(false, "compose", "info", UUID)
+	// runCommand(false, "compose", "metadata", UUID)
+	// runCommand(false, "compose", "logs", UUID)
+	// runCommand(false, "compose", "results", UUID)
+	// runCommand(false, "compose", "image", UUID)
+	runComposerCLI(t, false, "blueprints", "list")
+	// runCommand(false, "blueprints", "show", BLUEPRINT,....)
+	// runCommand(false, "blueprints", "changes", BLUEPRINT,....)
+	// runCommand(false, "blueprints", "diff", BLUEPRINT, FROM/NEWEST, TO/NEWEST/WORKSPACE)
+	// runCommand(false, "blueprints", "save", BLUEPRINT,...)
+	// runCommand(false, "blueprints", "delete", BLUEPRINT)
+	// runCommand(false, "blueprints", "depsolve", BLUEPRINT,...)
+	// runCommand(false, "blueprints", "push", BLUEPRINT.TOML)
+	// runCommand(false, "blueprints", "freeze", BLUEPRINT,...)
+	// runCommand(false, "blueprints", "freeze", "show", BLUEPRINT,...)
+	// runCommand(false, "blueprints", "freeze", "save", BLUEPRINT,...)
+	// runCommand(false, "blueprints", "tag", BLUEPRINT)
+	// runCommand(false, "blueprints", "undo", BLUEPRINT, COMMIT)
+	// runCommand(false, "blueprints", "workspace", BLUEPRINT)
+	runComposerCLI(t, false, "modules", "list")
+	runComposerCLI(t, false, "projects", "list")
+	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 testCompose(t *testing.T, outputType string) {
+	tmpdir := NewTemporaryWorkDir(t, "osbuild-tests-")
+	defer tmpdir.Close(t)
+
+	bp := blueprint.Blueprint{
+		Name:        "empty",
+		Description: "Test empty blueprint in toml format",
+	}
+	pushBlueprint(t, &bp)
+	defer deleteBlueprint(t, &bp)
+
+	runComposerCLI(t, false, "blueprints", "save", "empty")
+	_, err := os.Stat("empty.toml")
+	require.Nilf(t, err, "Error accessing 'empty.toml: %v'", err)
+
+	uuid := startCompose(t, "empty", outputType)
+	defer deleteCompose(t, uuid)
+	status := waitForCompose(t, uuid)
+	require.Equalf(t, status, "FINISHED", "Unexpected compose result: %s", status)
+
+	runComposerCLI(t, false, "compose", "image", uuid.String())
+}
+
+func startCompose(t *testing.T, name, outputType string) uuid.UUID {
+	var reply struct {
+		BuildID uuid.UUID `json:"build_id"`
+		Status  bool      `json:"status"`
+	}
+	rawReply := runComposerCLI(t, false, "compose", "start", name, outputType)
+	err := json.Unmarshal(rawReply, &reply)
+	require.Nilf(t, err, "Unexpected reply: %v", err)
+	require.Truef(t, reply.Status, "Unexpected status %v", reply.Status)
+
+	return reply.BuildID
+}
+
+func deleteCompose(t *testing.T, id uuid.UUID) {
+	type deleteUUID struct {
+		ID     uuid.UUID `json:"uuid"`
+		Status bool      `json:"status"`
+	}
+	var reply struct {
+		IDs    []deleteUUID  `json:"uuids"`
+		Errors []interface{} `json:"errors"`
+	}
+	rawReply := runComposerCLI(t, false, "compose", "delete", id.String())
+	err := json.Unmarshal(rawReply, &reply)
+	require.Nilf(t, err, "Unexpected reply: %v", err)
+	require.Zerof(t, len(reply.Errors), "Unexpected errors")
+	require.Equalf(t, len(reply.IDs), 1, "Unexpected number of UUIDs returned: %d", len(reply.IDs))
+	require.Truef(t, reply.IDs[0].Status, "Unexpected status %v", reply.IDs[0].Status)
+}
+
+func waitForCompose(t *testing.T, uuid uuid.UUID) string {
+	for {
+		status := getComposeStatus(t, true, uuid)
+		if status == "FINISHED" || status == "FAILED" {
+			return status
+		}
+		time.Sleep(time.Second)
+	}
+}
+
+func getComposeStatus(t *testing.T, quiet bool, uuid uuid.UUID) string {
+	var reply struct {
+		QueueStatus string `json:"queue_status"`
+	}
+	rawReply := runComposerCLI(t, quiet, "compose", "info", uuid.String())
+	err := json.Unmarshal(rawReply, &reply)
+	require.Nilf(t, err, "Unexpected reply: %v", err)
+
+	return reply.QueueStatus
+}
+
+func pushBlueprint(t *testing.T, bp *blueprint.Blueprint) {
+	tmpfile, err := ioutil.TempFile("", "osbuild-test-")
+	require.Nilf(t, err, "Could not create temporary file: %v", err)
+	defer os.Remove(tmpfile.Name())
+
+	err = toml.NewEncoder(tmpfile).Encode(bp)
+	require.Nilf(t, err, "Could not marshal blueprint TOML: %v", err)
+	err = tmpfile.Close()
+	require.Nilf(t, err, "Could not close toml file: %v", err)
+
+	var reply struct {
+		Status bool `json:"status"`
+	}
+	rawReply := runComposerCLI(t, false, "blueprints", "push", tmpfile.Name())
+	err = json.Unmarshal(rawReply, &reply)
+	require.Nilf(t, err, "Unexpected reply: %v", err)
+	require.Truef(t, reply.Status, "Unexpected status %v", reply.Status)
+}
+
+func deleteBlueprint(t *testing.T, bp *blueprint.Blueprint) {
+	var reply struct {
+		Status bool `json:"status"`
+	}
+	rawReply := runComposerCLI(t, false, "blueprints", "delete", bp.Name)
+	err := json.Unmarshal(rawReply, &reply)
+	require.Nilf(t, err, "Unexpected reply: %v", err)
+	require.Truef(t, reply.Status, "Unexpected status %v", reply.Status)
+}
+
+func runComposerCLI(t *testing.T, quiet bool, command ...string) json.RawMessage {
+	command = append([]string{"--json"}, command...)
+	cmd := exec.Command("composer-cli", command...)
+	stdout, err := cmd.StdoutPipe()
+	require.Nilf(t, err, "Could not create command: %v", err)
+
+	err = cmd.Start()
+	require.Nilf(t, err, "Could not start command: %v", err)
+
+	var result json.RawMessage
+
+	contents, err := ioutil.ReadAll(stdout)
+	require.Nilf(t, err, "Could not read stdout from command: %v", err)
+
+	if len(contents) != 0 {
+		err = json.Unmarshal(contents, &result)
+		if err != nil {
+			// We did not get JSON, try interpreting it as TOML
+			var data interface{}
+			err = toml.Unmarshal(contents, &data)
+			require.Nilf(t, err, "Could not parse output: %v", err)
+			buffer := bytes.Buffer{}
+			err = json.NewEncoder(&buffer).Encode(data)
+			require.Nilf(t, err, "Could not remarshal TOML to JSON: %v", err)
+			err = json.NewDecoder(&buffer).Decode(&result)
+			require.Nilf(t, err, "Could not decode the remarshalled JSON: %v", err)
+		}
+	}
+
+	err = cmd.Wait()
+	require.Nilf(t, err, "Command failed: %v", err)
+
+	buffer := bytes.Buffer{}
+	encoder := json.NewEncoder(&buffer)
+	encoder.SetIndent("", "  ")
+	err = encoder.Encode(result)
+	require.Nilf(t, err, "Could not remarshal the recevied JSON: %v", err)
+
+	return result
+}
+
+type TemporaryWorkDir struct {
+	OldWorkDir string
+	Path       string
+}
+
+// Creates a new temporary directory based on pattern and changes the current
+// working directory to it.
+//
+// Example:
+//   d := NewTemporaryWorkDir(t, "foo-*")
+//   defer d.Close(t)
+func NewTemporaryWorkDir(t *testing.T, pattern string) TemporaryWorkDir {
+	var d TemporaryWorkDir
+	var err error
+
+	d.OldWorkDir, err = os.Getwd()
+	require.Nilf(t, err, "os.GetWd: %v", err)
+
+	d.Path, err = ioutil.TempDir("", pattern)
+	require.Nilf(t, err, "ioutil.TempDir: %v", err)
+
+	err = os.Chdir(d.Path)
+	require.Nilf(t, err, "os.ChDir: %v", err)
+
+	return d
+}
+
+// Change back to the previous working directory and removes the temporary one.
+func (d *TemporaryWorkDir) Close(t *testing.T) {
+	var err error
+
+	err = os.Chdir(d.OldWorkDir)
+	require.Nilf(t, err, "os.ChDir: %v", err)
+
+	err = os.RemoveAll(d.Path)
+	require.Nilf(t, err, "os.RemoveAll: %v", err)
+}
diff --git a/internal/client/client_test.go b/internal/client/client_test.go
index 4b68f0d7d..32fe5cab2 100644
--- a/internal/client/client_test.go
+++ b/internal/client/client_test.go
@@ -15,6 +15,7 @@ import (
 
 	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/rpmmd"
 	"github.com/osbuild/osbuild-composer/internal/weldr"
 
 	"github.com/google/go-cmp/cmp"
@@ -209,8 +210,9 @@ func TestMain(m *testing.M) {
 	fixture := rpmmd_mock.BaseFixture()
 	rpm := rpmmd_mock.NewRPMMDMock(fixture)
 	distro := test_distro.New()
+	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, logger, fixture.Store)
+	api := weldr.New(rpm, "test_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 6d5855573..56c4f10b6 100644
--- a/internal/distro/distro_test.go
+++ b/internal/distro/distro_test.go
@@ -47,8 +47,12 @@ func TestDistro_Manifest(t *testing.T) {
 			t.Logf("Skipping '%s'.", fileInfo.Name())
 			continue
 		}
+		repoMap, err := rpmmd.LoadRepositories([]string{"../.."}, tt.ComposeRequest.Distro)
+		if err != nil {
+			t.Fatalf("rpmmd.LoadRepositories: %v", err)
+		}
 		t.Run(tt.ComposeRequest.OutputFormat, func(t *testing.T) {
-			distros, err := distro.NewDefaultRegistry([]string{"../.."})
+			distros, err := distro.NewDefaultRegistry()
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -59,7 +63,7 @@ func TestDistro_Manifest(t *testing.T) {
 			}
 			size := d.GetSizeForOutputType(tt.ComposeRequest.OutputFormat, 0)
 			got, err := d.Manifest(tt.ComposeRequest.Blueprint.Customizations,
-				nil,
+				repoMap[tt.ComposeRequest.Arch],
 				tt.RpmMD.Packages,
 				tt.RpmMD.BuildPackages,
 				tt.ComposeRequest.Arch,
@@ -88,7 +92,7 @@ func TestDistro_RegistryList(t *testing.T) {
 		"rhel-8.2",
 	}
 
-	distros, err := distro.NewDefaultRegistry([]string{"../.."})
+	distros, err := distro.NewDefaultRegistry()
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/internal/distro/fedora30/distro_test.go b/internal/distro/fedora30/distro_test.go
index b9498fefe..fc5e258eb 100644
--- a/internal/distro/fedora30/distro_test.go
+++ b/internal/distro/fedora30/distro_test.go
@@ -19,7 +19,7 @@ func TestListOutputFormats(t *testing.T) {
 		"vmdk",
 	}
 
-	f30, err := fedora30.New([]string{"../../../"})
+	f30, err := fedora30.New()
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -96,7 +96,7 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			f30, err := fedora30.New([]string{"../../../"})
+			f30, err := fedora30.New()
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/internal/distro/fedora31/distro_test.go b/internal/distro/fedora31/distro_test.go
index 9ddf559ec..104442bec 100644
--- a/internal/distro/fedora31/distro_test.go
+++ b/internal/distro/fedora31/distro_test.go
@@ -19,7 +19,7 @@ func TestListOutputFormats(t *testing.T) {
 		"vmdk",
 	}
 
-	f31, err := fedora31.New([]string{"../../../"})
+	f31, err := fedora31.New()
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -96,7 +96,7 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			f31, err := fedora31.New([]string{"../../../"})
+			f31, err := fedora31.New()
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/internal/distro/fedora32/distro_test.go b/internal/distro/fedora32/distro_test.go
index 862d404c6..9d750a491 100644
--- a/internal/distro/fedora32/distro_test.go
+++ b/internal/distro/fedora32/distro_test.go
@@ -19,7 +19,7 @@ func TestListOutputFormats(t *testing.T) {
 		"vmdk",
 	}
 
-	f32, err := fedora32.New([]string{"../../../"})
+	f32, err := fedora32.New()
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -96,7 +96,7 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			f32, err := fedora32.New([]string{"../../../"})
+			f32, err := fedora32.New()
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/internal/distro/fedoratest/distro.go b/internal/distro/fedoratest/distro.go
index 873c14a59..5ac971036 100644
--- a/internal/distro/fedoratest/distro.go
+++ b/internal/distro/fedoratest/distro.go
@@ -29,15 +29,6 @@ func (d *FedoraTestDistro) ModulePlatformID() string {
 	return ModulePlatformID
 }
 
-func (d *FedoraTestDistro) Repositories(arch string) []rpmmd.RepoConfig {
-	return []rpmmd.RepoConfig{
-		{
-			Id:      "test-id",
-			BaseURL: "http://example.com/test/os/" + arch,
-		},
-	}
-}
-
 func (d *FedoraTestDistro) ListOutputFormats() []string {
 	return []string{"qcow2"}
 }
@@ -62,7 +53,7 @@ func (d *FedoraTestDistro) BuildPackages(outputArchitecture string) ([]string, e
 	return nil, nil
 }
 
-func (d *FedoraTestDistro) pipeline(c *blueprint.Customizations, additionalRepos []rpmmd.RepoConfig, buildPackages, basePackages []rpmmd.PackageSpec, outputArch, outputFormat string, size uint64) (*osbuild.Pipeline, error) {
+func (d *FedoraTestDistro) pipeline(c *blueprint.Customizations, repos []rpmmd.RepoConfig, buildPackages, basePackages []rpmmd.PackageSpec, outputArch, outputFormat string, size uint64) (*osbuild.Pipeline, error) {
 	if outputFormat == "qcow2" && outputArch == "x86_64" {
 		return &osbuild.Pipeline{}, nil
 	} else {
@@ -74,8 +65,8 @@ func (r *FedoraTestDistro) sources(packages []rpmmd.PackageSpec) *osbuild.Source
 	return &osbuild.Sources{}
 }
 
-func (r *FedoraTestDistro) Manifest(c *blueprint.Customizations, additionalRepos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Manifest, error) {
-	pipeline, err := r.pipeline(c, additionalRepos, packageSpecs, buildPackageSpecs, outputArchitecture, outputFormat, size)
+func (r *FedoraTestDistro) Manifest(c *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Manifest, error) {
+	pipeline, err := r.pipeline(c, repos, packageSpecs, buildPackageSpecs, outputArchitecture, outputFormat, size)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/distro/rhel81/distro_test.go b/internal/distro/rhel81/distro_test.go
index af4350f32..5cb02c4de 100644
--- a/internal/distro/rhel81/distro_test.go
+++ b/internal/distro/rhel81/distro_test.go
@@ -19,7 +19,7 @@ func TestListOutputFormats(t *testing.T) {
 		"vmdk",
 	}
 
-	el81, err := rhel81.New([]string{"../../../"})
+	el81, err := rhel81.New()
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -96,7 +96,7 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			el81, err := rhel81.New([]string{"../../../"})
+			el81, err := rhel81.New()
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/internal/distro/rhel82/distro_test.go b/internal/distro/rhel82/distro_test.go
index c30947665..444812fb3 100644
--- a/internal/distro/rhel82/distro_test.go
+++ b/internal/distro/rhel82/distro_test.go
@@ -19,7 +19,7 @@ func TestListOutputFormats(t *testing.T) {
 		"vmdk",
 	}
 
-	el82, err := rhel82.New([]string{"../../../"})
+	el82, err := rhel82.New()
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -96,7 +96,7 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			el82, err := rhel82.New([]string{"../../../"})
+			el82, err := rhel82.New()
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/internal/distro/test/distro.go b/internal/distro/test/distro.go
index 642334ba1..5893570c0 100644
--- a/internal/distro/test/distro.go
+++ b/internal/distro/test/distro.go
@@ -25,15 +25,6 @@ func (d *TestDistro) ModulePlatformID() string {
 	return ModulePlatformID
 }
 
-func (d *TestDistro) Repositories(arch string) []rpmmd.RepoConfig {
-	return []rpmmd.RepoConfig{
-		{
-			Id:      "test-id",
-			BaseURL: "http://example.com/test/os/" + arch,
-		},
-	}
-}
-
 func (d *TestDistro) ListOutputFormats() []string {
 	return []string{"test_format"}
 }
@@ -54,7 +45,7 @@ func (d *TestDistro) BuildPackages(outputArchitecture string) ([]string, error)
 	return nil, nil
 }
 
-func (d *TestDistro) pipeline(c *blueprint.Customizations, additionalRepos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArch, outputFormat string, size uint64) (*osbuild.Pipeline, error) {
+func (d *TestDistro) pipeline(c *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArch, outputFormat string, size uint64) (*osbuild.Pipeline, error) {
 	if outputFormat == "test_output" && outputArch == "test_arch" {
 		return &osbuild.Pipeline{}, nil
 	}
@@ -66,8 +57,8 @@ func (d *TestDistro) sources(packages []rpmmd.PackageSpec) *osbuild.Sources {
 	return &osbuild.Sources{}
 }
 
-func (r *TestDistro) Manifest(c *blueprint.Customizations, additionalRepos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Manifest, error) {
-	pipeline, err := r.pipeline(c, additionalRepos, packageSpecs, buildPackageSpecs, outputArchitecture, outputFormat, size)
+func (r *TestDistro) Manifest(c *blueprint.Customizations, repos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, outputArchitecture, outputFormat string, size uint64) (*osbuild.Manifest, error) {
+	pipeline, err := r.pipeline(c, repos, packageSpecs, buildPackageSpecs, outputArchitecture, outputFormat, size)
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/weldr/api_test.go b/internal/weldr/api_test.go
index da1463d64..163706010 100644
--- a/internal/weldr/api_test.go
+++ b/internal/weldr/api_test.go
@@ -31,9 +31,10 @@ import (
 func createWeldrAPI(fixtureGenerator rpmmd_mock.FixtureGenerator) (*API, *store.Store) {
 	fixture := fixtureGenerator()
 	rpm := rpmmd_mock.NewRPMMDMock(fixture)
+	repos := []rpmmd.RepoConfig{{Id: "test-id", BaseURL: "http://example.com/test/os/x86_64"}}
 	d := test_distro.New()
 
-	return New(rpm, "x86_64", d, nil, fixture.Store), fixture.Store
+	return New(rpm, "x86_64", d, repos, nil, fixture.Store), fixture.Store
 }
 
 func TestBasic(t *testing.T) {