Repository: osbuild/osbuild-composer
Pull request #261 (2020-02-26 22:58:26)

osbuild weldr test
This is a rewrite of the osbuild-test code that was running composer-cli from inside Go. It includes a client library, tests for the library, and integration tests (which I called checks to prevent confusion with the unit tests in _test.go files).

It includes blueprint integration tests, 2 of which fail on osbuild-composer (I'll work on fixing them).

It doesn't currently include a test for the blueprint diff, the osbuild-composer implementation is incomplete and I am unsure how useful diff really is in practice so it currently prints 'SKIP' for it.

Output looks like this with osbuild-composer:

```
[root@composer-rawhide osbuild-composer]# go build -o osbuild-test ./cmd/osbuild-tests/ && ./osbuild-test
2020/02/26 17:55:06 Running API check on /run/weldr/api.socket
2020/02/26 17:55:06 OK: status request
2020/02/26 17:55:06 INFO: Running tests against: osbuild-composer devel server using V1 API
2020/02/26 17:55:06 INFO: Running API V0 checks
2020/02/26 17:55:06 OK: List blueprint changes was successful
2020/02/26 17:55:07 OK: Depsolve a blueprint was successful
2020/02/26 17:55:07 SKIP: Diff of blueprint changes was skipped, needs to be implemented
2020/02/26 17:55:08 OK: Freeze a blueprint was successful
2020/02/26 17:55:08 FAIL: Tag a blueprint Tag failed: ClientError: unexpected end of JSON input
2020/02/26 17:55:08 OK: Bump Blueprint Version number was successful
2020/02/26 17:55:08 OK: DELETE blueprint was successful
2020/02/26 17:55:08 OK: DELETE blueprint changes from workspace was successful
2020/02/26 17:55:08 OK: DELETE new blueprint from workspace was successful
2020/02/26 17:55:08 OK: Get JSON Blueprint was successful
2020/02/26 17:55:08 OK: Get TOML Blueprint was successful
2020/02/26 17:55:08 OK: List blueprints was successful
2020/02/26 17:55:08 OK: POST of a JSON blueprint was successful
2020/02/26 17:55:08 OK: POST JSON blueprint to workspace was successful
2020/02/26 17:55:08 OK: POST of a TOML blueprint was successful
2020/02/26 17:55:08 OK: POST TOML blueprint to workspace was successful
2020/02/26 17:55:08 FAIL: Undo blueprint changes failed to undo change: Wrong package in the blueprint: {bash 0.5.*}
2020/02/26 17:55:08 FAIL: One or more V0 API checks failed
2020/02/26 17:55:08 INFO: Running API V1 checks
2020/02/26 17:55:08 OK: ALL V1 API checks were successful

```


jikortus: @bcl I think we shouldn't drop the tests that actually use `composer-cli` (maybe just reduce them to some reasonable subset?). IMHO we could make use of both approaches (testing via API + also retain testing via `composer` cli, even though those tests would rather cover the CLI tool rather than `osbuild-composer`).
bcl: > * Please keep the library in `internal/` for now (exposing this should be a separate discussion, so far we intentionally kept everything internal)
> * Please leave the existing `composer-cli` tests as-is for now and introduce this as a new test-binary (feel free to rename the existing one for clarity). We should have a discussion about what belongs where and the direction we want to go in. I agree that we probably want to keep some minimal tests using `composer-cli` to verify that there are no quirks there, but that the bulk of it should be using this library to test the API directly, as that is what we actually expose and support.

Ok, that's why I kept them as a separate commit, I wasn't sure how well that would be received. I do think it is important to have a public library that can be used. Otherwise we will continue to use composer-cli hacks, and like I've said, it wasn't really written for that.

I'll move this to a new binary for now. I still think composer-cli has no business being called from inside go, parsing its --json output is a complete hack.

You may want to hold any other reviews until I get things moved around and address @ondrejbudai comments about errors.


Patch:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 000000000..83c046dd6
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,32 @@
+name: Lint
+
+# NOTE(mhayden): Restricting branches prevents jobs from being doubled since
+# a push to a pull request triggers two events.
+on:
+  pull_request:
+    branches:
+      - "*"
+  push:
+    branches:
+      - master
+
+jobs:
+  lint:
+    name: Lint
+    runs-on: ubuntu-latest
+    steps:
+
+      - name: Set up Go 1.13
+        uses: actions/setup-go@v1
+        with:
+          go-version: 1.13
+        id: go
+
+      - name: Check out code into the Go module directory
+        uses: actions/checkout@v1
+
+      - name: Install golangci-lint
+        run: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v1.23.7
+
+      - name: Run golangci-lint
+        run: $(go env GOPATH)/bin/golangci-lint run
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 000000000..1958a0ac2
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,12 @@
+linters-settings:
+  govet:
+    disable:
+      - shadow # default value recommended by golangci
+      - composites
+
+issues:
+  # Maximum issues count per one linter. Set to 0 to disable. Default is 50.
+  max-issues-per-linter: 0
+
+  # Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
+  max-same-issues: 0
diff --git a/Makefile b/Makefile
index 7d190ecb1..1b1202a30 100644
--- a/Makefile
+++ b/Makefile
@@ -9,6 +9,7 @@ build:
 	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 build -o osbuild-weldr-tests ./cmd/osbuild-weldr-tests/
 	go build -o osbuild-dnf-json-tests ./cmd/osbuild-dnf-json-tests/
 
 .PHONY: install
diff --git a/cmd/osbuild-composer/main.go b/cmd/osbuild-composer/main.go
index d622aee32..5dbe566cd 100644
--- a/cmd/osbuild-composer/main.go
+++ b/cmd/osbuild-composer/main.go
@@ -7,6 +7,7 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/rcm"
 	"io/ioutil"
 	"log"
+	"path"
 	"os"
 
 	"github.com/osbuild/osbuild-composer/internal/common"
@@ -73,8 +74,17 @@ func main() {
 	weldrListener := composerListeners[0]
 	jobListener := composerListeners[1]
 
-	rpm := rpmmd.NewRPMMD()
-	distros := distro.NewRegistry([]string{"/etc/osbuild-composer", "/usr/share/osbuild-composer"})
+	cacheDirectory, ok := os.LookupEnv("CACHE_DIRECTORY")
+	if !ok {
+		log.Fatal("CACHE_DIRECTORY is not set. Is the service file missing CacheDirectory=?")
+	}
+
+	rpm := rpmmd.NewRPMMD(path.Join(cacheDirectory, "rpmmd"))
+
+	distros, err := distro.NewDefaultRegistry([]string{"/etc/osbuild-composer", "/usr/share/osbuild-composer"})
+	if err != nil {
+		log.Fatalf("Error loading distros: %v", err)
+	}
 
 	distribution, err := distros.FromHost()
 	if err != nil {
@@ -91,7 +101,10 @@ func main() {
 	jobAPI := jobqueue.New(logger, store)
 	weldrAPI := weldr.New(rpm, common.CurrentArch(), distribution, logger, store)
 
-	go jobAPI.Serve(jobListener)
+	go func() {
+		err := jobAPI.Serve(jobListener)
+		common.PanicOnError(err)
+	}()
 
 	// Optionally run RCM API as well as Weldr API
 	if rcmApiListeners, exists := listeners["osbuild-rcm.socket"]; exists {
@@ -124,9 +137,14 @@ func main() {
 			}
 
 			listener := tls.NewListener(listener, tlsConfig)
-			go jobAPI.Serve(listener)
+			go func() {
+				err := jobAPI.Serve(listener)
+				common.PanicOnError(err)
+			}()
 		}
 	}
 
-	weldrAPI.Serve(weldrListener)
+	err = weldrAPI.Serve(weldrListener)
+	common.PanicOnError(err)
+
 }
diff --git a/cmd/osbuild-pipeline/main.go b/cmd/osbuild-pipeline/main.go
index 467882b6e..4349e16d1 100644
--- a/cmd/osbuild-pipeline/main.go
+++ b/cmd/osbuild-pipeline/main.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"io/ioutil"
 	"os"
+	"path"
 
 	"github.com/osbuild/osbuild-composer/internal/common"
 
@@ -63,7 +64,11 @@ func main() {
 		}
 	}
 
-	distros := distro.NewRegistry([]string{"."})
+	distros, err := distro.NewDefaultRegistry([]string{"."})
+	if err != nil {
+		panic(err)
+	}
+
 	d := distros.GetDistro(distroArg)
 	if d == nil {
 		panic("unknown distro: " + distroArg)
@@ -87,7 +92,12 @@ func main() {
 	}
 	packages = append(pkgs, packages...)
 
-	rpmmd := rpmmd.NewRPMMD()
+	home, err := os.UserHomeDir()
+	if err != nil {
+		panic("os.UserHomeDir(): " + err.Error())
+	}
+
+	rpmmd := rpmmd.NewRPMMD(path.Join(home, ".cache/osbuild-composer/rpmmd"))
 	packageSpecs, checksums, err := rpmmd.Depsolve(packages, exclude_pkgs, d.Repositories(archArg), d.ModulePlatformID(), false)
 	if err != nil {
 		panic("Could not depsolve: " + err.Error())
diff --git a/cmd/osbuild-worker/main.go b/cmd/osbuild-worker/main.go
index 3a61b86bb..b3725fa72 100644
--- a/cmd/osbuild-worker/main.go
+++ b/cmd/osbuild-worker/main.go
@@ -100,7 +100,10 @@ func (c *ComposerClient) AddJob() (*jobqueue.Job, error) {
 	}
 
 	var b bytes.Buffer
-	json.NewEncoder(&b).Encode(request{})
+	err := json.NewEncoder(&b).Encode(request{})
+	if err != nil {
+		return nil, err
+	}
 	response, err := c.client.Post(c.createURL("/job-queue/v1/jobs"), "application/json", &b)
 	if err != nil {
 		return nil, err
@@ -110,7 +113,7 @@ func (c *ComposerClient) AddJob() (*jobqueue.Job, error) {
 	if response.StatusCode != http.StatusCreated {
 		rawR, _ := ioutil.ReadAll(response.Body)
 		r := string(rawR)
-		return nil, errors.New(fmt.Sprintf("couldn't create job, got %d: %s", response.StatusCode, r))
+		return nil, fmt.Errorf("couldn't create job, got %d: %s", response.StatusCode, r)
 	}
 
 	job := &jobqueue.Job{}
@@ -124,7 +127,10 @@ func (c *ComposerClient) AddJob() (*jobqueue.Job, error) {
 
 func (c *ComposerClient) UpdateJob(job *jobqueue.Job, status common.ImageBuildState, result *common.ComposeResult) error {
 	var b bytes.Buffer
-	json.NewEncoder(&b).Encode(&jobqueue.JobStatus{status, result})
+	err := json.NewEncoder(&b).Encode(&jobqueue.JobStatus{status, result})
+	if err != nil {
+		return err
+	}
 	urlPath := fmt.Sprintf("/job-queue/v1/jobs/%s/builds/%d", job.ID.String(), job.ImageBuildID)
 	url := c.createURL(urlPath)
 	req, err := http.NewRequest("PATCH", url, &b)
diff --git a/dnf-json b/dnf-json
old mode 100644
new mode 100755
index e8547a0f7..a2909659f
--- a/dnf-json
+++ b/dnf-json
@@ -129,7 +129,12 @@ with tempfile.TemporaryDirectory() as persistdir:
             exit_with_dnf_error("DepsolveError", f"There was a problem depsolving {arguments['package-specs']}: {e}")
 
         dependencies = []
-        for package in base.transaction.install_set:
+        for tsi in base.transaction:
+            # avoid using the install_set() helper, as it does not guarantee a stable order
+            if tsi.action not in dnf.transaction.FORWARD_ACTIONS:
+                continue
+            package = tsi.pkg
+
             dependencies.append({
                 "name": package.name,
                 "epoch": package.epoch,
diff --git a/golang-github-osbuild-composer.spec b/golang-github-osbuild-composer.spec
index 0a0dd2870..e550344a5 100644
--- a/golang-github-osbuild-composer.spec
+++ b/golang-github-osbuild-composer.spec
@@ -67,6 +67,7 @@ export GOFLAGS=-mod=vendor
 %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-weldr-tests %{goipath}/cmd/osbuild-weldr-tests
 %gobuild -o _bin/osbuild-dnf-json-tests %{goipath}/cmd/osbuild-dnf-json-tests
 %gobuild -o _bin/osbuild-image-tests %{goipath}/cmd/osbuild-image-tests
 
@@ -78,6 +79,7 @@ install -m 0755 -vp dnf-json                                %{buildroot}%{_libex
 
 install -m 0755 -vd                                         %{buildroot}%{_libexecdir}/tests/osbuild-composer
 install -m 0755 -vp _bin/osbuild-tests                      %{buildroot}%{_libexecdir}/tests/osbuild-composer/
+install -m 0755 -vp _bin/osbuild-weldr-tests                %{buildroot}%{_libexecdir}/tests/osbuild-composer/
 install -m 0755 -vp _bin/osbuild-dnf-json-tests             %{buildroot}%{_libexecdir}/tests/osbuild-composer/
 install -m 0755 -vp _bin/osbuild-image-tests                %{buildroot}%{_libexecdir}/tests/osbuild-composer/
 install -m 0755 -vp tools/image-info                        %{buildroot}%{_libexecdir}/osbuild-composer/
@@ -122,9 +124,7 @@ export GOPATH=$PWD/_build:%{gopath}
 %{_datadir}/osbuild-composer/
 %{_unitdir}/osbuild-composer.service
 %{_unitdir}/osbuild-composer.socket
-%{_unitdir}/osbuild-remote-worker@.service
 %{_unitdir}/osbuild-remote-worker.socket
-%{_unitdir}/osbuild-worker@.service
 %{_sysusersdir}/osbuild-composer.conf
 
 %package rcm
@@ -161,6 +161,8 @@ The worker for osbuild-composer
 
 %files worker
 %{_libexecdir}/osbuild-composer/osbuild-worker
+%{_unitdir}/osbuild-worker@.service
+%{_unitdir}/osbuild-remote-worker@.service
 
 %post worker
 %systemd_post osbuild-worker@.service osbuild-remote-worker@.service
diff --git a/internal/blueprint/blueprint.go b/internal/blueprint/blueprint.go
index 81b6c32d9..aa6c6a133 100644
--- a/internal/blueprint/blueprint.go
+++ b/internal/blueprint/blueprint.go
@@ -22,7 +22,7 @@ type Blueprint struct {
 type Change struct {
 	Commit    string    `json:"commit" toml:"commit"`
 	Message   string    `json:"message" toml:"message"`
-	Revision  *string   `json:"revision" toml:"revision"`
+	Revision  *int      `json:"revision" toml:"revision"`
 	Timestamp string    `json:"timestamp" toml:"timestamp"`
 	Blueprint Blueprint `json:"-" toml:"-"`
 }
diff --git a/internal/client/blueprints.go b/internal/client/blueprints.go
new file mode 100644
index 000000000..99ade9cef
--- /dev/null
+++ b/internal/client/blueprints.go
@@ -0,0 +1,167 @@
+// Package client - blueprints contains functions for the blueprint API
+// Copyright (C) 2020 by Red Hat, Inc.
+package client
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+)
+
+// PostTOMLBlueprintV0 sends a TOML blueprint string to the API
+// and returns an APIResponse
+func PostTOMLBlueprintV0(socket, blueprint string) (*APIResponse, error) {
+	body, resp, err := PostTOML(socket, "/api/v0/blueprints/new", blueprint)
+	if resp != nil || err != nil {
+		return resp, err
+	}
+	return NewAPIResponse(body)
+}
+
+// PostTOMLWorkspaceV0 sends a TOML blueprint string to the API
+// and returns an APIResponse
+func PostTOMLWorkspaceV0(socket, blueprint string) (*APIResponse, error) {
+	body, resp, err := PostTOML(socket, "/api/v0/blueprints/workspace", blueprint)
+	if resp != nil || err != nil {
+		return resp, err
+	}
+	return NewAPIResponse(body)
+}
+
+// PostJSONBlueprintV0 sends a JSON blueprint string to the API
+// and returns an APIResponse
+func PostJSONBlueprintV0(socket, blueprint string) (*APIResponse, error) {
+	body, resp, err := PostJSON(socket, "/api/v0/blueprints/new", blueprint)
+	if resp != nil || err != nil {
+		return resp, err
+	}
+	return NewAPIResponse(body)
+}
+
+// PostJSONWorkspaceV0 sends a JSON blueprint string to the API
+// and returns an APIResponse
+func PostJSONWorkspaceV0(socket, blueprint string) (*APIResponse, error) {
+	body, resp, err := PostJSON(socket, "/api/v0/blueprints/workspace", blueprint)
+	if resp != nil || err != nil {
+		return resp, err
+	}
+	return NewAPIResponse(body)
+}
+
+// DeleteBlueprintV0 deletes the named blueprint and returns an APIResponse
+func DeleteBlueprintV0(socket, bpName string) (*APIResponse, error) {
+	body, resp, err := DeleteRaw(socket, "/api/v0/blueprints/delete/"+bpName)
+	if resp != nil || err != nil {
+		return resp, err
+	}
+	return NewAPIResponse(body)
+}
+
+// DeleteWorkspaceV0 deletes the named blueprint's workspace and returns an APIResponse
+func DeleteWorkspaceV0(socket, bpName string) (*APIResponse, error) {
+	body, resp, err := DeleteRaw(socket, "/api/v0/blueprints/workspace/"+bpName)
+	if resp != nil || err != nil {
+		return resp, err
+	}
+	return NewAPIResponse(body)
+}
+
+// ListBlueprintsV0 returns a list of blueprint names
+func ListBlueprintsV0(socket string) ([]string, *APIResponse, error) {
+	body, resp, err := GetJSONAll(socket, "/api/v0/blueprints/list")
+	if resp != nil || err != nil {
+		return nil, resp, err
+	}
+	var list BlueprintsListV0
+	err = json.Unmarshal(body, &list)
+	if err != nil {
+		return nil, nil, err
+	}
+	return list.Blueprints, nil, nil
+}
+
+// GetBlueprintInfoTOMLV0 returns the requested blueprint as a TOML string
+func GetBlueprintInfoTOMLV0(socket, bpName string) (string, *APIResponse, error) {
+	body, resp, err := GetRaw(socket, "GET", "/api/v0/blueprints/info/"+bpName+"?format=toml")
+	if resp != nil || err != nil {
+		return "", resp, err
+	}
+	return string(body), nil, nil
+}
+
+// GetBlueprintsInfoJSONV0 returns the requested blueprints and their changed state
+func GetBlueprintsInfoJSONV0(socket, bpName string) (BlueprintsInfoV0, *APIResponse, error) {
+	body, resp, err := GetRaw(socket, "GET", "/api/v0/blueprints/info/"+bpName)
+	if resp != nil || err != nil {
+		return BlueprintsInfoV0{}, resp, err
+	}
+	var info BlueprintsInfoV0
+	err = json.Unmarshal(body, &info)
+	if err != nil {
+		return BlueprintsInfoV0{}, nil, err
+	}
+	return info, nil, nil
+}
+
+// GetBlueprintsChangesV0 returns the changes to the listed blueprints
+func GetBlueprintsChangesV0(socket string, bpNames []string) (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
+	}
+	var changes BlueprintsChangesV0
+	err = json.Unmarshal(body, &changes)
+	if err != nil {
+		return BlueprintsChangesV0{}, nil, err
+	}
+	return changes, nil, nil
+}
+
+// UndoBlueprintChangeV0 reverts a blueprint to a previous commit
+func UndoBlueprintChangeV0(socket, blueprint, commit string) (*APIResponse, error) {
+	request := fmt.Sprintf("/api/v0/blueprints/undo/%s/%s", blueprint, commit)
+	body, resp, err := PostRaw(socket, request, "", nil)
+	if resp != nil || err != nil {
+		return resp, err
+	}
+	return NewAPIResponse(body)
+}
+
+// TagBlueprintV0 tags the current blueprint commit as a new revision
+func TagBlueprintV0(socket, blueprint string) (*APIResponse, error) {
+	body, resp, err := PostRaw(socket, "/api/v0/blueprints/tag/"+blueprint, "", nil)
+	if resp != nil || err != nil {
+		return resp, err
+	}
+	return NewAPIResponse(body)
+}
+
+// DepsolveBlueprintV0 depsolves the listed blueprint
+func DepsolveBlueprintV0(socket, blueprint string) (BlueprintsDepsolveV0, *APIResponse, error) {
+	body, resp, err := GetRaw(socket, "GET", "/api/v0/blueprints/depsolve/"+blueprint)
+	if resp != nil || err != nil {
+		return BlueprintsDepsolveV0{}, resp, err
+	}
+	var deps BlueprintsDepsolveV0
+	err = json.Unmarshal(body, &deps)
+	if err != nil {
+		return BlueprintsDepsolveV0{}, nil, err
+	}
+	return deps, nil, nil
+}
+
+// FreezeBlueprintV0 depsolves the listed blueprint and returns the blueprint with frozen package
+// versions
+func FreezeBlueprintV0(socket, blueprint string) (BlueprintsFreezeV0, *APIResponse, error) {
+	body, resp, err := GetRaw(socket, "GET", "/api/v0/blueprints/freeze/"+blueprint)
+	if resp != nil || err != nil {
+		return BlueprintsFreezeV0{}, resp, err
+	}
+	var frozen BlueprintsFreezeV0
+	err = json.Unmarshal(body, &frozen)
+	if err != nil {
+		return BlueprintsFreezeV0{}, nil, err
+	}
+	return frozen, nil, nil
+}
diff --git a/internal/client/client.go b/internal/client/client.go
new file mode 100644
index 000000000..e2c1c73fa
--- /dev/null
+++ b/internal/client/client.go
@@ -0,0 +1,226 @@
+// Package client contains functions for communicating with the API server
+// Copyright (C) 2020 by Red Hat, Inc.
+package client
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"net"
+	"net/http"
+	"time"
+)
+
+// Request handles sending the request, handling errors, returning the response
+// socket is the path to a Unix Domain socket
+// path is the full URL path, including query strings
+// body is the data to send with POST
+// headers is a map of header:value to add to the request
+//
+// If it is successful a http.Response will be returned. If there is an error, the response will be
+// nil and error will be returned.
+func Request(socket, method, path, body string, headers map[string]string) (*http.Response, error) {
+	client := http.Client{
+		// TODO This may be too short/simple for downloading images
+		Timeout: 60 * time.Second,
+		Transport: &http.Transport{
+			DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
+				return net.Dial("unix", socket)
+			},
+		},
+	}
+
+	req, err := http.NewRequest(method, "http://localhost"+path, bytes.NewReader([]byte(body)))
+	if err != nil {
+		return nil, err
+	}
+
+	for h, v := range headers {
+		req.Header.Set(h, v)
+	}
+
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+
+	return resp, nil
+}
+
+// APIErrorMsg is an individual API error with an ID and a message string
+type APIErrorMsg struct {
+	ID  string `json:"id"`
+	Msg string `json:"msg"`
+}
+
+// String returns the error id and message as a string
+func (r *APIErrorMsg) String() string {
+	return fmt.Sprintf("%s: %s", r.ID, r.Msg)
+}
+
+// APIResponse is returned by some requests to indicate success or failure.
+// It is always returned when the status code is 400, indicating some kind of error with the request.
+// If Status is true the Errors list will not be included or will be empty.
+// When Status is false it will include at least one APIErrorMsg with details about the error.
+type APIResponse struct {
+	Status bool          `json:"status"`
+	Errors []APIErrorMsg `json:"errors,omitempty"`
+}
+
+// String returns the description of the first error, if there is one
+func (r *APIResponse) String() string {
+	if len(r.Errors) == 0 {
+		return ""
+	}
+	return r.Errors[0].String()
+}
+
+// AllErrors returns a list of error description strings
+func (r *APIResponse) AllErrors() (all []string) {
+	for i := range r.Errors {
+		all = append(all, r.Errors[i].String())
+	}
+	return all
+}
+
+// NewAPIResponse converts the response body to a status response
+func NewAPIResponse(body []byte) (*APIResponse, error) {
+	var status APIResponse
+	err := json.Unmarshal(body, &status)
+	if err != nil {
+		return nil, err
+	}
+	return &status, nil
+}
+
+// apiError converts an API error 400 JSON to a status response
+//
+// The response body should alway be of the form:
+//     {"status": false, "errors": [{"id": ERROR_ID, "msg": ERROR_MESSAGE}, ...]}
+func apiError(resp *http.Response) (*APIResponse, error) {
+	defer resp.Body.Close()
+
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	return NewAPIResponse(body)
+}
+
+// GetRaw returns raw data from a GET request
+// Errors from the API are returned as an APIResponse, client errors are returned as error
+func GetRaw(socket, method, path string) ([]byte, *APIResponse, error) {
+	resp, err := Request(socket, method, path, "", map[string]string{})
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// Convert the API's JSON error response to an error type and return it
+	if resp.StatusCode == 400 {
+		apiResponse, err := apiError(resp)
+		return nil, apiResponse, err
+	}
+	defer resp.Body.Close()
+
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return body, nil, nil
+}
+
+// GetJSONAll returns all JSON results from a GET request using offset/limit
+// This function makes 2 requests, the first with limit=0 to get the total number of results,
+// and then with limit=TOTAL to fetch all of the results.
+// The path passed to GetJSONAll should not include the limit or offset query parameters
+// Errors from the API are returned as an APIResponse, client errors are returned as error
+func GetJSONAll(socket, path string) ([]byte, *APIResponse, error) {
+	body, api, err := GetRaw(socket, "GET", path+"?limit=0")
+	if api != nil || err != nil {
+		return nil, api, err
+	}
+
+	// We just want the total
+	var j interface{}
+	err = json.Unmarshal(body, &j)
+	if err != nil {
+		return nil, nil, err
+	}
+	m := j.(map[string]interface{})
+	var v interface{}
+	var ok bool
+	if v, ok = m["total"]; !ok {
+		return nil, nil, errors.New("Response is missing the total value")
+	}
+
+	switch total := v.(type) {
+	case float64:
+		allResults := fmt.Sprintf("%s?limit=%v", path, total)
+		return GetRaw(socket, "GET", allResults)
+	}
+	return nil, nil, errors.New("Response 'total' is not a float64")
+}
+
+// PostRaw sends a POST with raw data and returns the raw response body
+// Errors from the API are returned as an APIResponse, client errors are returned as error
+func PostRaw(socket, path, body string, headers map[string]string) ([]byte, *APIResponse, error) {
+	resp, err := Request(socket, "POST", path, body, headers)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// Convert the API's JSON error response to an APIResponse
+	if resp.StatusCode == 400 {
+		apiResponse, err := apiError(resp)
+		return nil, apiResponse, err
+	}
+	defer resp.Body.Close()
+
+	responseBody, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return responseBody, nil, nil
+}
+
+// PostTOML sends a POST with TOML data and the Content-Type header set to "text/x-toml"
+// Errors from the API are returned as an APIResponse, client errors are returned as error
+func PostTOML(socket, path, body string) ([]byte, *APIResponse, error) {
+	headers := map[string]string{"Content-Type": "text/x-toml"}
+	return PostRaw(socket, path, body, headers)
+}
+
+// PostJSON sends a POST with JSON data and the Content-Type header set to "application/json"
+// Errors from the API are returned as an APIResponse, client errors are returned as error
+func PostJSON(socket, path, body string) ([]byte, *APIResponse, error) {
+	headers := map[string]string{"Content-Type": "application/json"}
+	return PostRaw(socket, path, body, headers)
+}
+
+// DeleteRaw sends a DELETE request
+// Errors from the API are returned as an APIResponse, client errors are returned as error
+func DeleteRaw(socket, path string) ([]byte, *APIResponse, error) {
+	resp, err := Request(socket, "DELETE", path, "", nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// Convert the API's JSON error response to an APIResponse
+	if resp.StatusCode == 400 {
+		apiResponse, err := apiError(resp)
+		return nil, apiResponse, err
+	}
+	defer resp.Body.Close()
+
+	responseBody, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return responseBody, nil, nil
+}
diff --git a/internal/client/json.go b/internal/client/json.go
new file mode 100644
index 000000000..4cf955342
--- /dev/null
+++ b/internal/client/json.go
@@ -0,0 +1,77 @@
+// Package client - json contains Exported API response structures
+// Copyright (C) 2020 by Red Hat, Inc.
+package client
+
+import (
+	"github.com/osbuild/osbuild-composer/internal/blueprint"
+	"github.com/osbuild/osbuild-composer/internal/rpmmd"
+)
+
+// StatusV0 is the response to /api/status from a v0+ server
+type StatusV0 struct {
+	API           string   `json:"api"`
+	DBSupported   bool     `json:"db_supported"`
+	DBVersion     string   `json:"db_version"`
+	SchemaVersion string   `json:"schema_version"`
+	Backend       string   `json:"backend"`
+	Build         string   `json:"build"`
+	Messages      []string `json:"messages"`
+}
+
+// BlueprintsListV0 is the response to /blueprints/list request
+type BlueprintsListV0 struct {
+	Total      uint     `json:"total"`
+	Offset     uint     `json:"offset"`
+	Limit      uint     `json:"limit"`
+	Blueprints []string `json:"blueprints"`
+}
+
+// ResponseError holds the API response error details
+type ResponseError struct {
+	Code int    `json:"code,omitempty"`
+	ID   string `json:"id"`
+	Msg  string `json:"msg"`
+}
+
+// BlueprintsInfoV0 is the response to /blueprints/info?format=json request
+type BlueprintsInfoV0 struct {
+	Blueprints []blueprint.Blueprint `json:"blueprints"`
+	Changes    []infoChange          `json:"changes"`
+	Errors     []ResponseError       `json:"errors"`
+}
+type infoChange struct {
+	Changed bool   `json:"changed"`
+	Name    string `json:"name"`
+}
+
+// BlueprintsChangesV0 is the response to /blueprints/changes/ request
+type BlueprintsChangesV0 struct {
+	BlueprintsChanges []bpChange      `json:"blueprints"`
+	Errors            []ResponseError `json:"errors"`
+	Limit             uint            `json:"limit"`
+	Offset            uint            `json:"offset"`
+}
+type bpChange struct {
+	Changes []blueprint.Change `json:"changes"`
+	Name    string             `json:"name"`
+	Total   int                `json:"total"`
+}
+
+// BlueprintsDepsolveV0 is the response to /blueprints/depsolve/ request
+type BlueprintsDepsolveV0 struct {
+	Blueprints []depsolveEntry `json:"blueprints"`
+	Errors     []ResponseError `json:"errors"`
+}
+type depsolveEntry struct {
+	Blueprint    blueprint.Blueprint `json:"blueprint"`
+	Dependencies []rpmmd.PackageSpec `json:"dependencies"`
+}
+
+// BlueprintsFreezeV0 is the response to /blueprints/freeze/ request
+type BlueprintsFreezeV0 struct {
+	Blueprints []blueprintFrozen `json:"blueprints"`
+	Errors     []ResponseError   `json:"errors"`
+}
+type blueprintFrozen struct {
+	Blueprint blueprint.Blueprint `json:"blueprint"`
+}
diff --git a/internal/client/weldr.go b/internal/client/weldr.go
new file mode 100644
index 000000000..711c898de
--- /dev/null
+++ b/internal/client/weldr.go
@@ -0,0 +1,20 @@
+// Package client - weldr contains functions to return API structures
+// Copyright (C) 2020 by Red Hat, Inc.
+package client
+
+import (
+	"encoding/json"
+)
+
+// GetStatusV0 makes a GET request to /api/status and returns the v0 response as a StatusResponseV0
+func GetStatusV0(socket string) (reply StatusV0, resp *APIResponse, err error) {
+	body, resp, err := GetRaw(socket, "GET", "/api/status")
+	if resp != nil || err != nil {
+		return reply, resp, err
+	}
+	err = json.Unmarshal(body, &reply)
+	if err != nil {
+		return reply, nil, err
+	}
+	return reply, nil, nil
+}
diff --git a/internal/common/compose_result.go b/internal/common/compose_result.go
index f49ff111a..c427e6e44 100644
--- a/internal/common/compose_result.go
+++ b/internal/common/compose_result.go
@@ -34,7 +34,7 @@ type ComposeResult struct {
 	Success bool `json:"success"`
 }
 
-func (cr *ComposeResult) Write(writer io.Writer) {
+func (cr *ComposeResult) Write(writer io.Writer) error {
 	if cr.Build == nil && len(cr.Stages) == 0 && cr.Assembler == nil {
 		fmt.Fprintf(writer, "The compose result is empty.\n")
 	}
@@ -46,7 +46,10 @@ func (cr *ComposeResult) Write(writer io.Writer) {
 			fmt.Fprintf(writer, "Stage %s\n", stage.Name)
 			enc := json.NewEncoder(writer)
 			enc.SetIndent("", "  ")
-			enc.Encode(stage.Options)
+			err := enc.Encode(stage.Options)
+			if err != nil {
+				return err
+			}
 			fmt.Fprintf(writer, "\nOutput:\n%s\n", stage.Output)
 		}
 	}
@@ -57,7 +60,10 @@ func (cr *ComposeResult) Write(writer io.Writer) {
 			fmt.Fprintf(writer, "Stage: %s\n", stage.Name)
 			enc := json.NewEncoder(writer)
 			enc.SetIndent("", "  ")
-			enc.Encode(stage.Options)
+			err := enc.Encode(stage.Options)
+			if err != nil {
+				return err
+			}
 			fmt.Fprintf(writer, "\nOutput:\n%s\n", stage.Output)
 		}
 	}
@@ -66,7 +72,12 @@ func (cr *ComposeResult) Write(writer io.Writer) {
 		fmt.Fprintf(writer, "Assembler %s:\n", cr.Assembler.Name)
 		enc := json.NewEncoder(writer)
 		enc.SetIndent("", "  ")
-		enc.Encode(cr.Assembler.Options)
+		err := enc.Encode(cr.Assembler.Options)
+		if err != nil {
+			return err
+		}
 		fmt.Fprintf(writer, "\nOutput:\n%s\n", cr.Assembler.Output)
 	}
+
+	return nil
 }
diff --git a/internal/common/helpers.go b/internal/common/helpers.go
index 81075224f..2f12b33fe 100644
--- a/internal/common/helpers.go
+++ b/internal/common/helpers.go
@@ -15,3 +15,9 @@ func CurrentArch() string {
 		panic("unsupported architecture")
 	}
 }
+
+func PanicOnError(err error) {
+	if err != nil {
+		panic(err)
+	}
+}
diff --git a/internal/common/types.go b/internal/common/types.go
index d62e4ea76..b517e9b36 100644
--- a/internal/common/types.go
+++ b/internal/common/types.go
@@ -57,7 +57,7 @@ func marshalHelper(input int, mapping map[string]int, errorMessage string) ([]by
 // See unmarshalHelper for introduction. This helper can create a list of possible values of a single type.
 func listHelper(mapping map[string]int) []string {
 	ret := make([]string, 0)
-	for k, _ := range mapping {
+	for k := range mapping {
 		ret = append(ret, k)
 	}
 	return ret
@@ -65,7 +65,7 @@ func listHelper(mapping map[string]int) []string {
 
 // See unmarshalHelper for introduction. With this helper one can make sure a value exists in a set of existing values.
 func existsHelper(mapping map[string]int, testedValue string) bool {
-	for k, _ := range mapping {
+	for k := range mapping {
 		if k == testedValue {
 			return true
 		}
@@ -247,6 +247,7 @@ const (
 	Fedora30 Distribution = iota
 	Fedora31
 	Fedora32
+	RHEL81
 	RHEL82
 )
 
@@ -258,6 +259,7 @@ func getDistributionMapping() map[string]int {
 		"fedora-30": int(Fedora30),
 		"fedora-31": int(Fedora31),
 		"fedora-32": int(Fedora32),
+		"rhel-8.1":  int(RHEL81),
 		"rhel-8.2":  int(RHEL82),
 	}
 	return mapping
@@ -269,6 +271,7 @@ func (dist *Distribution) ModulePlatformID() (string, error) {
 		Fedora30: "platform:f30",
 		Fedora31: "platform:f31",
 		Fedora32: "platform:f32",
+		RHEL81:   "platform:el8",
 		RHEL82:   "platform:el8",
 	}
 	id, exists := mapping[*dist]
diff --git a/internal/distro/distro.go b/internal/distro/distro.go
index 45fb64ded..a9d14d686 100644
--- a/internal/distro/distro.go
+++ b/internal/distro/distro.go
@@ -3,6 +3,7 @@ package distro
 import (
 	"bufio"
 	"errors"
+	"fmt"
 	"io"
 	"os"
 	"strings"
@@ -16,6 +17,7 @@ import (
 	"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"
 )
 
@@ -69,47 +71,43 @@ type Registry struct {
 	distros map[common.Distribution]Distro
 }
 
-func WithSingleDistro(dist Distro) *Registry {
+func NewRegistry(distros ...Distro) (*Registry, error) {
 	reg := &Registry{
 		distros: make(map[common.Distribution]Distro),
 	}
-	reg.register(dist)
-	return reg
+	for _, distro := range distros {
+		distroTag := distro.Distribution()
+		if _, exists := reg.distros[distroTag]; exists {
+			return nil, fmt.Errorf("NewRegistry: passed two distros with the same name: %s", distro.Name())
+		}
+		reg.distros[distroTag] = distro
+	}
+	return reg, nil
 }
 
-func NewRegistry(confPaths []string) *Registry {
-	distros := &Registry{
-		distros: make(map[common.Distribution]Distro),
-	}
-	f30 := fedora30.New(confPaths)
-	if f30 == nil {
-		panic("Attempt to register Fedora 30 failed")
+// Create a new Registry containing all known distros.
+func NewDefaultRegistry(confPaths []string) (*Registry, error) {
+	f30, err := fedora30.New(confPaths)
+	if err != nil {
+		return nil, fmt.Errorf("error loading fedora30: %v", err)
 	}
-	distros.register(f30)
-	f31 := fedora31.New(confPaths)
-	if f31 == nil {
-		panic("Attempt to register Fedora 31 failed")
+	f31, err := fedora31.New(confPaths)
+	if err != nil {
+		return nil, fmt.Errorf("error loading fedora31: %v", err)
 	}
-	distros.register(f31)
-	f32 := fedora32.New(confPaths)
-	if f32 == nil {
-		panic("Attempt to register Fedora 32 failed")
+	f32, err := fedora32.New(confPaths)
+	if err != nil {
+		return nil, fmt.Errorf("error loading fedora32: %v", err)
 	}
-	distros.register(f32)
-	el82 := rhel82.New(confPaths)
-	if el82 == nil {
-		panic("Attempt to register RHEL 8.2 failed")
+	el81, err := rhel81.New(confPaths)
+	if err != nil {
+		return nil, fmt.Errorf("error loading rhel81: %v", err)
 	}
-	distros.register(el82)
-	return distros
-}
-
-func (r *Registry) register(distro Distro) {
-	distroTag := distro.Distribution()
-	if _, exists := r.distros[distroTag]; exists {
-		panic("a distro with this name already exists: " + distro.Name())
+	el82, err := rhel82.New(confPaths)
+	if err != nil {
+		return nil, fmt.Errorf("error loading rhel82: %v", err)
 	}
-	r.distros[distroTag] = distro
+	return NewRegistry(f30, f31, f32, el81, el82)
 }
 
 func (r *Registry) GetDistro(name string) Distro {
diff --git a/internal/distro/fedora30/distro.go b/internal/distro/fedora30/distro.go
index 7387c95ee..c38130c2c 100644
--- a/internal/distro/fedora30/distro.go
+++ b/internal/distro/fedora30/distro.go
@@ -2,6 +2,7 @@ package fedora30
 
 import (
 	"errors"
+	"fmt"
 	"log"
 	"sort"
 	"strconv"
@@ -46,7 +47,7 @@ type output struct {
 const Distro = common.Fedora30
 const ModulePlatformID = "platform:f30"
 
-func New(confPaths []string) *Fedora30 {
+func New(confPaths []string) (*Fedora30, error) {
 	const GigaByte = 1024 * 1024 * 1024
 
 	r := Fedora30{
@@ -65,8 +66,7 @@ func New(confPaths []string) *Fedora30 {
 
 	repoMap, err := rpmmd.LoadRepositories(confPaths, r.Name())
 	if err != nil {
-		log.Printf("Could not load repository data for %s: %s", r.Name(), err.Error())
-		return nil
+		return nil, fmt.Errorf("Could not load repository data for %s: %s", r.Name(), err.Error())
 	}
 
 	repos, exists := repoMap["x86_64"]
@@ -307,7 +307,7 @@ func New(confPaths []string) *Fedora30 {
 		},
 	}
 
-	return &r
+	return &r, nil
 }
 
 func (r *Fedora30) Name() string {
diff --git a/internal/distro/fedora31/distro.go b/internal/distro/fedora31/distro.go
index 52eff793f..23e7bd36f 100644
--- a/internal/distro/fedora31/distro.go
+++ b/internal/distro/fedora31/distro.go
@@ -2,6 +2,7 @@ package fedora31
 
 import (
 	"errors"
+	"fmt"
 	"log"
 	"sort"
 	"strconv"
@@ -46,7 +47,7 @@ type output struct {
 const Distro = common.Fedora31
 const ModulePlatformID = "platform:f31"
 
-func New(confPaths []string) *Fedora31 {
+func New(confPaths []string) (*Fedora31, error) {
 	const GigaByte = 1024 * 1024 * 1024
 
 	r := Fedora31{
@@ -65,8 +66,7 @@ func New(confPaths []string) *Fedora31 {
 
 	repoMap, err := rpmmd.LoadRepositories(confPaths, r.Name())
 	if err != nil {
-		log.Printf("Could not load repository data for %s: %s", r.Name(), err.Error())
-		return nil
+		return nil, fmt.Errorf("Could not load repository data for %s: %s", r.Name(), err.Error())
 	}
 
 	repos, exists := repoMap["x86_64"]
@@ -307,7 +307,7 @@ func New(confPaths []string) *Fedora31 {
 		},
 	}
 
-	return &r
+	return &r, nil
 }
 
 func (r *Fedora31) Name() string {
diff --git a/internal/distro/fedora32/distro.go b/internal/distro/fedora32/distro.go
index d2a7b737e..56d237d48 100644
--- a/internal/distro/fedora32/distro.go
+++ b/internal/distro/fedora32/distro.go
@@ -2,6 +2,7 @@ package fedora32
 
 import (
 	"errors"
+	"fmt"
 	"log"
 	"sort"
 	"strconv"
@@ -46,7 +47,7 @@ type output struct {
 const Distro = common.Fedora32
 const ModulePlatformID = "platform:f32"
 
-func New(confPaths []string) *Fedora32 {
+func New(confPaths []string) (*Fedora32, error) {
 	const GigaByte = 1024 * 1024 * 1024
 
 	r := Fedora32{
@@ -65,8 +66,7 @@ func New(confPaths []string) *Fedora32 {
 
 	repoMap, err := rpmmd.LoadRepositories(confPaths, r.Name())
 	if err != nil {
-		log.Printf("Could not load repository data for %s: %s", r.Name(), err.Error())
-		return nil
+		return nil, fmt.Errorf("Could not load repository data for %s: %s", r.Name(), err.Error())
 	}
 
 	repos, exists := repoMap["x86_64"]
@@ -307,7 +307,7 @@ func New(confPaths []string) *Fedora32 {
 		},
 	}
 
-	return &r
+	return &r, nil
 }
 
 func (r *Fedora32) Name() string {
diff --git a/internal/distro/rhel81/distro.go b/internal/distro/rhel81/distro.go
new file mode 100644
index 000000000..397eadb22
--- /dev/null
+++ b/internal/distro/rhel81/distro.go
@@ -0,0 +1,858 @@
+package rhel81
+
+import (
+	"errors"
+	"fmt"
+	"log"
+	"sort"
+	"strconv"
+
+	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/osbuild"
+
+	"github.com/google/uuid"
+
+	"github.com/osbuild/osbuild-composer/internal/blueprint"
+	"github.com/osbuild/osbuild-composer/internal/crypt"
+	"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
+	BuildPackages      []string
+	UEFI               bool
+	Repositories       []rpmmd.RepoConfig
+}
+
+type output struct {
+	Name             string
+	MimeType         string
+	Packages         []string
+	ExcludedPackages []string
+	EnabledServices  []string
+	DisabledServices []string
+	Bootable         bool
+	DefaultTarget    string
+	KernelOptions    string
+	DefaultSize      uint64
+	Assembler        func(uefi bool, size uint64) *osbuild.Assembler
+}
+
+const Distro = common.RHEL81
+const ModulePlatformID = "platform:el8"
+
+func New(confPaths []string) (*RHEL81, error) {
+	const GigaByte = 1024 * 1024 * 1024
+
+	r := RHEL81{
+		arches:  map[string]arch{},
+		outputs: map[string]output{},
+		buildPackages: []string{
+			"dnf",
+			"dosfstools",
+			"dracut-config-generic",
+			"e2fsprogs",
+			"glibc",
+			"policycoreutils",
+			"python36",
+			"qemu-img",
+			"systemd",
+			"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): %s", r.Name(), err.Error())
+	} else {
+		r.arches["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): %s", r.Name(), err.Error())
+	} else {
+		r.arches["aarch64"] = arch{
+			Name: "aarch64",
+			BootloaderPackages: []string{
+				"dracut-config-generic",
+				"efibootmgr",
+				"grub2-efi-aa64",
+				"grub2-tools",
+				"shim-aa64",
+			},
+			UEFI:         true,
+			Repositories: repos,
+		}
+	}
+
+	r.outputs["ami"] = output{
+		Name:     "image.raw.xz",
+		MimeType: "application/octet-stream",
+		Packages: []string{
+			"checkpolicy",
+			"chrony",
+			"cloud-init",
+			"cloud-init",
+			"cloud-utils-growpart",
+			"@core",
+			"dhcp-client",
+			"dracut-config-generic",
+			"gdisk",
+			"insights-client",
+			"kernel",
+			"langpacks-en",
+			"net-tools",
+			"NetworkManager",
+			"redhat-release",
+			"redhat-release-eula",
+			"rng-tools",
+			"rsync",
+			"selinux-policy-targeted",
+			"tar",
+			"yum-utils",
+
+			// TODO this doesn't exist in BaseOS or AppStream
+			// "rh-amazon-rhui-client",
+		},
+		ExcludedPackages: []string{
+			"aic94xx-firmware",
+			"alsa-firmware",
+			"alsa-lib",
+			"alsa-tools-firmware",
+			"biosdevname",
+			"dracut-config-rescue",
+			"firewalld",
+			"iprutils",
+			"ivtv-firmware",
+			"iwl1000-firmware",
+			"iwl100-firmware",
+			"iwl105-firmware",
+			"iwl135-firmware",
+			"iwl2000-firmware",
+			"iwl2030-firmware",
+			"iwl3160-firmware",
+			"iwl3945-firmware",
+			"iwl4965-firmware",
+			"iwl5000-firmware",
+			"iwl5150-firmware",
+			"iwl6000-firmware",
+			"iwl6000g2a-firmware",
+			"iwl6000g2b-firmware",
+			"iwl6050-firmware",
+			"iwl7260-firmware",
+			"libertas-sd8686-firmware",
+			"libertas-sd8787-firmware",
+			"libertas-usb8388-firmware",
+			"plymouth",
+
+			// TODO this cannot be removed, because the kernel (?)
+			// depends on it. The ec2 kickstart force-removes it.
+			// "linux-firmware",
+
+			// TODO setfiles failes because of usr/sbin/timedatex. Exlude until
+			// https://errata.devel.redhat.com/advisory/47339 lands
+			"timedatex",
+		},
+		DefaultTarget: "multi-user.target",
+		Bootable:      true,
+		KernelOptions: "ro console=ttyS0,115200n8 console=tty0 net.ifnames=0 rd.blacklist=nouveau nvme_core.io_timeout=4294967295 crashkernel=auto",
+		DefaultSize:   6 * GigaByte,
+		Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
+			return r.qemuAssembler("raw.xz", "image.raw.xz", uefi, size)
+		},
+	}
+
+	r.outputs["ext4-filesystem"] = output{
+		Name:     "filesystem.img",
+		MimeType: "application/octet-stream",
+		Packages: []string{
+			"policycoreutils",
+			"selinux-policy-targeted",
+			"kernel",
+			"firewalld",
+			"chrony",
+			"dracut-config-generic",
+			"langpacks-en",
+		},
+		ExcludedPackages: []string{
+			"dracut-config-rescue",
+
+			// TODO setfiles failes because of usr/sbin/timedatex. Exlude until
+			// https://errata.devel.redhat.com/advisory/47339 lands
+			"timedatex",
+		},
+		Bootable:      false,
+		KernelOptions: "ro net.ifnames=0",
+		DefaultSize:   2 * GigaByte,
+		Assembler:     func(uefi bool, size uint64) *osbuild.Assembler { return r.rawFSAssembler("filesystem.img", size) },
+	}
+
+	r.outputs["partitioned-disk"] = output{
+		Name:     "disk.img",
+		MimeType: "application/octet-stream",
+		Packages: []string{
+			"@core",
+			"chrony",
+			"dracut-config-generic",
+			"firewalld",
+			"kernel",
+			"langpacks-en",
+			"selinux-policy-targeted",
+		},
+		ExcludedPackages: []string{
+			"dracut-config-rescue",
+
+			// TODO setfiles failes because of usr/sbin/timedatex. Exlude until
+			// https://errata.devel.redhat.com/advisory/47339 lands
+			"timedatex",
+		},
+		Bootable:      true,
+		KernelOptions: "ro net.ifnames=0",
+		DefaultSize:   2 * GigaByte,
+		Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
+			return r.qemuAssembler("raw", "disk.img", uefi, size)
+		},
+	}
+
+	r.outputs["qcow2"] = output{
+		Name:     "disk.qcow2",
+		MimeType: "application/x-qemu-disk",
+		Packages: []string{
+			"@core",
+			"chrony",
+			"dnf",
+			"kernel",
+			"yum",
+			"nfs-utils",
+			"dnf-utils",
+			"cloud-init",
+			"python3-jsonschema",
+			"qemu-guest-agent",
+			"cloud-utils-growpart",
+			"dracut-config-generic",
+			"dracut-norescue",
+			"tar",
+			"tcpdump",
+			"rsync",
+			"dnf-plugin-spacewalk",
+			"rhn-client-tools",
+			"rhnlib",
+			"rhnsd",
+			"rhn-setup",
+			"NetworkManager",
+			"dhcp-client",
+			"cockpit-ws",
+			"cockpit-system",
+			"subscription-manager-cockpit",
+			"redhat-release",
+			"redhat-release-eula",
+			"rng-tools",
+			"insights-client",
+			// TODO: rh-amazon-rhui-client
+		},
+		ExcludedPackages: []string{
+			"dracut-config-rescue",
+			"aic94xx-firmware",
+			"alsa-firmware",
+			"alsa-lib",
+			"alsa-tools-firmware",
+			"firewalld",
+			"ivtv-firmware",
+			"iwl1000-firmware",
+			"iwl100-firmware",
+			"iwl105-firmware",
+			"iwl135-firmware",
+			"iwl2000-firmware",
+			"iwl2030-firmware",
+			"iwl3160-firmware",
+			"iwl3945-firmware",
+			"iwl4965-firmware",
+			"iwl5000-firmware",
+			"iwl5150-firmware",
+			"iwl6000-firmware",
+			"iwl6000g2a-firmware",
+			"iwl6000g2b-firmware",
+			"iwl6050-firmware",
+			"iwl7260-firmware",
+			"libertas-sd8686-firmware",
+			"libertas-sd8787-firmware",
+			"libertas-usb8388-firmware",
+			"langpacks-*",
+			"langpacks-en",
+			"biosdevname",
+			"plymouth",
+			"iprutils",
+			"langpacks-en",
+			"fedora-release",
+			"fedora-repos",
+
+			// TODO setfiles failes because of usr/sbin/timedatex. Exlude until
+			// https://errata.devel.redhat.com/advisory/47339 lands
+			"timedatex",
+		},
+		Bootable:      true,
+		KernelOptions: "console=ttyS0 console=ttyS0,115200n8 no_timer_check crashkernel=auto net.ifnames=0",
+		DefaultSize:   2 * GigaByte,
+		Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
+			return r.qemuAssembler("qcow2", "disk.qcow2", uefi, size)
+		},
+	}
+
+	r.outputs["openstack"] = output{
+		Name:     "disk.qcow2",
+		MimeType: "application/x-qemu-disk",
+		Packages: []string{
+			// Defaults
+			"@Core",
+			"langpacks-en",
+
+			// Don't run dracut in host-only mode, in order to pull in
+			// the hv_vmbus, hv_netvsc and hv_storvsc modules into the initrd.
+			"dracut-config-generic",
+
+			// From the lorax kickstart
+			"kernel",
+			"selinux-policy-targeted",
+			"cloud-init",
+			"qemu-guest-agent",
+			"spice-vdagent",
+		},
+		ExcludedPackages: []string{
+			"dracut-config-rescue",
+		},
+		Bootable:      true,
+		KernelOptions: "ro net.ifnames=0",
+		DefaultSize:   2 * GigaByte,
+		Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
+			return r.qemuAssembler("qcow2", "disk.qcow2", uefi, size)
+		},
+	}
+
+	r.outputs["tar"] = output{
+		Name:     "root.tar.xz",
+		MimeType: "application/x-tar",
+		Packages: []string{
+			"policycoreutils",
+			"selinux-policy-targeted",
+			"kernel",
+			"firewalld",
+			"chrony",
+			"dracut-config-generic",
+			"langpacks-en",
+		},
+		ExcludedPackages: []string{
+			"dracut-config-rescue",
+
+			// TODO setfiles failes because of usr/sbin/timedatex. Exlude until
+			// https://errata.devel.redhat.com/advisory/47339 lands
+			"timedatex",
+		},
+		Bootable:      false,
+		KernelOptions: "ro net.ifnames=0",
+		Assembler:     func(uefi bool, size uint64) *osbuild.Assembler { return r.tarAssembler("root.tar.xz", "xz") },
+	}
+
+	r.outputs["vhd"] = output{
+		Name:     "disk.vhd",
+		MimeType: "application/x-vhd",
+		Packages: []string{
+			// Defaults
+			"@Core",
+			"langpacks-en",
+
+			// Don't run dracut in host-only mode, in order to pull in
+			// the hv_vmbus, hv_netvsc and hv_storvsc modules into the initrd.
+			"dracut-config-generic",
+
+			// From the lorax kickstart
+			"kernel",
+			"selinux-policy-targeted",
+			"chrony",
+			"WALinuxAgent",
+			"python3",
+			"net-tools",
+			"cloud-init",
+			"cloud-utils-growpart",
+			"gdisk",
+		},
+		ExcludedPackages: []string{
+			"dracut-config-rescue",
+
+			// TODO setfiles failes because of usr/sbin/timedatex. Exlude until
+			// https://errata.devel.redhat.com/advisory/47339 lands
+			"timedatex",
+		},
+		EnabledServices: []string{
+			"sshd",
+			"waagent",
+		},
+		DefaultTarget: "multi-user.target",
+		Bootable:      true,
+		KernelOptions: "ro biosdevname=0 rootdelay=300 console=ttyS0 earlyprintk=ttyS0 net.ifnames=0",
+		DefaultSize:   2 * GigaByte,
+		Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
+			return r.qemuAssembler("vpc", "disk.vhd", uefi, size)
+		},
+	}
+
+	r.outputs["vmdk"] = output{
+		Name:     "disk.vmdk",
+		MimeType: "application/x-vmdk",
+		Packages: []string{
+			"@core",
+			"chrony",
+			"dracut-config-generic",
+			"firewalld",
+			"kernel",
+			"langpacks-en",
+			"open-vm-tools",
+			"selinux-policy-targeted",
+		},
+		ExcludedPackages: []string{
+			"dracut-config-rescue",
+
+			// TODO setfiles failes because of usr/sbin/timedatex. Exlude until
+			// https://errata.devel.redhat.com/advisory/47339 lands
+			"timedatex",
+		},
+		Bootable:      true,
+		KernelOptions: "ro net.ifnames=0",
+		DefaultSize:   2 * GigaByte,
+		Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
+			return r.qemuAssembler("vmdk", "disk.vmdk", uefi, size)
+		},
+	}
+
+	return &r, nil
+}
+
+func (r *RHEL81) Name() string {
+	name, exists := Distro.ToString()
+	if !exists {
+		panic("Fatal error, hardcoded distro value in rhel81 package is not valid!")
+	}
+	return name
+}
+
+func (r *RHEL81) Distribution() common.Distribution {
+	return Distro
+}
+
+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 {
+		formats = append(formats, name)
+	}
+	sort.Strings(formats)
+	return formats
+}
+
+func (r *RHEL81) FilenameFromType(outputFormat string) (string, string, error) {
+	if output, exists := r.outputs[outputFormat]; exists {
+		return output.Name, output.MimeType, nil
+	}
+	return "", "", errors.New("invalid output format: " + outputFormat)
+}
+
+func (r *RHEL81) GetSizeForOutputType(outputFormat string, size uint64) uint64 {
+	const MegaByte = 1024 * 1024
+	// Microsoft Azure requires vhd images to be rounded up to the nearest MB
+	if outputFormat == "vhd" && size%MegaByte != 0 {
+		size = (size/MegaByte + 1) * MegaByte
+	}
+	if size == 0 {
+		size = r.outputs[outputFormat].DefaultSize
+	}
+	return size
+}
+
+func (r *RHEL81) BasePackages(outputFormat string, outputArchitecture string) ([]string, []string, error) {
+	output, exists := r.outputs[outputFormat]
+	if !exists {
+		return nil, nil, errors.New("invalid output format: " + outputFormat)
+	}
+
+	packages := output.Packages
+	if output.Bootable {
+		arch, exists := r.arches[outputArchitecture]
+		if !exists {
+			return nil, nil, errors.New("invalid architecture: " + outputArchitecture)
+		}
+
+		packages = append(packages, arch.BootloaderPackages...)
+	}
+
+	return packages, output.ExcludedPackages, nil
+}
+
+func (r *RHEL81) BuildPackages(outputArchitecture string) ([]string, error) {
+	arch, exists := r.arches[outputArchitecture]
+	if !exists {
+		return nil, errors.New("invalid architecture: " + outputArchitecture)
+	}
+
+	return append(r.buildPackages, arch.BuildPackages...), nil
+}
+
+func (r *RHEL81) Pipeline(b *blueprint.Blueprint, additionalRepos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, checksums map[string]string, outputArchitecture, outputFormat string, size uint64) (*osbuild.Pipeline, error) {
+	output, exists := r.outputs[outputFormat]
+	if !exists {
+		return nil, errors.New("invalid output format: " + outputFormat)
+	}
+
+	arch, exists := r.arches[outputArchitecture]
+	if !exists {
+		return nil, errors.New("invalid architecture: " + outputArchitecture)
+	}
+
+	p := &osbuild.Pipeline{}
+	p.SetBuild(r.buildPipeline(arch, checksums), "org.osbuild.rhel81")
+
+	packages, excludedPackages, err := r.BasePackages(outputFormat, outputArchitecture)
+	if err != nil {
+		return nil, err
+	}
+	packages = append(packages, b.GetPackages()...)
+	p.AddStage(osbuild.NewDNFStage(r.dnfStageOptions(arch, additionalRepos, checksums, packages, excludedPackages)))
+	p.AddStage(osbuild.NewFixBLSStage())
+
+	if output.Bootable {
+		p.AddStage(osbuild.NewFSTabStage(r.fsTabStageOptions(arch.UEFI)))
+	}
+
+	kernelOptions := output.KernelOptions
+	if kernel := b.GetKernel(); kernel != nil {
+		kernelOptions += " " + kernel.Append
+	}
+	p.AddStage(osbuild.NewGRUB2Stage(r.grub2StageOptions(kernelOptions, arch.UEFI)))
+
+	// TODO support setting all languages and install corresponding langpack-* package
+	language, keyboard := b.GetPrimaryLocale()
+
+	if language != nil {
+		p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{*language}))
+	} else {
+		p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{"en_US"}))
+	}
+
+	if keyboard != nil {
+		p.AddStage(osbuild.NewKeymapStage(&osbuild.KeymapStageOptions{*keyboard}))
+	}
+
+	if hostname := b.GetHostname(); hostname != nil {
+		p.AddStage(osbuild.NewHostnameStage(&osbuild.HostnameStageOptions{*hostname}))
+	}
+
+	timezone, ntpServers := b.GetTimezoneSettings()
+
+	// TODO install chrony when this is set?
+	if timezone != nil {
+		p.AddStage(osbuild.NewTimezoneStage(&osbuild.TimezoneStageOptions{*timezone}))
+	}
+
+	if len(ntpServers) > 0 {
+		p.AddStage(osbuild.NewChronyStage(&osbuild.ChronyStageOptions{ntpServers}))
+	}
+
+	if users := b.GetUsers(); len(users) > 0 {
+		options, err := r.userStageOptions(users)
+		if err != nil {
+			return nil, err
+		}
+		p.AddStage(osbuild.NewUsersStage(options))
+	}
+
+	if groups := b.GetGroups(); len(groups) > 0 {
+		p.AddStage(osbuild.NewGroupsStage(r.groupStageOptions(groups)))
+	}
+
+	if services := b.GetServices(); services != nil || output.EnabledServices != nil {
+		p.AddStage(osbuild.NewSystemdStage(r.systemdStageOptions(output.EnabledServices, output.DisabledServices, services, output.DefaultTarget)))
+	}
+
+	if firewall := b.GetFirewall(); firewall != nil {
+		p.AddStage(osbuild.NewFirewallStage(r.firewallStageOptions(firewall)))
+	}
+
+	p.AddStage(osbuild.NewSELinuxStage(r.selinuxStageOptions()))
+
+	p.Assembler = output.Assembler(arch.UEFI, size)
+
+	return p, nil
+}
+
+func (r *RHEL81) Sources(packages []rpmmd.PackageSpec) *osbuild.Sources {
+	return &osbuild.Sources{}
+}
+
+func (r *RHEL81) Runner() string {
+	return "org.osbuild.rhel81"
+}
+
+func (r *RHEL81) buildPipeline(arch arch, checksums map[string]string) *osbuild.Pipeline {
+	packages, err := r.BuildPackages(arch.Name)
+	if err != nil {
+		panic("impossibly invalid arch")
+	}
+
+	p := &osbuild.Pipeline{}
+	p.AddStage(osbuild.NewDNFStage(r.dnfStageOptions(arch, nil, checksums, packages, nil)))
+	return p
+}
+
+func (r *RHEL81) dnfStageOptions(arch arch, additionalRepos []rpmmd.RepoConfig, checksums map[string]string, packages, excludedPackages []string) *osbuild.DNFStageOptions {
+	options := &osbuild.DNFStageOptions{
+		ReleaseVersion:   "8",
+		BaseArchitecture: arch.Name,
+		ModulePlatformId: ModulePlatformID,
+	}
+	for _, repo := range append(arch.Repositories, additionalRepos...) {
+		options.AddRepository(&osbuild.DNFRepository{
+			BaseURL:    repo.BaseURL,
+			MetaLink:   repo.Metalink,
+			MirrorList: repo.MirrorList,
+			Checksum:   checksums[repo.Id],
+		})
+	}
+
+	sort.Strings(packages)
+	for _, pkg := range packages {
+		options.AddPackage(pkg)
+	}
+
+	sort.Strings(excludedPackages)
+	for _, pkg := range excludedPackages {
+		options.ExcludePackage(pkg)
+	}
+
+	return options
+}
+
+func (r *RHEL81) userStageOptions(users []blueprint.UserCustomization) (*osbuild.UsersStageOptions, error) {
+	options := osbuild.UsersStageOptions{
+		Users: make(map[string]osbuild.UsersStageOptionsUser),
+	}
+
+	for _, c := range users {
+		if c.Password != nil && !crypt.PasswordIsCrypted(*c.Password) {
+			cryptedPassword, err := crypt.CryptSHA512(*c.Password)
+			if err != nil {
+				return nil, err
+			}
+
+			c.Password = &cryptedPassword
+		}
+
+		user := osbuild.UsersStageOptionsUser{
+			Groups:      c.Groups,
+			Description: c.Description,
+			Home:        c.Home,
+			Shell:       c.Shell,
+			Password:    c.Password,
+			Key:         c.Key,
+		}
+
+		if c.UID != nil {
+			uid := strconv.Itoa(*c.UID)
+			user.UID = &uid
+		}
+
+		if c.GID != nil {
+			gid := strconv.Itoa(*c.GID)
+			user.GID = &gid
+		}
+
+		options.Users[c.Name] = user
+	}
+
+	return &options, nil
+}
+
+func (r *RHEL81) groupStageOptions(groups []blueprint.GroupCustomization) *osbuild.GroupsStageOptions {
+	options := osbuild.GroupsStageOptions{
+		Groups: map[string]osbuild.GroupsStageOptionsGroup{},
+	}
+
+	for _, group := range groups {
+		groupData := osbuild.GroupsStageOptionsGroup{
+			Name: group.Name,
+		}
+		if group.GID != nil {
+			gid := strconv.Itoa(*group.GID)
+			groupData.GID = &gid
+		}
+
+		options.Groups[group.Name] = groupData
+	}
+
+	return &options
+}
+
+func (r *RHEL81) firewallStageOptions(firewall *blueprint.FirewallCustomization) *osbuild.FirewallStageOptions {
+	options := osbuild.FirewallStageOptions{
+		Ports: firewall.Ports,
+	}
+
+	if firewall.Services != nil {
+		options.EnabledServices = firewall.Services.Enabled
+		options.DisabledServices = firewall.Services.Disabled
+	}
+
+	return &options
+}
+
+func (r *RHEL81) systemdStageOptions(enabledServices, disabledServices []string, s *blueprint.ServicesCustomization, target string) *osbuild.SystemdStageOptions {
+	if s != nil {
+		enabledServices = append(enabledServices, s.Enabled...)
+		disabledServices = append(disabledServices, s.Disabled...)
+	}
+	return &osbuild.SystemdStageOptions{
+		EnabledServices:  enabledServices,
+		DisabledServices: disabledServices,
+		DefaultTarget:    target,
+	}
+}
+
+func (r *RHEL81) fsTabStageOptions(uefi bool) *osbuild.FSTabStageOptions {
+	options := osbuild.FSTabStageOptions{}
+	options.AddFilesystem("0bd700f8-090f-4556-b797-b340297ea1bd", "xfs", "/", "defaults", 0, 0)
+	if uefi {
+		options.AddFilesystem("46BB-8120", "vfat", "/boot/efi", "umask=0077,shortname=winnt", 0, 2)
+	}
+	return &options
+}
+
+func (r *RHEL81) grub2StageOptions(kernelOptions string, uefi bool) *osbuild.GRUB2StageOptions {
+	id, err := uuid.Parse("0bd700f8-090f-4556-b797-b340297ea1bd")
+	if err != nil {
+		panic("invalid UUID")
+	}
+
+	var uefiOptions *osbuild.GRUB2UEFI
+	if uefi {
+		uefiOptions = &osbuild.GRUB2UEFI{
+			Vendor: "redhat",
+		}
+	}
+
+	return &osbuild.GRUB2StageOptions{
+		RootFilesystemUUID: id,
+		KernelOptions:      kernelOptions,
+		Legacy:             !uefi,
+		UEFI:               uefiOptions,
+	}
+}
+
+func (r *RHEL81) selinuxStageOptions() *osbuild.SELinuxStageOptions {
+	return &osbuild.SELinuxStageOptions{
+		FileContexts: "etc/selinux/targeted/contexts/files/file_contexts",
+	}
+}
+
+func (r *RHEL81) qemuAssembler(format string, filename string, uefi bool, size uint64) *osbuild.Assembler {
+	var options osbuild.QEMUAssemblerOptions
+	if uefi {
+		fstype := uuid.MustParse("C12A7328-F81F-11D2-BA4B-00A0C93EC93B")
+		options = osbuild.QEMUAssemblerOptions{
+			Format:   format,
+			Filename: filename,
+			Size:     size,
+			PTUUID:   "8DFDFF87-C96E-EA48-A3A6-9408F1F6B1EF",
+			PTType:   "gpt",
+			Partitions: []osbuild.QEMUPartition{
+				{
+					Start: 2048,
+					Size:  972800,
+					Type:  &fstype,
+					Filesystem: osbuild.QEMUFilesystem{
+						Type:       "vfat",
+						UUID:       "46BB-8120",
+						Label:      "EFI System Partition",
+						Mountpoint: "/boot/efi",
+					},
+				},
+				{
+					Start: 976896,
+					Filesystem: osbuild.QEMUFilesystem{
+						Type:       "xfs",
+						UUID:       "0bd700f8-090f-4556-b797-b340297ea1bd",
+						Mountpoint: "/",
+					},
+				},
+			},
+		}
+	} else {
+		options = osbuild.QEMUAssemblerOptions{
+			Format:   format,
+			Filename: filename,
+			Size:     size,
+			PTUUID:   "0x14fc63d2",
+			PTType:   "mbr",
+			Partitions: []osbuild.QEMUPartition{
+				{
+					Start:    2048,
+					Bootable: true,
+					Filesystem: osbuild.QEMUFilesystem{
+						Type:       "xfs",
+						UUID:       "0bd700f8-090f-4556-b797-b340297ea1bd",
+						Mountpoint: "/",
+					},
+				},
+			},
+		}
+	}
+	return osbuild.NewQEMUAssembler(&options)
+}
+
+func (r *RHEL81) tarAssembler(filename, compression string) *osbuild.Assembler {
+	return osbuild.NewTarAssembler(
+		&osbuild.TarAssemblerOptions{
+			Filename:    filename,
+			Compression: compression,
+		})
+}
+
+func (r *RHEL81) rawFSAssembler(filename string, size uint64) *osbuild.Assembler {
+	id, err := uuid.Parse("0bd700f8-090f-4556-b797-b340297ea1bd")
+	if err != nil {
+		panic("invalid UUID")
+	}
+	return osbuild.NewRawFSAssembler(
+		&osbuild.RawFSAssemblerOptions{
+			Filename:           filename,
+			RootFilesystemUUDI: id,
+			Size:               size,
+			FilesystemType:     "xfs",
+		})
+}
diff --git a/internal/distro/rhel82/distro.go b/internal/distro/rhel82/distro.go
index 657e97fb2..934a1dd9e 100644
--- a/internal/distro/rhel82/distro.go
+++ b/internal/distro/rhel82/distro.go
@@ -2,6 +2,7 @@ package rhel82
 
 import (
 	"errors"
+	"fmt"
 	"log"
 	"sort"
 	"strconv"
@@ -47,7 +48,7 @@ type output struct {
 const Distro = common.RHEL82
 const ModulePlatformID = "platform:el8"
 
-func New(confPaths []string) *RHEL82 {
+func New(confPaths []string) (*RHEL82, error) {
 	const GigaByte = 1024 * 1024 * 1024
 
 	r := RHEL82{
@@ -70,8 +71,7 @@ func New(confPaths []string) *RHEL82 {
 
 	repoMap, err := rpmmd.LoadRepositories(confPaths, r.Name())
 	if err != nil {
-		log.Printf("Could not load repository data for %s: %s", r.Name(), err.Error())
-		return nil
+		return nil, fmt.Errorf("Could not load repository data for %s: %s", r.Name(), err.Error())
 	}
 
 	repos, exists := repoMap["x86_64"]
@@ -446,7 +446,7 @@ func New(confPaths []string) *RHEL82 {
 		},
 	}
 
-	return &r
+	return &r, nil
 }
 
 func (r *RHEL82) Name() string {
diff --git a/internal/jobqueue/api.go b/internal/jobqueue/api.go
index de3b001fd..f588054f8 100644
--- a/internal/jobqueue/api.go
+++ b/internal/jobqueue/api.go
@@ -74,7 +74,10 @@ func statusResponseError(writer http.ResponseWriter, code int, errors ...string)
 	writer.WriteHeader(code)
 
 	for _, err := range errors {
-		writer.Write([]byte(err))
+		_, e := writer.Write([]byte(err))
+		if e != nil {
+			panic(e)
+		}
 	}
 }
 
diff --git a/internal/jobqueue/job.go b/internal/jobqueue/job.go
index 8f8ad8172..de3f91eaa 100644
--- a/internal/jobqueue/job.go
+++ b/internal/jobqueue/job.go
@@ -46,7 +46,11 @@ func (e *TargetsError) Error() string {
 }
 
 func (job *Job) Run(uploader LocalTargetUploader) (*common.ComposeResult, error) {
-	distros := distro.NewRegistry([]string{"/etc/osbuild-composer", "/usr/share/osbuild-composer"})
+	distros, err := distro.NewDefaultRegistry([]string{"/etc/osbuild-composer", "/usr/share/osbuild-composer"})
+	if err != nil {
+		return nil, fmt.Errorf("error loading distros: %v", err)
+	}
+
 	d := distros.GetDistro(job.Distro)
 	if d == nil {
 		return nil, fmt.Errorf("unknown distro: %s", job.Distro)
@@ -175,10 +179,3 @@ func (job *Job) Run(uploader LocalTargetUploader) (*common.ComposeResult, error)
 
 	return &result, nil
 }
-
-func runCommand(command string, params ...string) error {
-	cp := exec.Command(command, params...)
-	cp.Stderr = os.Stderr
-	cp.Stdout = os.Stdout
-	return cp.Run()
-}
diff --git a/internal/mocks/distro/distro_mock.go b/internal/mocks/distro/distro_mock.go
index 178ee3e93..ce65d5005 100644
--- a/internal/mocks/distro/distro_mock.go
+++ b/internal/mocks/distro/distro_mock.go
@@ -5,10 +5,10 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/distro/fedoratest"
 )
 
-func NewRegistry() *distro.Registry {
+func NewDefaultRegistry() (*distro.Registry, error) {
 	ftest := fedoratest.New()
 	if ftest == nil {
 		panic("Attempt to register Fedora test failed")
 	}
-	return distro.WithSingleDistro(ftest)
+	return distro.NewRegistry(ftest)
 }
diff --git a/internal/mocks/rpmmd/fixtures.go b/internal/mocks/rpmmd/fixtures.go
index 7b8c2f7ca..34c17f77f 100644
--- a/internal/mocks/rpmmd/fixtures.go
+++ b/internal/mocks/rpmmd/fixtures.go
@@ -94,7 +94,7 @@ func createBaseStoreFixture() *store.Store {
 	}
 
 	d := test_distro.New()
-	r := distro_mock.NewRegistry()
+	r, _ := distro_mock.NewDefaultRegistry()
 	s := store.New(nil, d, *r)
 
 	s.Blueprints[bName] = b
@@ -191,7 +191,7 @@ func createStoreWithoutComposesFixture() *store.Store {
 	}
 
 	d := test_distro.New()
-	r := distro_mock.NewRegistry()
+	r, _ := distro_mock.NewDefaultRegistry()
 	s := store.New(nil, d, *r)
 
 	s.Blueprints[bName] = b
diff --git a/internal/rpmmd/repository.go b/internal/rpmmd/repository.go
index c417fba44..baff1b53c 100644
--- a/internal/rpmmd/repository.go
+++ b/internal/rpmmd/repository.go
@@ -6,7 +6,6 @@ import (
 	"io/ioutil"
 	"os"
 	"os/exec"
-	"path"
 	"sort"
 	"time"
 
@@ -211,22 +210,22 @@ func runDNF(command string, arguments interface{}, result interface{}) error {
 	return nil
 }
 
-type rpmmdImpl struct{}
-
-func NewRPMMD() RPMMD {
-	return &rpmmdImpl{}
+type rpmmdImpl struct {
+	CacheDir string
 }
 
-func (*rpmmdImpl) FetchMetadata(repos []RepoConfig, modulePlatformID string) (PackageList, map[string]string, error) {
-	cacheDirectory, ok := os.LookupEnv("CACHE_DIRECTORY")
-	if !ok {
-		panic("CACHE_DIRECTORY is not set. Is the service file missing CacheDirectory=?")
+func NewRPMMD(cacheDir string) RPMMD {
+	return &rpmmdImpl{
+		CacheDir: cacheDir,
 	}
+}
+
+func (r *rpmmdImpl) FetchMetadata(repos []RepoConfig, modulePlatformID string) (PackageList, map[string]string, error) {
 	var arguments = struct {
 		Repos            []RepoConfig `json:"repos"`
 		CacheDir         string       `json:"cachedir"`
 		ModulePlatformID string       `json:"module_platform_id"`
-	}{repos, path.Join(cacheDirectory, "rpmmd"), modulePlatformID}
+	}{repos, r.CacheDir, modulePlatformID}
 	var reply struct {
 		Checksums map[string]string `json:"checksums"`
 		Packages  PackageList       `json:"packages"`
@@ -238,11 +237,7 @@ func (*rpmmdImpl) FetchMetadata(repos []RepoConfig, modulePlatformID string) (Pa
 	return reply.Packages, reply.Checksums, err
 }
 
-func (*rpmmdImpl) Depsolve(specs, excludeSpecs []string, repos []RepoConfig, modulePlatformID string, clean bool) ([]PackageSpec, map[string]string, error) {
-	cacheDirectory, ok := os.LookupEnv("CACHE_DIRECTORY")
-	if !ok {
-		panic("CACHE_DIRECTORY is not set. Is the service file missing CacheDirectory=?")
-	}
+func (r *rpmmdImpl) Depsolve(specs, excludeSpecs []string, repos []RepoConfig, modulePlatformID string, clean bool) ([]PackageSpec, map[string]string, error) {
 	var arguments = struct {
 		PackageSpecs     []string     `json:"package-specs"`
 		ExcludSpecs      []string     `json:"exclude-specs"`
@@ -250,7 +245,7 @@ func (*rpmmdImpl) Depsolve(specs, excludeSpecs []string, repos []RepoConfig, mod
 		CacheDir         string       `json:"cachedir"`
 		ModulePlatformID string       `json:"module_platform_id"`
 		Clean            bool         `json:"clean,omitempty"`
-	}{specs, excludeSpecs, repos, path.Join(cacheDirectory, "rpmmd"), modulePlatformID, clean}
+	}{specs, excludeSpecs, repos, r.CacheDir, modulePlatformID, clean}
 	var reply struct {
 		Checksums    map[string]string `json:"checksums"`
 		Dependencies []PackageSpec     `json:"dependencies"`
diff --git a/internal/store/store.go b/internal/store/store.go
index 8afeca2f7..661a2fa0b 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -243,7 +243,10 @@ func randomSHA1String() (string, error) {
 	} else if n != 20 {
 		return "", errors.New("randomSHA1String: short read from rand")
 	}
-	hash.Write(data)
+	_, err = hash.Write(data)
+	if err != nil {
+		return "", err
+	}
 	return hex.EncodeToString(hash.Sum(nil)), nil
 }
 
diff --git a/internal/upload/azure/azure.go b/internal/upload/azure/azure.go
index 88f07ea9a..713748030 100644
--- a/internal/upload/azure/azure.go
+++ b/internal/upload/azure/azure.go
@@ -151,7 +151,7 @@ func UploadImage(credentials Credentials, metadata ImageMetadata, fileName strin
 	var blobChecksum []byte = props.ContentMD5()
 	var fileChecksum []byte = imageFileHash.Sum(nil)
 
-	if bytes.Compare(blobChecksum, fileChecksum) != 0 {
+	if !bytes.Equal(blobChecksum, fileChecksum) {
 		return &errorString{"error during image upload. the image seems to be corrupted"}
 	}
 
diff --git a/internal/weldr/api.go b/internal/weldr/api.go
index 40e461e9f..05200d715 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -153,9 +153,7 @@ func isRequestVersionAtLeast(params httprouter.Params, minVersion uint) bool {
 
 	version, err := strconv.ParseUint(versionString, 10, 0)
 
-	if err != nil {
-		panic(err)
-	}
+	common.PanicOnError(err)
 
 	return uint(version) >= minVersion
 }
@@ -178,7 +176,8 @@ func statusResponseOK(writer http.ResponseWriter) {
 	}
 
 	writer.WriteHeader(http.StatusOK)
-	json.NewEncoder(writer).Encode(reply{true})
+	err := json.NewEncoder(writer).Encode(reply{true})
+	common.PanicOnError(err)
 }
 
 type responseError struct {
@@ -194,7 +193,8 @@ func statusResponseError(writer http.ResponseWriter, code int, errors ...respons
 	}
 
 	writer.WriteHeader(code)
-	json.NewEncoder(writer).Encode(reply{false, errors})
+	err := json.NewEncoder(writer).Encode(reply{false, errors})
+	common.PanicOnError(err)
 }
 
 func (api *API) statusHandler(writer http.ResponseWriter, request *http.Request, _ httprouter.Params) {
@@ -208,7 +208,7 @@ func (api *API) statusHandler(writer http.ResponseWriter, request *http.Request,
 		Messages      []string `json:"messages"`
 	}
 
-	json.NewEncoder(writer).Encode(reply{
+	err := json.NewEncoder(writer).Encode(reply{
 		API:           "1",
 		DBSupported:   true,
 		DBVersion:     "0",
@@ -217,6 +217,7 @@ func (api *API) statusHandler(writer http.ResponseWriter, request *http.Request,
 		Build:         "devel",
 		Messages:      make([]string, 0),
 	})
+	common.PanicOnError(err)
 }
 
 func (api *API) sourceListHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -234,9 +235,10 @@ func (api *API) sourceListHandler(writer http.ResponseWriter, request *http.Requ
 		names = append(names, repo.Id)
 	}
 
-	json.NewEncoder(writer).Encode(reply{
+	err := json.NewEncoder(writer).Encode(reply{
 		Sources: names,
 	})
+	common.PanicOnError(err)
 }
 
 func (api *API) sourceEmptyInfoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -314,14 +316,16 @@ func (api *API) sourceInfoHandler(writer http.ResponseWriter, request *http.Requ
 
 	format := q.Get("format")
 	if format == "json" || format == "" {
-		json.NewEncoder(writer).Encode(reply{
+		err := json.NewEncoder(writer).Encode(reply{
 			Sources: sources,
 			Errors:  errors,
 		})
+		common.PanicOnError(err)
 	} else if format == "toml" {
 		encoder := toml.NewEncoder(writer)
 		encoder.Indent = ""
-		encoder.Encode(sources)
+		err := encoder.Encode(sources)
+		common.PanicOnError(err)
 	} else {
 		errors := responseError{
 			ID:  "InvalidChars",
@@ -480,12 +484,13 @@ func (api *API) modulesListHandler(writer http.ResponseWriter, request *http.Req
 		modules[i] = module{packageInfos[start+i].Name, "rpm"}
 	}
 
-	json.NewEncoder(writer).Encode(reply{
+	err = json.NewEncoder(writer).Encode(reply{
 		Total:   total,
 		Offset:  offset,
 		Limit:   limit,
 		Modules: modules,
 	})
+	common.PanicOnError(err)
 }
 
 func (api *API) projectsListHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -532,12 +537,13 @@ func (api *API) projectsListHandler(writer http.ResponseWriter, request *http.Re
 		packages[i] = packageInfos[start+i]
 	}
 
-	json.NewEncoder(writer).Encode(reply{
+	err = json.NewEncoder(writer).Encode(reply{
 		Total:    total,
 		Offset:   offset,
 		Limit:    limit,
 		Projects: packages,
 	})
+	common.PanicOnError(err)
 }
 
 func (api *API) modulesInfoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -614,7 +620,7 @@ func (api *API) modulesInfoHandler(writer http.ResponseWriter, request *http.Req
 	packageInfos := foundPackages.ToPackageInfos()
 
 	if modulesRequested {
-		for i, _ := range packageInfos {
+		for i := range packageInfos {
 			err := packageInfos[i].FillDependencies(api.rpmmd, api.distro.Repositories(api.arch), api.distro.ModulePlatformID())
 			if err != nil {
 				errors := responseError{
@@ -628,10 +634,11 @@ func (api *API) modulesInfoHandler(writer http.ResponseWriter, request *http.Req
 	}
 
 	if modulesRequested {
-		json.NewEncoder(writer).Encode(modulesReply{packageInfos})
+		err = json.NewEncoder(writer).Encode(modulesReply{packageInfos})
 	} else {
-		json.NewEncoder(writer).Encode(projectsReply{packageInfos})
+		err = json.NewEncoder(writer).Encode(projectsReply{packageInfos})
 	}
+	common.PanicOnError(err)
 }
 
 func (api *API) projectsDepsolveHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -656,9 +663,10 @@ func (api *API) projectsDepsolveHandler(writer http.ResponseWriter, request *htt
 		return
 	}
 
-	json.NewEncoder(writer).Encode(reply{
+	err = json.NewEncoder(writer).Encode(reply{
 		Projects: packages,
 	})
+	common.PanicOnError(err)
 }
 
 func (api *API) blueprintsListHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -688,12 +696,13 @@ func (api *API) blueprintsListHandler(writer http.ResponseWriter, request *http.
 	offset = min(offset, total)
 	limit = min(limit, total-offset)
 
-	json.NewEncoder(writer).Encode(reply{
+	err = json.NewEncoder(writer).Encode(reply{
 		Total:      total,
 		Offset:     offset,
 		Limit:      limit,
 		Blueprints: names[offset : offset+limit],
 	})
+	common.PanicOnError(err)
 }
 
 func (api *API) blueprintsInfoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -756,11 +765,12 @@ func (api *API) blueprintsInfoHandler(writer http.ResponseWriter, request *http.
 
 	format := q.Get("format")
 	if format == "json" || format == "" {
-		json.NewEncoder(writer).Encode(reply{
+		err := json.NewEncoder(writer).Encode(reply{
 			Blueprints: blueprints,
 			Changes:    changes,
 			Errors:     []responseError{},
 		})
+		common.PanicOnError(err)
 	} else if format == "toml" {
 		// lorax concatenates multiple blueprints with `\n\n` here,
 		// which is never useful. Deviate by only returning the first
@@ -775,7 +785,8 @@ func (api *API) blueprintsInfoHandler(writer http.ResponseWriter, request *http.
 		}
 		encoder := toml.NewEncoder(writer)
 		encoder.Indent = ""
-		encoder.Encode(blueprints[0])
+		err := encoder.Encode(blueprints[0])
+		common.PanicOnError(err)
 	} else {
 		errors := responseError{
 			ID:  "InvalidChars",
@@ -841,10 +852,11 @@ func (api *API) blueprintsDepsolveHandler(writer http.ResponseWriter, request *h
 		blueprints = append(blueprints, entry{*blueprint, dependencies})
 	}
 
-	json.NewEncoder(writer).Encode(reply{
+	err := json.NewEncoder(writer).Encode(reply{
 		Blueprints: blueprints,
 		Errors:     []responseError{},
 	})
+	common.PanicOnError(err)
 }
 
 // setPkgEVRA replaces the version globs in the blueprint with their EVRA values from the dependencies
@@ -958,10 +970,11 @@ func (api *API) blueprintsFreezeHandler(writer http.ResponseWriter, request *htt
 		blueprints = append(blueprints, blueprintFrozen{blueprint})
 	}
 
-	json.NewEncoder(writer).Encode(reply{
+	err := json.NewEncoder(writer).Encode(reply{
 		Blueprints: blueprints,
 		Errors:     errors,
 	})
+	common.PanicOnError(err)
 }
 
 func (api *API) blueprintsDiffHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -1052,7 +1065,8 @@ func (api *API) blueprintsDiffHandler(writer http.ResponseWriter, request *http.
 		diffs = append(diffs, diff{Old: &pack{oldPackage}, New: nil})
 	}
 
-	json.NewEncoder(writer).Encode(reply{diffs})
+	err := json.NewEncoder(writer).Encode(reply{diffs})
+	common.PanicOnError(err)
 }
 
 func (api *API) blueprintsChangesHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -1103,6 +1117,10 @@ func (api *API) blueprintsChangesHandler(writer http.ResponseWriter, request *ht
 		}
 		bpChanges := api.store.GetBlueprintChanges(name)
 		if bpChanges != nil {
+			// Sort the changes by Timestamp, descending
+			sort.Slice(bpChanges, func(i, j int) bool {
+				return bpChanges[i].Timestamp > bpChanges[j].Timestamp
+			})
 			change := change{
 				Changes: bpChanges,
 				Name:    name,
@@ -1118,12 +1136,13 @@ func (api *API) blueprintsChangesHandler(writer http.ResponseWriter, request *ht
 		}
 	}
 
-	json.NewEncoder(writer).Encode(reply{
+	err = json.NewEncoder(writer).Encode(reply{
 		BlueprintsChanges: allChanges,
 		Errors:            errors,
 		Offset:            offset,
 		Limit:             limit,
 	})
+	common.PanicOnError(err)
 }
 
 func (api *API) blueprintsNewHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -1231,7 +1250,13 @@ func (api *API) blueprintUndoHandler(writer http.ResponseWriter, request *http.R
 	bpChange := api.store.GetBlueprintChange(name, commit)
 	bp := bpChange.Blueprint
 	commitMsg := name + ".toml reverted to commit " + commit
-	api.store.PushBlueprint(bp, commitMsg)
+	err := api.store.PushBlueprint(bp, commitMsg)
+	if err != nil {
+		statusResponseError(writer, http.StatusInternalServerError, responseError{
+			ID:  "BlueprintError",
+			Msg: err.Error(),
+		})
+	}
 	statusResponseOK(writer)
 }
 
@@ -1358,7 +1383,8 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 		return
 	}
 
-	json.NewEncoder(writer).Encode(reply)
+	err = json.NewEncoder(writer).Encode(reply)
+	common.PanicOnError(err)
 }
 
 func (api *API) composeDeleteHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -1420,7 +1446,8 @@ func (api *API) composeDeleteHandler(writer http.ResponseWriter, request *http.R
 		Errors []composeDeleteError  `json:"errors"`
 	}{results, errors}
 
-	json.NewEncoder(writer).Encode(reply)
+	err := json.NewEncoder(writer).Encode(reply)
+	common.PanicOnError(err)
 }
 
 func (api *API) composeTypesHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -1440,7 +1467,8 @@ func (api *API) composeTypesHandler(writer http.ResponseWriter, request *http.Re
 		reply.Types = append(reply.Types, composeType{format, true})
 	}
 
-	json.NewEncoder(writer).Encode(reply)
+	err := json.NewEncoder(writer).Encode(reply)
+	common.PanicOnError(err)
 }
 
 func (api *API) composeQueueHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -1463,7 +1491,8 @@ func (api *API) composeQueueHandler(writer http.ResponseWriter, request *http.Re
 		}
 	}
 
-	json.NewEncoder(writer).Encode(reply)
+	err := json.NewEncoder(writer).Encode(reply)
+	common.PanicOnError(err)
 }
 
 func (api *API) composeStatusHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -1528,7 +1557,8 @@ func (api *API) composeStatusHandler(writer http.ResponseWriter, request *http.R
 
 	reply.UUIDs = composesToComposeEntries(composes, uuids, isRequestVersionAtLeast(params, 1))
 
-	json.NewEncoder(writer).Encode(reply)
+	err = json.NewEncoder(writer).Encode(reply)
+	common.PanicOnError(err)
 }
 
 func (api *API) composeInfoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -1589,7 +1619,8 @@ func (api *API) composeInfoHandler(writer http.ResponseWriter, request *http.Req
 		reply.Uploads = TargetsToUploadResponses(compose.ImageBuilds[0].Targets)
 	}
 
-	json.NewEncoder(writer).Encode(reply)
+	err = json.NewEncoder(writer).Encode(reply)
+	common.PanicOnError(err)
 }
 
 func (api *API) composeImageHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -1656,7 +1687,8 @@ func (api *API) composeImageHandler(writer http.ResponseWriter, request *http.Re
 	writer.Header().Set("Content-Type", imageMime)
 	writer.Header().Set("Content-Length", fmt.Sprintf("%d", fileSize))
 
-	io.Copy(writer, reader)
+	_, err = io.Copy(writer, reader)
+	common.PanicOnError(err)
 }
 
 func (api *API) composeLogsHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -1716,7 +1748,8 @@ func (api *API) composeLogsHandler(writer http.ResponseWriter, request *http.Req
 		return
 	}
 
-	resultReader.Close()
+	err = resultReader.Close()
+	common.PanicOnError(err)
 
 	writer.Header().Set("Content-Disposition", "attachment; filename="+id.String()+"-logs.tar")
 	writer.Header().Set("Content-Type", "application/x-tar")
@@ -1725,7 +1758,8 @@ func (api *API) composeLogsHandler(writer http.ResponseWriter, request *http.Req
 
 	// tar format needs to contain file size before the actual file content, therefore the intermediate buffer
 	var fileContents bytes.Buffer
-	result.Write(&fileContents)
+	err = result.Write(&fileContents)
+	common.PanicOnError(err)
 
 	header := &tar.Header{
 		Name: "logs/osbuild.log",
@@ -1733,9 +1767,14 @@ func (api *API) composeLogsHandler(writer http.ResponseWriter, request *http.Req
 		Size: int64(fileContents.Len()),
 	}
 
-	tw.WriteHeader(header)
-	io.Copy(tw, &fileContents)
-	tw.Close()
+	err = tw.WriteHeader(header)
+	common.PanicOnError(err)
+
+	_, err = io.Copy(tw, &fileContents)
+	common.PanicOnError(err)
+
+	err = tw.Close()
+	common.PanicOnError(err)
 }
 
 func (api *API) composeLogHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -1801,9 +1840,11 @@ func (api *API) composeLogHandler(writer http.ResponseWriter, request *http.Requ
 		return
 	}
 
-	resultReader.Close()
+	err = resultReader.Close()
+	common.PanicOnError(err)
 
-	result.Write(writer)
+	err = result.Write(writer)
+	common.PanicOnError(err)
 }
 
 func (api *API) composeFinishedHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -1823,7 +1864,8 @@ func (api *API) composeFinishedHandler(writer http.ResponseWriter, request *http
 		}
 	}
 
-	json.NewEncoder(writer).Encode(reply)
+	err := json.NewEncoder(writer).Encode(reply)
+	common.PanicOnError(err)
 }
 
 func (api *API) composeFailedHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
@@ -1843,14 +1885,12 @@ func (api *API) composeFailedHandler(writer http.ResponseWriter, request *http.R
 		}
 	}
 
-	json.NewEncoder(writer).Encode(reply)
+	err := json.NewEncoder(writer).Encode(reply)
+	common.PanicOnError(err)
 }
 
 func (api *API) fetchPackageList() (rpmmd.PackageList, error) {
-	var repos []rpmmd.RepoConfig
-	for _, repo := range api.distro.Repositories(api.arch) {
-		repos = append(repos, repo)
-	}
+	repos := api.distro.Repositories(api.arch)
 	for _, source := range api.store.GetAllSources() {
 		repos = append(repos, source.RepoConfig())
 	}
@@ -1871,10 +1911,7 @@ func getPkgNameGlob(pkg blueprint.Package) string {
 }
 
 func (api *API) depsolveBlueprint(bp *blueprint.Blueprint, outputType, arch string, clean bool) ([]rpmmd.PackageSpec, []rpmmd.PackageSpec, map[string]string, error) {
-	var repos []rpmmd.RepoConfig
-	for _, repo := range api.distro.Repositories(api.arch) {
-		repos = append(repos, repo)
-	}
+	repos := api.distro.Repositories(api.arch)
 	for _, source := range api.store.GetAllSources() {
 		repos = append(repos, source.RepoConfig())
 	}
@@ -1909,6 +1946,9 @@ func (api *API) depsolveBlueprint(bp *blueprint.Blueprint, outputType, arch stri
 			return nil, nil, nil, err
 		}
 		buildPackages, _, err = api.rpmmd.Depsolve(buildSpecs, nil, repos, api.distro.ModulePlatformID(), false)
+		if err != nil {
+			return nil, nil, nil, err
+		}
 	}
 
 	return packages, buildPackages, checksums, err
diff --git a/internal/weldrcheck/blueprints.go b/internal/weldrcheck/blueprints.go
new file mode 100644
index 000000000..37aa81a12
--- /dev/null
+++ b/internal/weldrcheck/blueprints.go
@@ -0,0 +1,1109 @@
+// Package weldrcheck - blueprints contains functions to check the blueprints API
+// Copyright (C) 2020 by Red Hat, Inc.
+package weldrcheck
+
+import (
+	"log"
+	"reflect"
+	"sort"
+	"strings"
+
+	"github.com/BurntSushi/toml"
+
+	"github.com/osbuild/osbuild-composer/internal/client"
+)
+
+type checkBlueprintsV0 struct {
+	socket string
+}
+
+// Run will execute the API V0 Blueprint check functions
+// This will call all of the methods that start with 'Check', passing them a pointer to a
+// checkBlueprintsV0 struct and expecting a bool to be returned to indicate whether or not the test
+// passed.
+// If any of the tests fail it will return false.
+func (c *checkBlueprintsV0) Run() bool {
+	pass := true
+
+	// Construct a reflect.Type to use for checking method type signatures
+	boolType := reflect.TypeOf(true)
+	structType := reflect.TypeOf(c)
+	funcType := reflect.FuncOf([]reflect.Type{structType}, []reflect.Type{boolType}, false)
+
+	structValue := reflect.ValueOf(c)
+	// Get all the exported methods on this struct named 'Check*' and run them
+	for i := 0; i < structType.NumMethod(); i++ {
+		method := structType.Method(i)
+		// Make sure it starts with Check and matches the type signature
+		if strings.HasPrefix(method.Name, "Check") {
+			if method.Type != funcType {
+				log.Printf("ERROR: Check function '%s' has wrong type: %s", method.Name, method.Type)
+				pass = false
+				continue
+			}
+
+			r := structValue.Method(i).Call(nil)
+			if !r[0].Bool() {
+				pass = false
+			}
+		}
+	}
+
+	return pass
+}
+
+// POST a new TOML blueprint
+func (c *checkBlueprintsV0) CheckPostTOML() bool {
+	name := "POST of a TOML blueprint"
+
+	bp := `
+		name="test-toml-blueprint-v0"
+		description="postTOMLBlueprintV0"
+		version="0.0.1"
+		[[packages]]
+		name="bash"
+		version="*"
+
+		[[modules]]
+		name="util-linux"
+		version="*"
+
+		[[customizations.user]]
+		name="root"
+		password="qweqweqwe"
+		`
+	resp, err := client.PostTOMLBlueprintV0(c.socket, bp)
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if resp.Status != true {
+		log.Printf("FAIL: %s failed: %s", name, resp)
+		return false
+	}
+	log.Printf("OK: %s was successful", name)
+	return true
+}
+
+// POST a new JSON blueprint
+func (c *checkBlueprintsV0) CheckPostJSON() bool {
+	name := "POST of a JSON blueprint"
+
+	bp := `{
+		"name": "test-json-blueprint-v0",
+		"description": "postJSONBlueprintV0",
+		"version": "0.0.1",
+		"packages": [{"name": "bash", "version": "*"}],
+		"modules": [{"name": "util-linux", "version": "*"}],
+		"customizations": {"user": [{"name": "root", "password": "qweqweqwe"}]}
+	}`
+
+	resp, err := client.PostJSONBlueprintV0(c.socket, bp)
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if resp.Status != true {
+		log.Printf("FAIL: %s failed: %s", name, resp)
+		return false
+	}
+	log.Printf("OK: %s was successful", name)
+	return true
+}
+
+// POST a blueprint to the workspace as TOML
+func (c *checkBlueprintsV0) CheckPostTOMLWS() bool {
+	name := "POST TOML blueprint to workspace"
+
+	bp := `
+		name="test-toml-blueprint-ws-v0"
+		description="postTOMLBlueprintWSV0"
+		version="0.0.1"
+		[[packages]]
+		name="bash"
+		version="*"
+
+		[[modules]]
+		name="util-linux"
+		version="*"
+
+		[[customizations.user]]
+		name="root"
+		password="qweqweqwe"
+		`
+	resp, err := client.PostTOMLWorkspaceV0(c.socket, bp)
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if resp.Status != true {
+		log.Printf("FAIL: %s failed: %s", name, resp)
+		return false
+	}
+	log.Printf("OK: %s was successful", name)
+	return true
+}
+
+// POST a blueprint to the workspace as JSON
+func (c *checkBlueprintsV0) CheckPostJSONWS() bool {
+	name := "POST JSON blueprint to workspace"
+
+	bp := `{
+		"name": "test-json-blueprint-ws-v0",
+		"description": "postJSONBlueprintWSV0",
+		"version": "0.0.1",
+		"packages": [{"name": "bash", "version": "*"}],
+		"modules": [{"name": "util-linux", "version": "*"}],
+		"customizations": {"user": [{"name": "root", "password": "qweqweqwe"}]}
+	}`
+
+	resp, err := client.PostJSONWorkspaceV0(c.socket, bp)
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if resp.Status != true {
+		log.Printf("FAIL: %s failed: %s", name, resp)
+		return false
+	}
+	log.Printf("OK: %s was successful", name)
+	return true
+}
+
+// delete a blueprint
+func (c *checkBlueprintsV0) CheckDelete() bool {
+	name := "DELETE blueprint"
+
+	// POST a blueprint to delete
+	bp := `{
+		"name": "test-delete-blueprint-v0",
+		"description": "deleteBlueprintV0",
+		"version": "0.0.1",
+		"packages": [{"name": "bash", "version": "*"}],
+		"modules": [{"name": "util-linux", "version": "*"}],
+		"customizations": {"user": [{"name": "root", "password": "qweqweqwe"}]}
+	}`
+
+	resp, err := client.PostJSONBlueprintV0(c.socket, bp)
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if resp.Status != true {
+		log.Printf("FAIL: %s failed: %s", name, resp)
+		return false
+	}
+
+	// Delete the blueprint
+	resp, err = client.DeleteBlueprintV0(c.socket, "test-delete-blueprint-v0")
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if resp.Status != true {
+		log.Printf("FAIL: %s failed: %s", name, resp)
+		return false
+	}
+
+	log.Printf("OK: %s was successful", name)
+	return true
+}
+
+// delete a new blueprint from the workspace
+func (c *checkBlueprintsV0) CheckDeleteNewWS() bool {
+	name := "DELETE new blueprint from workspace"
+
+	// POST a blueprint to delete
+	bp := `{
+		"name": "test-delete-new-blueprint-ws-v0",
+		"description": "deleteNewBlueprintWSV0",
+		"version": "0.0.1",
+		"packages": [{"name": "bash", "version": "*"}],
+		"modules": [{"name": "util-linux", "version": "*"}],
+		"customizations": {"user": [{"name": "root", "password": "qweqweqwe"}]}
+	}`
+
+	resp, err := client.PostJSONWorkspaceV0(c.socket, bp)
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if resp.Status != true {
+		log.Printf("FAIL: %s failed: %s", name, resp)
+		return false
+	}
+
+	// Delete the blueprint
+	resp, err = client.DeleteWorkspaceV0(c.socket, "test-delete-new-blueprint-ws-v0")
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if resp.Status != true {
+		log.Printf("FAIL: %s failed: %s", name, resp)
+		return false
+	}
+
+	log.Printf("OK: %s was successful", name)
+	return true
+}
+
+// delete blueprint changes from the workspace
+func (c *checkBlueprintsV0) CheckDeleteChangesWS() bool {
+	name := "DELETE blueprint changes from workspace"
+
+	// POST a blueprint first
+	bp := `{
+		"name": "test-delete-blueprint-changes-ws-v0",
+		"description": "deleteBlueprintChangesWSV0",
+		"version": "0.0.1",
+		"packages": [{"name": "bash", "version": "*"}],
+		"modules": [{"name": "util-linux", "version": "*"}],
+		"customizations": {"user": [{"name": "root", "password": "qweqweqwe"}]}
+	}`
+
+	resp, err := client.PostJSONBlueprintV0(c.socket, bp)
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if resp.Status != true {
+		log.Printf("FAIL: %s failed: %s", name, resp)
+		return false
+	}
+
+	// Post blueprint changes to the workspace
+	bp = `{
+		"name": "test-delete-blueprint-changes-ws-v0",
+		"description": "workspace copy",
+		"version": "0.2.0",
+		"packages": [{"name": "frobozz", "version": "*"}],
+		"modules": [{"name": "util-linux", "version": "*"}],
+		"customizations": {"user": [{"name": "root", "password": "qweqweqwe"}]}
+	}`
+
+	resp, err = client.PostJSONWorkspaceV0(c.socket, bp)
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if resp.Status != true {
+		log.Printf("FAIL: %s failed: %s", name, resp)
+		return false
+	}
+
+	// Get the blueprint, make sure it is the modified one and that changes = true
+	info, api, err := client.GetBlueprintsInfoJSONV0(c.socket, "test-delete-blueprint-changes-ws-v0")
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if api != nil {
+		log.Printf("FAIL: %s BlueprintsInfo request failed: %s", name, api)
+		return false
+	}
+
+	if len(info.Blueprints) < 1 {
+		log.Printf("FAIL: %s failed: No blueprints returned", name)
+		return false
+	}
+
+	if len(info.Changes) < 1 {
+		log.Printf("FAIL: %s failed: No change states returned", name)
+		return false
+	}
+
+	if info.Blueprints[0].Name != "test-delete-blueprint-changes-ws-v0" {
+		log.Printf("FAIL: %s failed: wrong blueprint returned", name)
+		return false
+	}
+
+	if info.Changes[0].Name != "test-delete-blueprint-changes-ws-v0" {
+		log.Printf("FAIL: %s failed: wrong change state returned", name)
+		return false
+	}
+
+	if info.Changes[0].Changed != true {
+		log.Printf("FAIL: %s failed: wrong change state returned (false)", name)
+		return false
+	}
+
+	if info.Blueprints[0].Description != "workspace copy" {
+		log.Printf("FAIL: %s failed: workspace copy not returned", name)
+		return false
+	}
+
+	// Delete the blueprint from the workspace
+	resp, err = client.DeleteWorkspaceV0(c.socket, "test-delete-blueprint-changes-ws-v0")
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if resp.Status != true {
+		log.Printf("FAIL: %s failed: %s", name, resp)
+		return false
+	}
+
+	// Get the blueprint, make sure it is the un-modified one
+	info, api, err = client.GetBlueprintsInfoJSONV0(c.socket, "test-delete-blueprint-changes-ws-v0")
+	if err != nil {
+		log.Printf("FAIL: %s failed: %s", name, err)
+		return false
+	}
+	if api != nil {
+		log.Printf("FAIL: %s BlueprintsInfo request failed: %s", name, api)
+		return false
+	}
+
+	if len(info.Blueprints) < 1 {
+		log.Printf("FAIL: %s failed: No blueprints returned", name)
+		return false
+	}
+
+	if len(info.Changes) < 1 {
+		log.Printf("FAIL: %s failed: No change states returned", name)
+		return false
+	}
+
+	if info.Blueprints[0].Name != "test-delete-blueprint-changes-ws-v0" {
+		log.Printf("FAIL: %s failed: wrong blueprint returned", name)
+		return false
+	}
+
+	if info.Changes[0].Name != "test-delete-blueprint-changes-ws-v0" {
+		log.Printf("FAIL: %s failed: wrong change state returned", name)
+		return false
+	}
+
+	if info.Changes[0].Changed != false {
+		log.Printf("FAIL: %s failed: wrong change state returned (true)", name)
+		return false
+	}
+
+	if info.Blueprints[0].Description != "deleteBlueprintChangesWSV0" {
+		log.Printf("FAIL: %s failed: original blueprint not returned", name)
+		return false
+	}
+
+	log.Printf("OK: %s was successful", name)
+	return true
+}
+
+// list blueprints
+func (c *checkBlueprintsV0) CheckList() bool {
+	name := "List blueprints"
+	// Post a couple of blueprints
+	bps := []string{`{
+		"name": "test-list-blueprint-1-v0",
+		"description": "listBlueprintsV0",
+		"version": "0.0.1",
+		"packages": [{"name": "bash", "version": "*"}],
+		"modules": [{"name": "util-linux", "version": "*"}],
+		"customizations": {"user": [{"name": "root", "password": "qweqweqwe"}]}
+	}`,
+		`{
+		"name": "test-list-blueprint-2-v0",
+		"description": "listBlueprintsV0",
+		"version": "0.0.1",
+		"packages": [{"name": "bash", "version": "*"}],
+		"modules": [{"name": "util-linux", "version": "*"}],
+		"customizations": {"user": [{"name": "root", "password": "qweqweqwe"}]}
+	}`}
+
+	for i := range bps {
+		resp, err := client.PostJSONBlueprintV0(c.socket, bps[i])
+		if err != nil {
+			log.Printf("FAIL: %s failed with a client error: %s", name, err)
+			return false
+		}
+		if resp.Status != true {
+			log.Printf("FAIL: %s failed: %s", name, resp)
+			return false
+		}
+	}
+
+	// Get the list of blueprints
+	list, api, err := client.ListBlueprintsV0(c.socket)
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err.Error())
+		return false
+	}
+	if api != nil {
+		log.Printf("FAIL: %s ListBlueprints failed: %s", name, api)
+		return false
+	}
+
+	// Make sure the blueprints are in the list
+	sorted := sort.StringSlice(list)
+	if !isStringInSlice(sorted, "test-list-blueprint-1-v0") ||
+		!isStringInSlice(sorted, "test-list-blueprint-2-v0") {
+		log.Printf("FAIL: %s failed", name)
+		return false
+	}
+
+	log.Printf("OK: %s was successful", name)
+	return true
+}
+
+// get blueprint contents as TOML
+func (c *checkBlueprintsV0) CheckGetTOML() bool {
+	name := "Get TOML Blueprint"
+	bp := `{
+		"name": "test-get-blueprint-1-v0",
+		"description": "getTOMLBlueprintV0",
+		"version": "0.0.1",
+		"packages": [{"name": "bash", "version": "*"}],
+		"modules": [{"name": "util-linux", "version": "*"}],
+		"customizations": {"user": [{"name": "root", "password": "qweqweqwe"}]}
+	}`
+
+	// Post a blueprint
+	resp, err := client.PostJSONBlueprintV0(c.socket, bp)
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if resp.Status != true {
+		log.Printf("FAIL: %s failed: %s", name, resp)
+		return false
+	}
+
+	// Get it as TOML
+	body, api, err := client.GetBlueprintInfoTOMLV0(c.socket, "test-get-blueprint-1-v0")
+	if err != nil {
+		log.Printf("FAIL: %s failed: %s", name, err)
+		return false
+	}
+	if api != nil {
+		log.Printf("FAIL: %s GetBlueprintInfo failed: %s", name, api)
+		return false
+	}
+
+	if len(body) == 0 {
+		log.Printf("FAIL: %s failed: body of response is empty", name)
+		return false
+	}
+
+	// Can it be decoded as TOML?
+	var decoded interface{}
+	if _, err := toml.Decode(body, &decoded); err != nil {
+		log.Printf("FAIL: %s failed: %s", name, err)
+		return false
+	}
+
+	log.Printf("OK: %s was successful", name)
+	return true
+}
+
+// get blueprint contents as JSON
+func (c *checkBlueprintsV0) CheckGetJSON() bool {
+	name := "Get JSON Blueprint"
+	bp := `{
+		"name": "test-get-blueprint-2-v0",
+		"description": "getJSONBlueprintV0",
+		"version": "0.0.1",
+		"packages": [{"name": "bash", "version": "*"}],
+		"modules": [{"name": "util-linux", "version": "*"}],
+		"customizations": {"user": [{"name": "root", "password": "qweqweqwe"}]}
+	}`
+
+	// Post a blueprint
+	resp, err := client.PostJSONBlueprintV0(c.socket, bp)
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if resp.Status != true {
+		log.Printf("FAIL: %s failed: %s", name, resp)
+		return false
+	}
+
+	// Get the blueprint and its changed state
+	info, api, err := client.GetBlueprintsInfoJSONV0(c.socket, "test-get-blueprint-2-v0")
+	if err != nil {
+		log.Printf("FAIL: %s failed: %s", name, err)
+		return false
+	}
+	if api != nil {
+		log.Printf("FAIL: %s GetBlueprintsInfo failed: %s", name, api)
+		return false
+	}
+
+	if len(info.Blueprints) < 1 {
+		log.Printf("FAIL: %s failed: No blueprints returned", name)
+		return false
+	}
+
+	if len(info.Changes) < 1 {
+		log.Printf("FAIL: %s failed: No change states returned", name)
+		return false
+	}
+
+	if info.Blueprints[0].Name != "test-get-blueprint-2-v0" {
+		log.Printf("FAIL: %s failed: wrong blueprint returned", name)
+		return false
+	}
+
+	if info.Changes[0].Name != "test-get-blueprint-2-v0" {
+		log.Printf("FAIL: %s failed: wrong change state returned", name)
+		return false
+	}
+
+	if info.Changes[0].Changed != false {
+		log.Printf("FAIL: %s failed: unexpected changes", name)
+		return false
+	}
+
+	log.Printf("OK: %s was successful", name)
+	return true
+}
+
+// pushing the same blueprint bumps the version number returned by show
+func (c *checkBlueprintsV0) CheckBumpVersion() bool {
+	name := "Bump Blueprint Version number"
+	bp := `{
+		"name": "test-bump-blueprint-1-v0",
+		"description": "bumpBlueprintVersionV0",
+		"version": "2.1.2",
+		"packages": [{"name": "bash", "version": "*"}],
+		"modules": [{"name": "util-linux", "version": "*"}],
+		"customizations": {"user": [{"name": "root", "password": "qweqweqwe"}]}
+	}`
+
+	// List blueprints
+	list, api, err := client.ListBlueprintsV0(c.socket)
+	if err != nil {
+		log.Printf("FAIL: %s failed: %s", name, err.Error())
+		return false
+	}
+	if api != nil {
+		log.Printf("FAIL: %s ListBlueprints failed: %s", name, api)
+		return false
+	}
+
+	// If the blueprint already exists it needs to be deleted to start from a known state
+	sorted := sort.StringSlice(list)
+	if isStringInSlice(sorted, "test-bump-blueprint-1-v0") {
+		// Delete this blueprint if it already exists
+		resp, err := client.DeleteBlueprintV0(c.socket, "test-bump-blueprint-1-v0")
+		if err != nil {
+			log.Printf("FAIL: %s failed with a client error: %s", name, err)
+			return false
+		}
+		if resp.Status != true {
+			log.Printf("FAIL: %s failed: %s", name, resp)
+			return false
+		}
+	}
+
+	// Post a blueprint
+	resp, err := client.PostJSONBlueprintV0(c.socket, bp)
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if resp.Status != true {
+		log.Printf("FAIL: %s failed: %s", name, resp)
+		return false
+	}
+
+	// Post a blueprint again to bump verion to 2.1.3
+	resp, err = client.PostJSONBlueprintV0(c.socket, bp)
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if resp.Status != true {
+		log.Printf("FAIL: %s failed: %s", name, resp)
+		return false
+	}
+
+	// Get the blueprint and its changed state
+	info, api, err := client.GetBlueprintsInfoJSONV0(c.socket, "test-bump-blueprint-1-v0")
+	if err != nil {
+		log.Printf("FAIL: %s failed: %s", name, err)
+		return false
+	}
+	if api != nil {
+		log.Printf("FAIL: %s GetBlueprintsInfo failed: %s", name, api)
+		return false
+	}
+
+	if len(info.Blueprints) < 1 {
+		log.Printf("FAIL: %s failed: No blueprints returned", name)
+		return false
+	}
+
+	if info.Blueprints[0].Name != "test-bump-blueprint-1-v0" {
+		log.Printf("FAIL: %s failed: wrong blueprint returned", name)
+		return false
+	}
+
+	if info.Blueprints[0].Version != "2.1.3" {
+		log.Printf("FAIL: %s failed: wrong blueprint version", name)
+		return false
+	}
+
+	log.Printf("OK: %s was successful", name)
+	return true
+}
+
+// Make several changes to a blueprint and list the changes
+func (c *checkBlueprintsV0) CheckBlueprintChangesV0() bool {
+	name := "List blueprint changes"
+
+	bps := []string{`{
+		"name": "test-blueprint-changes-v0",
+		"description": "CheckBlueprintChangesV0",
+		"version": "0.0.1",
+		"packages": [{"name": "bash", "version": "*"}],
+		"modules": [{"name": "util-linux", "version": "*"}]
+	}`,
+		`{
+		"name": "test-blueprint-changes-v0",
+		"description": "CheckBlueprintChangesV0",
+		"version": "0.1.0",
+		"packages": [{"name": "bash", "version": "*"}, {"name": "tmux", "version": "*"}],
+		"modules": [{"name": "util-linux", "version": "*"}],
+		"customizations": {"user": [{"name": "root", "password": "qweqweqwe"}]}
+	}`,
+		`{
+		"name": "test-blueprint-changes-v0",
+		"description": "CheckBlueprintChangesV0",
+		"version": "0.1.1",
+		"packages": [{"name": "bash", "version": "*"}, {"name": "tmux", "version": "*"}],
+		"modules": [],
+		"customizations": {"user": [{"name": "root", "password": "asdasdasd"}]}
+	}`}
+
+	// Push 3 changes to the blueprint
+	for i := range bps {
+		resp, err := client.PostJSONBlueprintV0(c.socket, bps[i])
+		if err != nil {
+			log.Printf("FAIL: %s failed with a client error: %s", name, err)
+			return false
+		}
+		if resp.Status != true {
+			log.Printf("FAIL: %s failed: %s", name, resp)
+			return false
+		}
+	}
+
+	// List the changes
+	changes, api, err := client.GetBlueprintsChangesV0(c.socket, []string{"test-blueprint-changes-v0"})
+	if err != nil {
+		log.Printf("FAIL: %s failed: %s", name, err)
+		return false
+	}
+	if api != nil {
+		log.Printf("FAIL: %s GetBlueprintsChanges failed: %s", name, api)
+		return false
+	}
+
+	if len(changes.BlueprintsChanges) != 1 {
+		log.Printf("FAIL: %s failed: No changes returned", name)
+		return false
+	}
+
+	if changes.BlueprintsChanges[0].Name != "test-blueprint-changes-v0" {
+		log.Printf("FAIL: %s failed: Wrong blueprint changes returned", name)
+		return false
+	}
+
+	if len(changes.BlueprintsChanges[0].Changes) < 3 {
+		log.Printf("FAIL: %s failed: Wrong number of changes returned", name)
+		return false
+	}
+
+	log.Printf("OK: %s was successful", name)
+	return true
+}
+
+// Undo blueprint changes
+func (c *checkBlueprintsV0) CheckUndoBlueprintV0() bool {
+	name := "Undo blueprint changes"
+
+	bps := []string{`{
+		"name": "test-undo-blueprint-v0",
+		"description": "CheckUndoBlueprintV0",
+		"version": "0.0.5",
+		"packages": [{"name": "bash", "version": "*"}],
+		"modules": [{"name": "util-linux", "version": "*"}],
+		"customizations": {"user": [{"name": "root", "password": "qweqweqwe"}]}
+	}`,
+		`{
+		"name": "test-undo-blueprint-v0",
+		"description": "CheckUndoBlueprintv0",
+		"version": "0.0.6",
+		"packages": [{"name": "bash", "version": "0.5.*"}],
+		"modules": [{"name": "util-linux", "version": "*"}],
+		"customizations": {"user": [{"name": "root", "password": "qweqweqwe"}]}
+	}`}
+
+	// Push original version of the blueprint
+	resp, err := client.PostJSONBlueprintV0(c.socket, bps[0])
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if resp.Status != true {
+		log.Printf("FAIL: %s failed: %s", name, resp)
+		return false
+	}
+
+	// Get the commit hash
+	changes, api, err := client.GetBlueprintsChangesV0(c.socket, []string{"test-undo-blueprint-v0"})
+	if err != nil {
+		log.Printf("FAIL: %s failed: %s", name, err)
+		return false
+	}
+	if api != nil {
+		log.Printf("FAIL: %s GetBlueprintsChanges failed: %s", name, api)
+		return false
+	}
+
+	if len(changes.BlueprintsChanges) != 1 {
+		log.Printf("FAIL: %s failed: No changes returned", name)
+		return false
+	}
+
+	if len(changes.BlueprintsChanges[0].Changes) < 1 {
+		log.Printf("FAIL: %s failed: Wrong number of changes returned", name)
+		return false
+	}
+	commit := changes.BlueprintsChanges[0].Changes[0].Commit
+
+	if len(commit) == 0 {
+		log.Printf("FAIL: %s failed: First commit is empty", name)
+		return false
+	}
+
+	// Push the new version with wrong bash version
+	resp, err = client.PostJSONBlueprintV0(c.socket, bps[1])
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if resp.Status != true {
+		log.Printf("FAIL: %s failed: %s", name, resp)
+		return false
+	}
+
+	// Get the blueprint, confirm bash version is '0.5.*'
+	info, api, err := client.GetBlueprintsInfoJSONV0(c.socket, "test-undo-blueprint-v0")
+	if err != nil {
+		log.Printf("FAIL: %s failed: %s", name, err)
+		return false
+	}
+	if api != nil {
+		log.Printf("FAIL: %s GetBlueprintsInfo failed: %s", name, api)
+		return false
+	}
+
+	if len(info.Blueprints) < 1 {
+		log.Printf("FAIL: %s failed: No blueprints returned", name)
+		return false
+	}
+
+	if len(info.Blueprints[0].Packages) < 1 {
+		log.Printf("FAIL: %s failed: No packages in the blueprint", name)
+		return false
+	}
+
+	if info.Blueprints[0].Packages[0].Name != "bash" ||
+		info.Blueprints[0].Packages[0].Version != "0.5.*" {
+		log.Printf("FAIL: %s failed to push change: Wrong package in the blueprint: %s", name, info.Blueprints[0].Packages[0])
+		log.Printf("%#v", info)
+		return false
+	}
+
+	// Revert the blueprint to the original version
+	resp, err = client.UndoBlueprintChangeV0(c.socket, "test-undo-blueprint-v0", commit)
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if resp.Status != true {
+		log.Printf("FAIL: %s failed: %s", name, resp)
+		return false
+	}
+
+	// Get the blueprint, confirm bash version is '*'
+	info, api, err = client.GetBlueprintsInfoJSONV0(c.socket, "test-undo-blueprint-v0")
+	if err != nil {
+		log.Printf("FAIL: %s failed: %s", name, err)
+		return false
+	}
+	if api != nil {
+		log.Printf("FAIL: %s GetBlueprintsInfo failed: %s", name, api)
+		return false
+	}
+
+	if len(info.Blueprints) < 1 {
+		log.Printf("FAIL: %s failed: No blueprints returned", name)
+		return false
+	}
+
+	if len(info.Blueprints[0].Packages) < 1 {
+		log.Printf("FAIL: %s failed: No packages in the blueprint", name)
+		return false
+	}
+
+	if info.Blueprints[0].Packages[0].Name != "bash" ||
+		info.Blueprints[0].Packages[0].Version != "*" {
+		log.Printf("FAIL: %s failed to undo change: Wrong package in the blueprint: %s", name, info.Blueprints[0].Packages[0])
+		log.Printf("%#v", info)
+		return false
+	}
+
+	log.Printf("OK: %s was successful", name)
+	return true
+}
+
+// Tag a blueprint with a new revision
+// The blueprint revision tag cannot be reset, it always increments by one, and cannot be deleted.
+// So to test tagging we tag two blueprint changes and make sure the second is first +1
+func (c *checkBlueprintsV0) CheckBlueprintTagV0() bool {
+	name := "Tag a blueprint"
+
+	bps := []string{`{
+		"name": "test-tag-blueprint-v0",
+		"description": "CheckBlueprintTagV0",
+		"version": "0.0.1",
+		"packages": [{"name": "bash", "version": "0.1.*"}],
+		"modules": [{"name": "util-linux", "version": "*"}],
+		"customizations": {"user": [{"name": "root", "password": "qweqweqwe"}]}
+	}`,
+		`{
+		"name": "test-tag-blueprint-v0",
+		"description": "CheckBlueprintTagV0",
+		"version": "0.0.1",
+		"packages": [{"name": "bash", "version": "0.5.*"}],
+		"modules": [{"name": "util-linux", "version": "*"}],
+		"customizations": {"user": [{"name": "root", "password": "qweqweqwe"}]}
+	}`}
+
+	// Push a blueprint
+	resp, err := client.PostJSONBlueprintV0(c.socket, bps[0])
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if resp.Status != true {
+		log.Printf("FAIL: %s POST failed: %s", name, resp)
+		return false
+	}
+
+	// Tag the blueprint
+	tagResp, err := client.TagBlueprintV0(c.socket, "test-tag-blueprint-v0")
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if tagResp.Status != true {
+		log.Printf("FAIL: %s Tag failed: %s", name, tagResp)
+		return false
+	}
+
+	// Get changes, get the blueprint's revision
+	changes, api, err := client.GetBlueprintsChangesV0(c.socket, []string{"test-tag-blueprint-v0"})
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if api != nil {
+		log.Printf("FAIL: %s GetBlueprintsChange failed: %s", name, api)
+		return false
+	}
+
+	if len(changes.BlueprintsChanges) != 1 {
+		log.Printf("FAIL: %s failed: No changes returned", name)
+		return false
+	}
+
+	if len(changes.BlueprintsChanges[0].Changes) < 1 {
+		log.Printf("FAIL: %s failed: Wrong number of changes returned", name)
+		return false
+	}
+
+	revision := changes.BlueprintsChanges[0].Changes[0].Revision
+	if revision == nil || *revision == 0 {
+		log.Printf("FAIL: %s failed: Revision is zero", name)
+		return false
+	}
+
+	// Push a new version of the blueprint
+	resp, err = client.PostJSONBlueprintV0(c.socket, bps[1])
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if resp.Status != true {
+		log.Printf("FAIL: %s POST failed: %s", name, resp)
+		return false
+	}
+
+	// Tag the blueprint
+	tagResp, err = client.TagBlueprintV0(c.socket, "test-tag-blueprint-v0")
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if tagResp.Status != true {
+		log.Printf("FAIL: %s Tag failed: %s", name, tagResp)
+		return false
+	}
+
+	// Get changes, confirm that Revision is revision +1
+	changes, api, err = client.GetBlueprintsChangesV0(c.socket, []string{"test-tag-blueprint-v0"})
+	if err != nil {
+		log.Printf("FAIL: %s failed: %s", name, err)
+		return false
+	}
+	if api != nil {
+		log.Printf("FAIL: %s GetBlueprintsChanges failed: %s", name, api)
+		return false
+	}
+
+	if len(changes.BlueprintsChanges) != 1 {
+		log.Printf("FAIL: %s failed: No changes returned", name)
+		return false
+	}
+
+	if len(changes.BlueprintsChanges[0].Changes) < 1 {
+		log.Printf("FAIL: %s failed: Wrong number of changes returned", name)
+		return false
+	}
+
+	newRevision := changes.BlueprintsChanges[0].Changes[0].Revision
+	if newRevision == nil || *newRevision != *revision+1 {
+		log.Printf("FAIL: %s failed: Revision is not %d", name, *revision+1)
+		return false
+	}
+
+	log.Printf("OK: %s was successful", name)
+	return true
+}
+
+// depsolve a blueprint with packages and modules
+func (c *checkBlueprintsV0) CheckBlueprintDepsolveV0() bool {
+	name := "Depsolve a blueprint"
+
+	bp := `{
+		"name": "test-deps-blueprint-v0",
+		"description": "CheckBlueprintDepsolveV0",
+		"version": "0.0.1",
+		"packages": [{"name": "bash", "version": "*"}],
+		"modules": [{"name": "util-linux", "version": "*"}]
+	}`
+
+	// Push a blueprint
+	resp, err := client.PostJSONBlueprintV0(c.socket, bp)
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if resp.Status != true {
+		log.Printf("FAIL: %s POST failed: %s", name, resp)
+		return false
+	}
+
+	// Depsolve the blueprint
+	deps, api, err := client.DepsolveBlueprintV0(c.socket, "test-deps-blueprint-v0")
+	if err != nil {
+		log.Printf("FAIL: %s failed: %s", name, err.Error())
+		return false
+	}
+	if api != nil {
+		log.Printf("FAIL: %s DepsolveBlueprint failed: %s", name, api)
+		return false
+	}
+
+	if len(deps.Blueprints) < 1 {
+		log.Printf("FAIL: %s failed: No blueprint dependencies returned", name)
+		return false
+	}
+
+	if len(deps.Blueprints[0].Dependencies) < 3 {
+		log.Printf("FAIL: %s failed: Not enough dependencies returned", name)
+		return false
+	}
+
+	// TODO
+	// Get the bash and util-linux dependencies and make sure their versions are not *
+
+	log.Printf("OK: %s was successful", name)
+	return true
+}
+
+// freeze a blueprint
+func (c *checkBlueprintsV0) CheckBlueprintFreezeV0() bool {
+	name := "Freeze a blueprint"
+
+	bp := `{
+		"name": "test-freeze-blueprint-v0",
+		"description": "CheckBlueprintFreezeV0",
+		"version": "0.0.1",
+		"packages": [{"name": "bash", "version": "*"}],
+		"modules": [{"name": "util-linux", "version": "*"}]
+	}`
+
+	// Push a blueprint
+	resp, err := client.PostJSONBlueprintV0(c.socket, bp)
+	if err != nil {
+		log.Printf("FAIL: %s failed with a client error: %s", name, err)
+		return false
+	}
+	if resp.Status != true {
+		log.Printf("FAIL: %s POST failed: %s", name, resp)
+		return false
+	}
+
+	// Freeze the blueprint
+	frozen, api, err := client.FreezeBlueprintV0(c.socket, "test-freeze-blueprint-v0")
+	if err != nil {
+		log.Printf("FAIL: %s failed: %s", name, err.Error())
+		return false
+	}
+	if api != nil {
+		log.Printf("FAIL: %s FreezeBlueprint failed: %s", name, api)
+		return false
+	}
+
+	if len(frozen.Blueprints) < 1 {
+		log.Printf("FAIL: %s failed: No frozen blueprints returned", name)
+		return false
+	}
+
+	if len(frozen.Blueprints[0].Blueprint.Packages) < 1 {
+		log.Printf("FAIL: %s failed: No frozen packages returned", name)
+		return false
+	}
+
+	if frozen.Blueprints[0].Blueprint.Packages[0].Name != "bash" ||
+		frozen.Blueprints[0].Blueprint.Packages[0].Version == "*" {
+		log.Printf("FAIL: %s failed: Incorrect frozen packages", name)
+		return false
+	}
+
+	if len(frozen.Blueprints[0].Blueprint.Modules) < 1 {
+		log.Printf("FAIL: %s failed: No frozen modules returned", name)
+		return false
+	}
+
+	if frozen.Blueprints[0].Blueprint.Modules[0].Name != "util-linux" ||
+		frozen.Blueprints[0].Blueprint.Modules[0].Version == "*" {
+		log.Printf("FAIL: %s failed: Incorrect frozen modules", name)
+		return false
+	}
+
+	log.Printf("OK: %s was successful", name)
+	return true
+}
+
+// diff of blueprint changes
+func (c *checkBlueprintsV0) CheckBlueprintDiffV0() bool {
+	name := "Diff of blueprint changes"
+	log.Printf("SKIP: %s was skipped, needs to be implemented", name)
+	return true
+}
diff --git a/internal/weldrcheck/check.go b/internal/weldrcheck/check.go
new file mode 100644
index 000000000..b22edd681
--- /dev/null
+++ b/internal/weldrcheck/check.go
@@ -0,0 +1,108 @@
+// Package weldrcheck contains functions used to run integration tests on a running API server
+// Copyright (C) 2020 by Red Hat, Inc.
+package weldrcheck
+
+import (
+	"log"
+	"os"
+	"sort"
+	"strconv"
+
+	"github.com/osbuild/osbuild-composer/internal/client"
+)
+
+// Checks 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 checks that depend on previous results from the same function
+// not from other functions.
+// The blueprint version number may get bumped if the server has had tests run before
+// do not assume the bp version will match unless first deleting the old one.
+
+// isStringInSlice returns true if the string is present, false if not
+// slice must be sorted
+// TODO decide if this belongs in a more widely useful package location
+func isStringInSlice(slice []string, s string) bool {
+	i := sort.SearchStrings(slice, s)
+	if i < len(slice) && slice[i] == s {
+		return true
+	}
+	return false
+}
+
+// Run the API V0 checks against the server
+// Return true if all the checks pass
+func runV0Checks(socket string) (pass bool) {
+	pass = true
+
+	bpv0 := checkBlueprintsV0{socket}
+	pass = bpv0.Run()
+
+	if pass {
+		log.Println("OK: ALL V0 API checks were successful")
+	} else {
+		log.Println("FAIL: One or more V0 API checks failed")
+	}
+	return pass
+}
+
+// Run the V1 checks against the server
+func runV1Checks(socket string) (pass bool) {
+	pass = true
+
+	if pass {
+		log.Println("OK: ALL V1 API checks were successful")
+	} else {
+		log.Println("FAIL: One or more V1 API checks failed")
+	}
+	return pass
+}
+
+// Run executes all of the weldr API checks against a running API server
+// This is designed to run against any WELDR API server, not just osbuild-composer
+func Run(socket string) {
+	log.Printf("Running API check on %s", socket)
+
+	// Does the socket exist?
+	if _, err := os.Stat(socket); os.IsNotExist(err) {
+		log.Printf("ERROR: API socket %s is missing", socket)
+		os.Exit(1)
+	}
+	// Does the server respond to /api/status?
+	status, resp, err := client.GetStatusV0(socket)
+	if err != nil {
+		log.Printf("ERROR: status request failed with client error: %s", err)
+		// If status check fails there is no point in continuing
+		os.Exit(1)
+	}
+	if resp != nil {
+		log.Printf("ERROR: status request failed: %v", resp)
+		// If status check fails there is no point in continuing
+		os.Exit(1)
+	}
+	log.Print("OK: status request")
+	apiVersion, e := strconv.Atoi(status.API)
+	if e != nil {
+		log.Printf("ERROR: status API version error: %s", e)
+		log.Println("ERROR: Only running V0 checks")
+		apiVersion = 0
+	}
+	log.Printf("INFO: Running tests against: %s %s server using V%d API", status.Backend, status.Build, apiVersion)
+
+	// Run the V0 checks
+	log.Println("INFO: Running API V0 checks")
+	pass := runV0Checks(socket)
+
+	// Run the V1 checks if the server claims to support it
+	if apiVersion > 0 {
+		log.Println("INFO: Running API V1 checks")
+		passV1 := runV1Checks(socket)
+
+		pass = pass && passV1
+	}
+
+	if !pass {
+		os.Exit(1)
+	}
+	os.Exit(0)
+}
diff --git a/repositories/rhel-8.1.json b/repositories/rhel-8.1.json
new file mode 100644
index 000000000..860df8c39
--- /dev/null
+++ b/repositories/rhel-8.1.json
@@ -0,0 +1,26 @@
+{
+    "aarch64": [
+        {
+            "id": "baseos",
+            "name": "BaseOS",
+            "BaseURL": "http://download.devel.redhat.com/rhel-8/rel-eng/RHEL-8/RHEL-8.1.0-RC-1.1/compose/BaseOS/aarch64/os"
+        },
+        {
+            "id": "appstream",
+            "name": "AppStream",
+            "BaseURL": "http://download.devel.redhat.com/rhel-8/rel-eng/RHEL-8/RHEL-8.1.0-RC-1.1/compose/AppStream/aarch64/os"
+        }
+    ],
+    "x86_64": [
+        {
+            "id": "baseos",
+            "name": "BaseOS",
+            "BaseURL": "http://download.devel.redhat.com/rhel-8/rel-eng/RHEL-8/RHEL-8.1.0-RC-1.1/compose/BaseOS/x86_64/os"
+        },
+        {
+            "id": "appstream",
+            "name": "AppStream",
+            "BaseURL": "http://download.devel.redhat.com/rhel-8/rel-eng/RHEL-8/RHEL-8.1.0-RC-1.1/compose/AppStream/x86_64/os"
+        }
+    ]
+}


Test patch:
diff --git a/cmd/osbuild-dnf-json-tests/main.go b/cmd/osbuild-dnf-json-tests/main.go
index 4c56b1845..1b58a9c42 100644
--- a/cmd/osbuild-dnf-json-tests/main.go
+++ b/cmd/osbuild-dnf-json-tests/main.go
@@ -57,7 +57,7 @@ func TestFetchChecksum(quiet bool, dir string) {
 	if !quiet {
 		log.Println("Running TestFetchChecksum on:", dir)
 	}
-	rpmMetadata := rpmmd.NewRPMMD()
+	rpmMetadata := rpmmd.NewRPMMD(path.Join(dir, "rpmmd"))
 	_, c, err := rpmMetadata.FetchMetadata([]rpmmd.RepoConfig{repoCfg}, "platform:f31")
 	if err != nil {
 		log.Panic("Failed to fetch checksum:", err)
diff --git a/cmd/osbuild-tests/main.go b/cmd/osbuild-tests/main.go
index 8f2ef955b..b4c7e7d1b 100644
--- a/cmd/osbuild-tests/main.go
+++ b/cmd/osbuild-tests/main.go
@@ -96,7 +96,7 @@ func startCompose(name, outputType string) uuid.UUID {
 	if err != nil {
 		log.Fatalf("Unexpected reply: " + err.Error())
 	}
-	if reply.Status != true {
+	if !reply.Status {
 		log.Fatalf("Unexpected status %v", reply.Status)
 	}
 
@@ -123,7 +123,7 @@ func deleteCompose(id uuid.UUID) {
 	if len(reply.IDs) != 1 {
 		log.Fatalf("Unexpected number of UUIDs returned: %d", len(reply.IDs))
 	}
-	if reply.IDs[0].Status != true {
+	if !reply.IDs[0].Status {
 		log.Fatalf("Unexpected status %v", reply.IDs[0].Status)
 	}
 }
@@ -174,7 +174,7 @@ func pushBlueprint(bp *blueprint.Blueprint) {
 	if err != nil {
 		log.Fatalf("Unexpected reply: " + err.Error())
 	}
-	if reply.Status != true {
+	if !reply.Status {
 		log.Fatalf("Unexpected status %v", reply.Status)
 	}
 }
@@ -188,7 +188,7 @@ func deleteBlueprint(bp *blueprint.Blueprint) {
 	if err != nil {
 		log.Fatalf("Unexpected reply: " + err.Error())
 	}
-	if reply.Status != true {
+	if !reply.Status {
 		log.Fatalf("Unexpected status %v", reply.Status)
 	}
 }
@@ -205,7 +205,10 @@ func runComposerCLI(quiet bool, command ...string) json.RawMessage {
 		log.Printf("$ composer-cli %s\n", strings.Join(command, " "))
 	}
 
-	cmd.Start()
+	err = cmd.Start()
+	if err != nil {
+		log.Fatalf("Could not start command: %v", err)
+	}
 
 	var result json.RawMessage
 
diff --git a/internal/blueprint/blueprint_test.go b/internal/blueprint/blueprint_test.go
index 19358ef7b..6ab102ba5 100644
--- a/internal/blueprint/blueprint_test.go
+++ b/internal/blueprint/blueprint_test.go
@@ -78,7 +78,10 @@ func TestBumpVersion(t *testing.T) {
 
 	for _, c := range cases {
 		bp := c.NewBlueprint
-		bp.Initialize()
+		err := bp.Initialize()
+		if err != nil {
+			panic(err)
+		}
 
 		bp.BumpVersion(c.OldVersion)
 		if bp.Version != c.ExpectedVersion {
diff --git a/internal/client/client_test.go b/internal/client/client_test.go
new file mode 100644
index 000000000..6e90532c2
--- /dev/null
+++ b/internal/client/client_test.go
@@ -0,0 +1,218 @@
+// Package client contains functions for communicating with the API server
+// Copyright (C) 2020 by Red Hat, Inc.
+package client
+
+import (
+	"io/ioutil"
+	"log"
+	"net"
+	"net/http"
+	"os"
+	"strings"
+	"testing"
+
+	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/weldr"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+// socketPath is the path to the temporary unix domain socket
+var socketPath string
+
+func TestRequest(t *testing.T) {
+	// Make a request to the status route
+	resp, err := Request(socketPath, "GET", "/api/status", "", map[string]string{})
+	if err != nil {
+		t.Fatalf("Request good route failed: %v", err)
+	}
+	if resp.StatusCode != 200 {
+		t.Fatalf("Request good route: %d != 200", resp.StatusCode)
+	}
+
+	// Make a request to a bad route
+	resp, err = Request(socketPath, "GET", "/invalidroute", "", map[string]string{})
+	if err != nil {
+		t.Fatalf("Request bad route failed: %v", err)
+	}
+	if resp.StatusCode != http.StatusNotFound {
+		t.Fatalf("Request bad route: %d != 404", resp.StatusCode)
+	}
+
+	// Test that apiError returns an error when trying to parse non-JSON response
+	_, err = apiError(resp)
+	if err == nil {
+		t.Fatalf("apiError of a 404 response did not return an error")
+	}
+
+	// Make a request with a bad offset to trigger a JSON response with Status set to 400
+	resp, err = Request(socketPath, "GET", "/api/v0/blueprints/list?offset=bad", "", map[string]string{})
+	if err != nil {
+		t.Fatalf("Request bad offset failed: %v", err)
+	}
+	if resp.StatusCode != http.StatusBadRequest {
+		t.Fatalf("Request bad offset: %d != 400", resp.StatusCode)
+	}
+}
+
+func TestAPIErrorMsg(t *testing.T) {
+	err := APIErrorMsg{ID: "ERROR_ID", Msg: "Meaningful error message"}
+	if diff := cmp.Diff(err.String(), "ERROR_ID: Meaningful error message"); diff != "" {
+		t.Fatalf("APIErrorMsg: %s", diff)
+	}
+}
+
+func TestAPIResponse(t *testing.T) {
+	resp := APIResponse{Status: true}
+	if resp.String() != "" {
+		t.Fatalf("Empty APIResponse Errors doesn't return empty string: %v", resp.String())
+	}
+
+	resp = APIResponse{Status: false,
+		Errors: []APIErrorMsg{
+			{ID: "ONE_ERROR", Msg: "First message"},
+			{ID: "TWO_ERROR", Msg: "Second message"}},
+	}
+	if diff := cmp.Diff(resp.String(), "ONE_ERROR: First message"); diff != "" {
+		t.Fatalf("APIResponse.Error: %s", diff)
+	}
+	if diff := cmp.Diff(resp.AllErrors(), []string{"ONE_ERROR: First message", "TWO_ERROR: Second message"}); diff != "" {
+		t.Fatalf("APIErrorMsg: %s", diff)
+	}
+}
+
+func TestGetRaw(t *testing.T) {
+	// Get raw data
+	b, resp, err := GetRaw(socketPath, "GET", "/api/status")
+	if err != nil {
+		t.Fatalf("GetRaw failed with a client error: %v", err)
+	}
+	if resp != nil {
+		t.Fatalf("GetRaw request failed: %v", err)
+	}
+	if len(b) == 0 {
+		t.Fatal("GetRaw returned an empty string")
+	}
+	// Get an API error
+	b, resp, err = GetRaw(socketPath, "GET", "/api/v0/blueprints/list?offset=bad")
+	if err != nil {
+		t.Fatalf("GetRaw bad request failed with a client error: %v", err)
+	}
+	if resp == nil {
+		t.Fatalf("GetRaw bad request did not return an error: %v", b)
+	}
+	if resp.Status != false {
+		t.Fatalf("Status != false: %#v", resp)
+	}
+	if len(resp.AllErrors()) < 1 {
+		t.Fatalf("GetRaw error did not return error message: %#v", resp)
+	} else if resp.Errors[0].ID != "BadLimitOrOffset" {
+		t.Fatalf("GetRaw error ID is not BadLimitOrOffset: %#v", resp)
+	}
+}
+
+func TestGetJSONAll(t *testing.T) {
+	// Get all the projects
+	b, resp, err := GetJSONAll(socketPath, "/api/v0/projects/list")
+	if err != nil {
+		t.Fatalf("GetJSONAll failed with a client error: %v", err)
+	}
+	if resp != nil {
+		t.Fatalf("GetJSONAll request failed: %v", resp)
+	}
+	if len(b) < 100 {
+		t.Fatalf("GetJSONAll response is too short: %#v", b)
+	}
+
+	// Run it on a route that doesn't support offset/limit
+	b, resp, err = GetJSONAll(socketPath, "/api/status")
+	if err == nil {
+		t.Fatalf("GetJSONAll bad route failed: %v", b)
+	}
+	if err.Error() != "Response is missing the total value" {
+		t.Fatalf("GetJSONAll bad route has unexpected total value: %v", resp)
+	}
+}
+
+func TestPostRaw(t *testing.T) {
+	// There are no routes that accept raw POST w/o Content-Type so this ends up testing the error path
+	b, resp, err := PostRaw(socketPath, "/api/v0/blueprints/new", "nobody", nil)
+	if err != nil {
+		t.Fatalf("PostRaw bad request failed with a client error: %v", err)
+	}
+	if resp == nil {
+		t.Fatalf("PostRaw bad request did not return an error: %v", b)
+	}
+	if resp.Status != false {
+		t.Fatalf("PostRaw bad request status != false: %#v", resp)
+	}
+	if len(resp.AllErrors()) < 1 {
+		t.Fatalf("GetRaw error did not return error message: %#v", resp)
+	} else if resp.Errors[0].ID != "BlueprintsError" {
+		t.Fatalf("GetRaw error ID is not BlueprintsError: %#v", resp)
+	}
+}
+
+func TestPostTOML(t *testing.T) {
+	blueprint := `name = "test-blueprint"
+				  description = "TOML test blueprint"
+				  version = "0.0.1"`
+	b, resp, err := PostTOML(socketPath, "/api/v0/blueprints/new", blueprint)
+	if err != nil {
+		t.Fatalf("PostTOML client failed: %v", err)
+	}
+	if resp != nil {
+		t.Fatalf("PostTOML request failed: %v", resp)
+	}
+	if !strings.Contains(string(b), "true") {
+		t.Fatalf("PostTOML failed: %#v", string(b))
+	}
+}
+
+func TestPostJSON(t *testing.T) {
+	blueprint := `{"name": "test-blueprint",
+				   "description": "JSON test blueprint",
+				   "version": "0.0.1"}`
+	b, resp, err := PostJSON(socketPath, "/api/v0/blueprints/new", blueprint)
+	if err != nil {
+		t.Fatalf("PostJSON client failed: %v", err)
+	}
+	if resp != nil {
+		t.Fatalf("PostJSON request failed: %v", resp)
+	}
+	if !strings.Contains(string(b), "true") {
+		t.Fatalf("PostJSON failed: %#v", string(b))
+	}
+}
+
+func TestMain(m *testing.M) {
+	// Setup the mocked server running on a temporary domain socket
+	tmpdir, err := ioutil.TempDir("", "client_test-")
+	if err != nil {
+		panic(err)
+	}
+	defer os.RemoveAll(tmpdir)
+
+	socketPath = tmpdir + "/client_test.socket"
+	ln, err := net.Listen("unix", socketPath)
+	if err != nil {
+		panic(err)
+	}
+
+	// Create a mock API server listening on the temporary socket
+	fixture := rpmmd_mock.BaseFixture()
+	rpm := rpmmd_mock.NewRPMMDMock(fixture)
+	distro := test_distro.New()
+	logger := log.New(os.Stdout, "", 0)
+	api := weldr.New(rpm, "test_arch", distro, logger, fixture.Store)
+	server := http.Server{Handler: api}
+	defer server.Close()
+
+	go func() {
+		server.Serve(ln)
+	}()
+
+	// Run the tests
+	os.Exit(m.Run())
+}
diff --git a/internal/distro/distro_test.go b/internal/distro/distro_test.go
index 26ae738a0..b81d81992 100644
--- a/internal/distro/distro_test.go
+++ b/internal/distro/distro_test.go
@@ -42,7 +42,10 @@ func TestDistro_Pipeline(t *testing.T) {
 			continue
 		}
 		t.Run(tt.Compose.OutputFormat, func(t *testing.T) {
-			distros := distro.NewRegistry([]string{"../.."})
+			distros, err := distro.NewDefaultRegistry([]string{"../.."})
+			if err != nil {
+				t.Fatal(err)
+			}
 			d := distros.GetDistro(tt.Compose.Distro)
 			if d == nil {
 				t.Errorf("unknown distro: %v", tt.Compose.Distro)
diff --git a/internal/distro/fedora30/distro_test.go b/internal/distro/fedora30/distro_test.go
index 1e6e69975..b9498fefe 100644
--- a/internal/distro/fedora30/distro_test.go
+++ b/internal/distro/fedora30/distro_test.go
@@ -4,7 +4,7 @@ import (
 	"reflect"
 	"testing"
 
-	"github.com/osbuild/osbuild-composer/internal/distro"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora30"
 )
 
 func TestListOutputFormats(t *testing.T) {
@@ -19,8 +19,11 @@ func TestListOutputFormats(t *testing.T) {
 		"vmdk",
 	}
 
-	distros := distro.NewRegistry([]string{"../../../"})
-	f30 := distros.GetDistro("fedora-30")
+	f30, err := fedora30.New([]string{"../../../"})
+	if err != nil {
+		t.Fatal(err)
+	}
+
 	if got := f30.ListOutputFormats(); !reflect.DeepEqual(got, want) {
 		t.Errorf("ListOutputFormats() = %v, want %v", got, want)
 	}
@@ -93,8 +96,10 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			distros := distro.NewRegistry([]string{"../../../"})
-			f30 := distros.GetDistro("fedora-30")
+			f30, err := fedora30.New([]string{"../../../"})
+			if err != nil {
+				t.Fatal(err)
+			}
 			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 b138f38fd..9ddf559ec 100644
--- a/internal/distro/fedora31/distro_test.go
+++ b/internal/distro/fedora31/distro_test.go
@@ -4,7 +4,7 @@ import (
 	"reflect"
 	"testing"
 
-	"github.com/osbuild/osbuild-composer/internal/distro"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora31"
 )
 
 func TestListOutputFormats(t *testing.T) {
@@ -19,8 +19,11 @@ func TestListOutputFormats(t *testing.T) {
 		"vmdk",
 	}
 
-	distros := distro.NewRegistry([]string{"../../../"})
-	f31 := distros.GetDistro("fedora-31")
+	f31, err := fedora31.New([]string{"../../../"})
+	if err != nil {
+		t.Fatal(err)
+	}
+
 	if got := f31.ListOutputFormats(); !reflect.DeepEqual(got, want) {
 		t.Errorf("ListOutputFormats() = %v, want %v", got, want)
 	}
@@ -93,8 +96,10 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			distros := distro.NewRegistry([]string{"../../../"})
-			f31 := distros.GetDistro("fedora-31")
+			f31, err := fedora31.New([]string{"../../../"})
+			if err != nil {
+				t.Fatal(err)
+			}
 			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 0ceb192ab..862d404c6 100644
--- a/internal/distro/fedora32/distro_test.go
+++ b/internal/distro/fedora32/distro_test.go
@@ -4,7 +4,7 @@ import (
 	"reflect"
 	"testing"
 
-	"github.com/osbuild/osbuild-composer/internal/distro"
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora32"
 )
 
 func TestListOutputFormats(t *testing.T) {
@@ -19,8 +19,11 @@ func TestListOutputFormats(t *testing.T) {
 		"vmdk",
 	}
 
-	distros := distro.NewRegistry([]string{"../../../"})
-	f32 := distros.GetDistro("fedora-32")
+	f32, err := fedora32.New([]string{"../../../"})
+	if err != nil {
+		t.Fatal(err)
+	}
+
 	if got := f32.ListOutputFormats(); !reflect.DeepEqual(got, want) {
 		t.Errorf("ListOutputFormats() = %v, want %v", got, want)
 	}
@@ -93,8 +96,10 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			distros := distro.NewRegistry([]string{"../../../"})
-			f32 := distros.GetDistro("fedora-32")
+			f32, err := fedora32.New([]string{"../../../"})
+			if err != nil {
+				t.Fatal(err)
+			}
 			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/rhel81/distro_test.go b/internal/distro/rhel81/distro_test.go
new file mode 100644
index 000000000..af4350f32
--- /dev/null
+++ b/internal/distro/rhel81/distro_test.go
@@ -0,0 +1,118 @@
+package rhel81_test
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/osbuild/osbuild-composer/internal/distro/rhel81"
+)
+
+func TestListOutputFormats(t *testing.T) {
+	want := []string{
+		"ami",
+		"ext4-filesystem",
+		"openstack",
+		"partitioned-disk",
+		"qcow2",
+		"tar",
+		"vhd",
+		"vmdk",
+	}
+
+	el81, err := rhel81.New([]string{"../../../"})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if got := el81.ListOutputFormats(); !reflect.DeepEqual(got, want) {
+		t.Errorf("ListOutputFormats() = %v, want %v", got, want)
+	}
+}
+
+func TestFilenameFromType(t *testing.T) {
+	type args struct {
+		outputFormat string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    string
+		want1   string
+		wantErr bool
+	}{
+		{
+			name:  "ami",
+			args:  args{"ami"},
+			want:  "image.raw.xz",
+			want1: "application/octet-stream",
+		},
+		{
+			name:  "ext4",
+			args:  args{"ext4-filesystem"},
+			want:  "filesystem.img",
+			want1: "application/octet-stream",
+		},
+		{
+			name:  "openstack",
+			args:  args{"openstack"},
+			want:  "disk.qcow2",
+			want1: "application/x-qemu-disk",
+		},
+		{
+			name:  "partitioned-disk",
+			args:  args{"partitioned-disk"},
+			want:  "disk.img",
+			want1: "application/octet-stream",
+		},
+		{
+			name:  "qcow2",
+			args:  args{"qcow2"},
+			want:  "disk.qcow2",
+			want1: "application/x-qemu-disk",
+		},
+		{
+			name:  "tar",
+			args:  args{"tar"},
+			want:  "root.tar.xz",
+			want1: "application/x-tar",
+		},
+		{
+			name:  "vhd",
+			args:  args{"vhd"},
+			want:  "disk.vhd",
+			want1: "application/x-vhd",
+		},
+		{
+			name:  "vmdk",
+			args:  args{"vmdk"},
+			want:  "disk.vmdk",
+			want1: "application/x-vmdk",
+		},
+		{
+			name:    "invalid-output-type",
+			args:    args{"foobar"},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			el81, err := rhel81.New([]string{"../../../"})
+			if err != nil {
+				t.Fatal(err)
+			}
+			got, got1, err := el81.FilenameFromType(tt.args.outputFormat)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("FilenameFromType() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !tt.wantErr {
+				if got != tt.want {
+					t.Errorf("FilenameFromType() got = %v, want %v", got, tt.want)
+				}
+				if got1 != tt.want1 {
+					t.Errorf("FilenameFromType() got1 = %v, want %v", got1, tt.want1)
+				}
+			}
+		})
+	}
+}
diff --git a/internal/distro/rhel82/distro_test.go b/internal/distro/rhel82/distro_test.go
index 3dea1259c..c30947665 100644
--- a/internal/distro/rhel82/distro_test.go
+++ b/internal/distro/rhel82/distro_test.go
@@ -4,7 +4,7 @@ import (
 	"reflect"
 	"testing"
 
-	"github.com/osbuild/osbuild-composer/internal/distro"
+	"github.com/osbuild/osbuild-composer/internal/distro/rhel82"
 )
 
 func TestListOutputFormats(t *testing.T) {
@@ -19,9 +19,12 @@ func TestListOutputFormats(t *testing.T) {
 		"vmdk",
 	}
 
-	distros := distro.NewRegistry([]string{"../../../"})
-	rhel82 := distros.GetDistro("rhel-8.2")
-	if got := rhel82.ListOutputFormats(); !reflect.DeepEqual(got, want) {
+	el82, err := rhel82.New([]string{"../../../"})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if got := el82.ListOutputFormats(); !reflect.DeepEqual(got, want) {
 		t.Errorf("ListOutputFormats() = %v, want %v", got, want)
 	}
 }
@@ -93,9 +96,11 @@ func TestFilenameFromType(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			distros := distro.NewRegistry([]string{"../../../"})
-			rhel82 := distros.GetDistro("rhel-8.2")
-			got, got1, err := rhel82.FilenameFromType(tt.args.outputFormat)
+			el82, err := rhel82.New([]string{"../../../"})
+			if err != nil {
+				t.Fatal(err)
+			}
+			got, got1, err := el82.FilenameFromType(tt.args.outputFormat)
 			if (err != nil) != tt.wantErr {
 				t.Errorf("FilenameFromType() error = %v, wantErr %v", err, tt.wantErr)
 				return
diff --git a/internal/jobqueue/api_test.go b/internal/jobqueue/api_test.go
index 69a5e1bd1..0cdf77026 100644
--- a/internal/jobqueue/api_test.go
+++ b/internal/jobqueue/api_test.go
@@ -34,7 +34,10 @@ func TestBasic(t *testing.T) {
 
 	for _, c := range cases {
 		distroStruct := test_distro.New()
-		registry := distro_mock.NewRegistry()
+		registry, err := distro_mock.NewDefaultRegistry()
+		if err != nil {
+			t.Fatal(err)
+		}
 		api := jobqueue.New(nil, store.New(nil, distroStruct, *registry))
 
 		test.TestNonJsonRoute(t, api, false, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedResponse)
@@ -44,11 +47,14 @@ func TestBasic(t *testing.T) {
 func TestCreate(t *testing.T) {
 	id, _ := uuid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff")
 	distroStruct := test_distro.New()
-	registry := distro_mock.NewRegistry()
+	registry, err := distro_mock.NewDefaultRegistry()
+	if err != nil {
+		t.Fatal(err)
+	}
 	store := store.New(nil, distroStruct, *registry)
 	api := jobqueue.New(nil, store)
 
-	err := store.PushCompose(id, &blueprint.Blueprint{}, nil, nil, map[string]string{"test-repo": "test:foo"}, "x86_64", "qcow2", 0, nil)
+	err = store.PushCompose(id, &blueprint.Blueprint{}, nil, nil, map[string]string{"test-repo": "test:foo"}, "x86_64", "qcow2", 0, nil)
 	if err != nil {
 		t.Fatalf("error pushing compose: %v", err)
 	}
@@ -60,7 +66,10 @@ 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()
-	registry := distro_mock.NewRegistry()
+	registry, err := distro_mock.NewDefaultRegistry()
+	if err != nil {
+		t.Fatal(err)
+	}
 	store := store.New(nil, distroStruct, *registry)
 	api := jobqueue.New(nil, store)
 
diff --git a/internal/osbuild/source_test.go b/internal/osbuild/source_test.go
index f24b434cb..a2ab9bcdc 100644
--- a/internal/osbuild/source_test.go
+++ b/internal/osbuild/source_test.go
@@ -86,7 +86,7 @@ func TestSource_UnmarshalJSON(t *testing.T) {
 			if err != nil {
 				t.Errorf("Could not marshal source: %v", err)
 			}
-			if bytes.Compare(gotBytes, tt.args.data) != 0 {
+			if !bytes.Equal(gotBytes, tt.args.data) {
 				t.Errorf("Expected '%v', got '%v'", string(tt.args.data), string(gotBytes))
 			}
 			if !reflect.DeepEqual(&gotSources, sources) {
diff --git a/internal/osbuild/stage_test.go b/internal/osbuild/stage_test.go
index e8be19c3d..082518a95 100644
--- a/internal/osbuild/stage_test.go
+++ b/internal/osbuild/stage_test.go
@@ -285,7 +285,7 @@ func TestStage_UnmarshalJSON(t *testing.T) {
 			if err != nil {
 				t.Errorf("Could not marshal stage: %v", err)
 			}
-			if bytes.Compare(gotBytes, tt.args.data) != 0 {
+			if !bytes.Equal(gotBytes, tt.args.data) {
 				t.Errorf("Expected `%v`, got `%v`", string(tt.args.data), string(gotBytes))
 			}
 			if !reflect.DeepEqual(&gotStage, stage) {
diff --git a/internal/rcm/api_test.go b/internal/rcm/api_test.go
index 1e0009d00..d29d96ffe 100644
--- a/internal/rcm/api_test.go
+++ b/internal/rcm/api_test.go
@@ -49,7 +49,10 @@ func TestBasicRcmAPI(t *testing.T) {
 		{"GET", "/v1/compose/7802c476-9cd1-41b7-ba81-43c1906bce73", `{"status":"RUNNING"}`, "application/json", http.StatusBadRequest, `{"error_reason":"Compose UUID does not exist"}`},
 	}
 
-	registry := distro_mock.NewRegistry()
+	registry, err := distro_mock.NewDefaultRegistry()
+	if err != nil {
+		t.Fatal(err)
+	}
 	distroStruct := fedoratest.New()
 	api := rcm.New(nil, store.New(nil, distroStruct, *registry), rpmmd_mock.NewRPMMDMock(rpmmd_mock.BaseFixture()))
 
@@ -74,7 +77,10 @@ func TestBasicRcmAPI(t *testing.T) {
 func TestSubmitCompose(t *testing.T) {
 	// Test the most basic use case: Submit a new job and get its status.
 	distroStruct := fedoratest.New()
-	registry := distro_mock.NewRegistry()
+	registry, err := distro_mock.NewDefaultRegistry()
+	if err != nil {
+		t.Fatal(err)
+	}
 	api := rcm.New(nil, store.New(nil, distroStruct, *registry), rpmmd_mock.NewRPMMDMock(rpmmd_mock.BaseFixture()))
 
 	var submit_reply struct {
diff --git a/internal/upload/awsupload/awsupload_test.go b/internal/upload/awsupload/awsupload_test.go
index 548eed6e9..fab6cefb5 100644
--- a/internal/upload/awsupload/awsupload_test.go
+++ b/internal/upload/awsupload/awsupload_test.go
@@ -109,10 +109,13 @@ func TestAWS_S3Upload(t *testing.T) {
 	// Delete the object from S3 after the test is finished
 	defer func() {
 		s := s3.New(sess)
-		s.DeleteObject(&s3.DeleteObjectInput{
+		_, err := s.DeleteObject(&s3.DeleteObjectInput{
 			Bucket: &bucket,
 			Key:    &s3Key,
 		})
+		if err != nil {
+			panic(err)
+		}
 	}()
 
 	// Set up temporary file for downloaded
diff --git a/internal/upload/azure/azure_test.go b/internal/upload/azure/azure_test.go
index 79b4bf2d6..e649f4b62 100644
--- a/internal/upload/azure/azure_test.go
+++ b/internal/upload/azure/azure_test.go
@@ -59,10 +59,11 @@ func TestAzure_FileUpload(t *testing.T) {
 	handleErrors(t, err)
 
 	h := md5.New()
-	io.Copy(h, f)
+	_, err = io.Copy(h, f)
 	handleErrors(t, err)
 	imageChecksum := h.Sum(nil)
-	f.Close()
+	err = f.Close()
+	handleErrors(t, err)
 
 	credentials := Credentials{
 		StorageAccount:   storageAccount,
@@ -99,7 +100,8 @@ func TestAzure_FileUpload(t *testing.T) {
 	handleErrors(t, err)
 	blobData := &bytes.Buffer{}
 	reader := get.Body(azblob.RetryReaderOptions{})
-	blobData.ReadFrom(reader)
+	_, err = blobData.ReadFrom(reader)
+	handleErrors(t, err)
 	reader.Close() // The client must close the response body when finished with it
 	blobBytes := blobData.Bytes()
 	blobChecksum := md5.Sum(blobBytes)
@@ -110,7 +112,7 @@ func TestAzure_FileUpload(t *testing.T) {
 	_, _ = blobURL.Delete(ctx, azblob.DeleteSnapshotsOptionInclude, azblob.BlobAccessConditions{})
 	_ = os.Remove(fileName)
 
-	if bytes.Compare(imageChecksum, blobChecksum[:]) != 0 {
+	if !bytes.Equal(imageChecksum, blobChecksum[:]) {
 		t.Fatalf("Checksums do not match! Local file: %x, cloud blob: %x", imageChecksum, blobChecksum[:])
 	}
 
diff --git a/internal/weldr/api_test.go b/internal/weldr/api_test.go
index b82a0d074..a2d0daa94 100644
--- a/internal/weldr/api_test.go
+++ b/internal/weldr/api_test.go
@@ -484,7 +484,7 @@ func TestComposeDelete(t *testing.T) {
 
 		idsInStore := []string{}
 
-		for id, _ := range s.Composes {
+		for id := range s.Composes {
 			idsInStore = append(idsInStore, id.String())
 		}
 
@@ -593,7 +593,10 @@ func TestComposeLogs(t *testing.T) {
 
 		var buffer bytes.Buffer
 
-		io.Copy(&buffer, tr)
+		_, err = io.Copy(&buffer, tr)
+		if err != nil {
+			t.Errorf("cannot copy untar result: %v", err)
+		}
 
 		if buffer.String() != c.ExpectedFileContent {
 			t.Errorf("%s: expected log content: %s, but got: %s", c.Path, c.ExpectedFileContent, buffer.String())