Repository: osbuild/osbuild-composer
Pull request #269 (2020-03-03 00:48:21)

distribution: set CACHE_DIRECTORY in the .service file
systemd >= 240 sets this variable to `/var/cache/` + the value of
CacheDirectory. osbuild-composer must run on earlier versions though
(specifically RHEL 8.2).


Patch:
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 4aadf84f2..5dbe566cd 100644
--- a/cmd/osbuild-composer/main.go
+++ b/cmd/osbuild-composer/main.go
@@ -80,7 +80,11 @@ func main() {
 	}
 
 	rpm := rpmmd.NewRPMMD(path.Join(cacheDirectory, "rpmmd"))
-	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 {
+		log.Fatalf("Error loading distros: %v", err)
+	}
 
 	distribution, err := distros.FromHost()
 	if err != nil {
diff --git a/cmd/osbuild-pipeline/main.go b/cmd/osbuild-pipeline/main.go
index b95343d2d..4349e16d1 100644
--- a/cmd/osbuild-pipeline/main.go
+++ b/cmd/osbuild-pipeline/main.go
@@ -64,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)
diff --git a/distribution/osbuild-composer.service b/distribution/osbuild-composer.service
index bd24805ce..09907ae83 100644
--- a/distribution/osbuild-composer.service
+++ b/distribution/osbuild-composer.service
@@ -13,5 +13,8 @@ WorkingDirectory=/usr/libexec/osbuild-composer/
 User=_osbuild-composer
 Restart=on-failure
 
+# systemd >= 240 sets this, but osbuild-composer runs on earlier versions
+Environment="CACHE_DIRECTORY=/var/cache/osbuild-composer"
+
 [Install]
 WantedBy=multi-user.target
diff --git a/golang-github-osbuild-composer.spec b/golang-github-osbuild-composer.spec
index cc559cecc..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/
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/distro/distro.go b/internal/distro/distro.go
index 56432f211..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"
@@ -70,52 +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")
-	}
-	distros.register(f30)
-	f31 := fedora31.New(confPaths)
-	if f31 == nil {
-		panic("Attempt to register Fedora 31 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(f31)
-	f32 := fedora32.New(confPaths)
-	if f32 == nil {
-		panic("Attempt to register Fedora 32 failed")
+	f31, err := fedora31.New(confPaths)
+	if err != nil {
+		return nil, fmt.Errorf("error loading fedora31: %v", err)
 	}
-	distros.register(f32)
-	el81 := rhel81.New(confPaths)
-	if el81 == nil {
-		panic("Attempt to register RHEL 8.1 failed")
+	f32, err := fedora32.New(confPaths)
+	if err != nil {
+		return nil, fmt.Errorf("error loading fedora32: %v", err)
 	}
-	distros.register(el81)
-	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
index c4129b58d..397eadb22 100644
--- a/internal/distro/rhel81/distro.go
+++ b/internal/distro/rhel81/distro.go
@@ -2,6 +2,7 @@ package rhel81
 
 import (
 	"errors"
+	"fmt"
 	"log"
 	"sort"
 	"strconv"
@@ -47,7 +48,7 @@ type output struct {
 const Distro = common.RHEL81
 const ModulePlatformID = "platform:el8"
 
-func New(confPaths []string) *RHEL81 {
+func New(confPaths []string) (*RHEL81, error) {
 	const GigaByte = 1024 * 1024 * 1024
 
 	r := RHEL81{
@@ -70,8 +71,7 @@ func New(confPaths []string) *RHEL81 {
 
 	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) *RHEL81 {
 		},
 	}
 
-	return &r
+	return &r, nil
 }
 
 func (r *RHEL81) Name() string {
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/job.go b/internal/jobqueue/job.go
index 5e63ada4a..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)
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/weldr/api.go b/internal/weldr/api.go
index a5dbaeac3..05200d715 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -1117,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,
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)
+}


Test patch:
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
index 9950c1eb2..af4350f32 100644
--- a/internal/distro/rhel81/distro_test.go
+++ b/internal/distro/rhel81/distro_test.go
@@ -4,7 +4,7 @@ import (
 	"reflect"
 	"testing"
 
-	"github.com/osbuild/osbuild-composer/internal/distro"
+	"github.com/osbuild/osbuild-composer/internal/distro/rhel81"
 )
 
 func TestListOutputFormats(t *testing.T) {
@@ -19,9 +19,12 @@ func TestListOutputFormats(t *testing.T) {
 		"vmdk",
 	}
 
-	distros := distro.NewRegistry([]string{"../../../"})
-	rhel81 := distros.GetDistro("rhel-8.2")
-	if got := rhel81.ListOutputFormats(); !reflect.DeepEqual(got, want) {
+	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)
 	}
 }
@@ -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{"../../../"})
-			rhel81 := distros.GetDistro("rhel-8.2")
-			got, got1, err := rhel81.FilenameFromType(tt.args.outputFormat)
+			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
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/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 {