Repository: osbuild/osbuild-composer
Pull request #440 (2020-03-31 20:24:40)

store: some random cleanups around PushCompose()
These fell out of work I'm doing on another branch. Makes sense to get them in separately to no make the other one too large.


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index fc6d8863a..2155b68b2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,10 +1,14 @@
 # Contributing to osbuild-composer
 
-First of all, thank you for taking the time to contribute to osbuild-composer. In this document you will find information that can help you with your contribution.
+First of all, thank you for taking the time to contribute to osbuild-composer.
+In this document you will find information that can help you with your
+contribution.
 
 ### Running from sources
 
-We recommend using the latest stable Fedora for development but latest minor release of RHEL/CentOS 8 should work just fine as well. To run osbuild-composer from sources, follow these steps:
+We recommend using the latest stable Fedora for development but latest minor
+release of RHEL/CentOS 8 should work just fine as well. To run osbuild-composer
+from sources, follow these steps:
 
 1. Clone the repository and create a local checkout:
 
@@ -22,7 +26,8 @@ $ sudo dnf -y install cockpit-composer             # Optional: Install cockpit i
 $ sudo systemctl start cockpit.socket              # Optional: Start cockpit
 ```
 
-3. Finally, use the following to compile the project from the working directory, install it, and then run it:
+3. Finally, use the following to compile the project from the working
+directory, install it, and then run it:
 
 ```
 $ make rpm
@@ -30,11 +35,15 @@ $ sudo dnf -y install rpmbuild/RPMS/x86_64/*
 $ sudo systemctl start osbuild-composer.socket
 ```
 
-You can now open https://localhost:9090 in your browser to open cockpit console and check the "Image Builder" section.
+You can now open https://localhost:9090 in your browser to open cockpit console
+and check the "Image Builder" section.
 
-Alternatively you can use `composer-cli` to interact with the Weldr API. We don't have any client for the RCM API, so the only option there is a plain `curl`.
+Alternatively you can use `composer-cli` to interact with the Weldr API. We
+don't have any client for the RCM API, so the only option there is a
+plain `curl`.
 
-When developing the code, use `go` executable to build, run, and test you code [1]:
+When developing the code, use `go` executable to build, run, and test you
+code [1]:
 
 ```
 $ go test ./...
@@ -48,21 +57,29 @@ See [test/README.md](test/README.md) for more information about testing.
 
 ### Planning the work
 
-In general we encourage you to first fill in an issue and discuss the feature you would like to work on before you start. This can prevent the scenario where you work on something we don't want to include in our code.
+In general we encourage you to first fill in an issue and discuss the feature
+you would like to work on before you start. This can prevent the scenario where
+you work on something we don't want to include in our code.
 
-That being said, you are of course welcome to implement an example of what you would like to achieve.
+That being said, you are of course welcome to implement an example of what you
+would like to achieve.
 
 ### Creating a PR
 
 The commits in the PR should have these properties:
 
 * Preferably minimal and well documented
-  * Where minimal means: don't do unrelated changes even if the code is obviously wrong
+  * Where minimal means: don't do unrelated changes even if the code is
+    obviously wrong
   * Well documented: both code and commit message
-  * The commit message should start with the module you work on, like: `weldr: `, or `rcm:`
-* The code should compile and the composer should start, so that we can run `git bisect` on it
+  * The commit message should start with the module you work on,
+    like: `weldr: `, or `rcm:`
+* The code should compile and the composer should start, so that we can run
+  `git bisect` on it
 * All code should be covered by unit-tests
 
 ### Notes:
 
-[1] If you are running macOS, you can still compile osbuild-composer. If it doesn't work out of the box, use `-tags macos` together with any `go` command, for example: `go test -tags macos ./...`
+[1] If you are running macOS, you can still compile osbuild-composer. If it
+    doesn't work out of the box, use `-tags macos` together with any `go`
+    command, for example: `go test -tags macos ./...`
diff --git a/Makefile b/Makefile
index b5773b5a3..27495e59c 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,110 @@
-COMMIT=$(shell git rev-parse HEAD)
+#
+# Maintenance Helpers
+#
+# This makefile contains targets used for development, as well as helpers to
+# aid automatization of maintenance. Unless a target is documented in
+# `make help`, it is not supported and is only meant to be used by developers
+# to aid their daily development work.
+#
+# All supported targets honor the `SRCDIR` variable to find the source-tree.
+# For most unsupported targets, you are expected to have the source-tree as
+# your working directory. To specify a different source-tree, simply override
+# the variable via `SRCDIR=<path>` on the commandline. By default, the working
+# directory is used for build output, but `BUILDDIR=<path>` allows overriding
+# it.
+#
+
+BUILDDIR ?= .
+SRCDIR ?= .
+
+RST2MAN ?= rst2man
+
+#
+# Automatic Variables
+#
+# This section contains a bunch of automatic variables used all over the place.
+# They mostly try to fetch information from the repository sources to avoid
+# hard-coding them in this makefile.
+#
+# Most of the variables here are pre-fetched so they will only ever be
+# evaluated once. This, however, means they are always executed regardless of
+# which target is run.
+#
+#     VERSION:
+#         This evaluates the `Version` field of the specfile. Therefore, it will
+#         be set to the latest version number of this repository without any
+#         prefix (just a plain number).
+#
+#     COMMIT:
+#         This evaluates to the latest git commit sha. This will not work if
+#         the source is not a git checkout. Hence, this variable is not
+#         pre-fetched but evaluated at time of use.
+#
+
+VERSION := $(shell (cd "$(SRCDIR)" && grep "^Version:" osbuild-composer.spec | sed 's/.*\([[:digit:]]\+\).*/\1/'))
+COMMIT = $(shell (cd "$(SRCDIR)" && git rev-parse HEAD))
+
+#
+# Generic Targets
+#
+# The following is a set of generic targets used across the makefile. The
+# following targets are defined:
+#
+#     help
+#         This target prints all supported targets. It is meant as
+#         documentation of targets we support and might use outside of this
+#         repository.
+#         This is also the default target.
+#
+#     $(BUILDDIR)/
+#     $(BUILDDIR)/%/
+#         This target simply creates the specified directory. It is limited to
+#         the build-dir as a safety measure. Note that this requires you to use
+#         a trailing slash after the directory to not mix it up with regular
+#         files. Lastly, you mostly want this as order-only dependency, since
+#         timestamps on directories do not affect their content.
+#
+
+.PHONY: help
+help:
+	@echo "make [TARGETS...]"
+	@echo
+	@echo "This is the maintenance makefile of osbuild. The following"
+	@echo "targets are available:"
+	@echo
+	@echo "    help:               Print this usage information."
+	@echo "    man:                Generate all man-pages"
+
+$(BUILDDIR)/:
+	mkdir -p "$@"
+
+$(BUILDDIR)/%/:
+	mkdir -p "$@"
+
+#
+# Documentation
+#
+# The following targets build the included documentation. This includes the
+# packaged man-pages, but also all other kinds of documentation that needs to
+# be generated. Note that these targets are relied upon by automatic
+# deployments to our website, as well as package manager scripts.
+#
+
+MANPAGES_RST = $(wildcard $(SRCDIR)/docs/*.[0123456789].rst)
+MANPAGES_TROFF = $(patsubst $(SRCDIR)/%.rst,$(BUILDDIR)/%,$(MANPAGES_RST))
+
+$(MANPAGES_TROFF): $(BUILDDIR)/docs/%: $(SRCDIR)/docs/%.rst | $(BUILDDIR)/docs/
+	$(RST2MAN) "$<" "$@"
+
+.PHONY: man
+man: $(MANPAGES_TROFF)
+
+#
+# Maintenance Targets
+#
+# The following targets are meant for development and repository maintenance.
+# They are not supported nor is their use recommended in scripts.
+#
 
 .PHONY: build
 build:
@@ -106,3 +212,66 @@ old-rpm: $(OLD_RPM_SPECFILE) $(RPM_TARBALL)
 	rpmbuild -bb \
 		--define "_topdir $(CURDIR)/rpmbuild" \
 		$(OLD_RPM_SPECFILE)
+
+#
+# Releasing
+#
+
+NEXT_VERSION := $(shell expr "$(VERSION)" + 1)
+
+.PHONY: release
+release:
+	@echo
+	@echo "Checklist for release of osbuild-composer-$(NEXT_VERSION):"
+	@echo
+	@echo " * Create news entry in NEWS.md with a short description of"
+	@echo "   any changes since the last release, which are relevant to"
+	@echo "   users, packagers, distributors, or dependent projects."
+	@echo
+	@echo "   Use the following template, break lines at 80ch:"
+	@echo
+	@echo "--------------------------------------------------------------------------------"
+	@echo "## CHANGES WITH $(NEXT_VERSION):"
+	@echo
+	@echo "        * ..."
+	@echo
+	@echo "        * ..."
+	@echo
+	@echo -n "        Contributions from: "
+	# We omit the contributor list if `git log` fails. If you hit this,
+	# consider fetching missing tags via `git fetch --tags`, or just copy
+	# this command and remove the stderr-redirect.
+	@echo `( git log --format='%an, ' v$(VERSION)..HEAD 2>/dev/null | sort -u | tr -d '\n' | sed 's/, $$//' ) || echo`
+	@echo
+	@echo "        - Location, YYYY-MM-DD"
+	@echo "--------------------------------------------------------------------------------"
+	@echo
+	@echo "   To get a list of changes since the last release, you may use:"
+	@echo
+	@echo "        git log v$(VERSION)..HEAD"
+	@echo
+	@echo " * Bump the project version. The canonical location so far is"
+	@echo "   'osbuild-composer.spec'."
+	@echo
+	@echo " * Make sure the spec-file is updated for the new release and"
+	@echo "   correctly supports all new features. This should already be"
+	@echo "   done by previous commits that introduced the changes, but"
+	@echo "   a sanity check does not hurt."
+	@echo
+	@echo " * Commit the version bump, specfile changes and NEWS.md in any"
+	@echo "   order you want."
+	@echo
+	@echo " * Tag the release via:"
+	@echo
+	@echo "        git tag -s -m 'osbuild-composer $(NEXT_VERSION)' v$(NEXT_VERSION) HEAD"
+	@echo
+	@echo " * Push master as well as the tag:"
+	@echo
+	@echo "        git push origin master"
+	@echo "        git push origin v$(NEXT_VERSION)"
+	@echo
+	@echo " * Create a release on github. Use 'NEWS.md' verbatim from the"
+	@echo "   top until the end of the section for this release as release"
+	@echo "   notes. Use 'v$(NEXT_VERSION)' as release name and as tag for"
+	@echo "   the release."
+	@echo
diff --git a/NEWS.md b/NEWS.md
new file mode 100644
index 000000000..d280e037f
--- /dev/null
+++ b/NEWS.md
@@ -0,0 +1,47 @@
+# OSBuild Composer - Operating System Image Composition Services
+
+## CHANGES WITH 8:
+
+        * All generated pipelines now use the `org.osbuild.rpm` stage of
+          *osbuild*, rather than `org.osbuild.dnf`. This improves on splitting
+          resource acquisition from image building and should make image
+          composition more reliable and faster.
+
+        * The `STATE_DIRECTORY` environment variable now allows changing the
+          state directory path of `osbuild-composer`. This is to support older
+          systemd versions that do not pass in `StateDirectory=` to the service
+          executable.
+
+        * Minor fixes and improvements all over the place.
+
+        Contributions from: Alexander Todorov, Brian C. Lane, Jacob Kozol, Jakub
+                            Rusz, Lars Karlitski, Major Hayden, Martin
+                            Sehnoutka, Ondřej Budai, Tom Gundersen
+
+        - Berlin, 2020-03-18
+
+## CHANGES WITH 7:
+
+        * Support for `RHEL 8.1` as image type is now available.
+
+        * Semantic versioning of blueprints in the lorax API is now enforced.
+          This was always the case for the original lorax API, and *Composer*
+          now follows this as well.
+
+        * Lots of internal improvements, including many automatic tests,
+          improved error handling, better cache directory management, as well
+          as preparations to move over from `org.osbuild.dnf` to
+          `org.osbuild.rpm` in all build pipelines.
+
+        Contributions from: Alexander Todorov, Brian C. Lane, Jacob Kozol, Lars
+                            Karlitski, Major Hayden, Ondřej Budai, Tom Gundersen
+
+        - Berlin, 2020-03-05
+
+## CHANGES BEFORE 7:
+
+        * Initial implementation of 'osbuild-composer'.
+
+        Contributions from: Alexander Todorov, Brian C. Lane, Christian Kellner,
+                            Jacob Kozol, Jakub Rusz, Lars Karlitski, Martin
+                            Sehnoutka, Ondřej Budai, Tom Gundersen
diff --git a/README.md b/README.md
index 8117742b8..de5271cb8 100644
--- a/README.md
+++ b/README.md
@@ -1,62 +1,82 @@
-# osbuild-composer
+OSBuild Composer
+================
 
-An HTTP service for building bootable OS images. It provides the same API as [lorax-composer](https://github.com/weldr/lorax) but in the background it uses [osbuild](https://github.com/osbuild/osbuild) to create the images.
+Operating System Image Composition Services
 
-You can control it in [Cockpit](https://github.com/weldr/cockpit-composer) or using the [composer-cli](https://weldr.io/lorax/composer-cli.html). To get started on Fedora, run:
+The composer project is a set of HTTP services for composing operating system
+images. It builds on the pipeline execution engine of *osbuild* and defines
+its own class of images that it supports building.
 
-```
-# dnf install cockpit-composer osbuild-composer composer-cli
-# systemctl enable --now cockpit.socket
-# systemctl enable --now osbuild-composer.socket
-```
+Multiple APIs are available to access a composer service. This includes
+support for the [lorax-composer](https://github.com/weldr/lorax) API, and as
+such can serve as drop-in replacement for lorax-composer.
 
-Now you can access the service using `composer-cli`, for example:
+You can control a composer instance either directly via the provided APIs, or
+through higher-level user-interfaces from external projects. This, for
+instance, includes a
+[Cockpit Module](https://github.com/weldr/cockpit-composer) or using the
+[composer-cli](https://weldr.io/lorax/composer-cli.html) command-line tool.
 
-```
-composer-cli status show
-```
+### Project
 
-or using a browser: `http://localhost:9090`
+ * **Website**: <https://www.osbuild.org>
+ * **Bug Tracker**: <https://github.com/osbuild/osbuild-composer/issues>
 
-## API documentation
+### About
 
-Please refer to the [lorax-composer](https://github.com/weldr/lorax)'s documenation as osbuild-composer is a drop-in replacement.
+Composer is a middleman between the workhorses from *osbuild* and the
+user-interfaces like *cockpit-composer*, *composer-cli*, or others. It defines
+a set of high-level image compositions that it supports building. Builds of
+these compositions can be requested via the different APIs of *Composer*, which
+will then translate the requests into pipeline-descriptions for *osbuild*. The
+pipeline output is then either provided back to the user, or uploaded to a user
+specified target.
 
-## High-level overview
+The following image visualizes the overall architecture of the OSBuild
+infrastructure and the place that *Composer* takes:
 
 ![overview](docs/osbuild-composer.svg)
 
-### Frontends
+Consult the `osbuild-composer(7)` man-page for an introduction into composer,
+information on running your own composer instance, as well as details on the
+provided infrastructure and services.
+
+### Requirements
+
+The requirements for this project are:
 
-`osbuild-composer` is meant to be used with 2 different front-ends. The primary one, which is meant for general use, is cockpit-composer. It is part of the Cockpit project and unless you have a strong reason not to use it, you should use it. `composer-cli` is a command line tool that can be used with `osbuild-composer`.
+ * `osbuild >= 7`
+ * `systemd >= 244`
 
-### Compose
-* Compose is what the user submits over one of the frontends
-* It contains of one or more **image builds**
-* It contains zero or more **upload actions**
+At build-time, the following software is required:
 
-### Image build
-* The resulting *image* has a *type*: https://github.com/osbuild/osbuild-composer/blob/master/internal/distro/fedora30/distro.go#L19
-* Running build in osbuild-composer is referred to as a "job" (internal terminology, not related to end-user experience)
+ * `go >= 1.14`
+ * `python-docutils >= 0.13`
 
-### Job
-* What composer submits to a worker
-* Is a unit of work performed by `osbuild` (internally it is a single execution of `osbuild`)
-* Consists of **one** image build and **zero or more** Upload actions
+### Build
 
-### Image type
-* In the cockpit-composer, for examples these are image types:
-  * Openstack
-  * Azure
-  * AWS
-* As of now, we name them internally by their file format: vhd, ami, etc.
-* You can see a list of types by executing: `composer-cli compose types`
+The standard go package system is used. Consult upstream documentation for
+detailed help. In most situations the following commands are sufficient to
+build and install from source:
+
+```sh
+mkdir build
+go build -o build ./...
+```
+
+The man-pages require `python-docutils` and can be built via:
+
+```sh
+make man
+```
 
-### Upload action
-* Each image can be, but does not have to be, uploaded to a remote location
-* One image can be uploaded to multiple locations
+### Repository:
 
+ - **web**:   <https://github.com/osbuild/osbuild-composer>
+ - **https**: `https://github.com/osbuild/osbuild-composer.git`
+ - **ssh**:   `git@github.com:osbuild/osbuild-composer.git`
 
-## Testing
+### License:
 
-See [test/README.md](test/README.md)
+ - **Apache-2.0**
+ - See LICENSE file for details.
diff --git a/docs/osbuild-composer.7.rst b/docs/osbuild-composer.7.rst
new file mode 100644
index 000000000..ca6884ea6
--- /dev/null
+++ b/docs/osbuild-composer.7.rst
@@ -0,0 +1,77 @@
+================
+osbuild-composer
+================
+
+------------------------
+OSBuild Composer Service
+------------------------
+
+:Manual section: 7
+:Manual group: Miscellaneous
+
+DESCRIPTION
+===========
+
+The composer project is a set of HTTP services for composing operating system
+images. It builds on the pipeline execution engine of *osbuild* [#osbuild]_ and
+defines its own class of images that it supports building.
+
+Multiple APIs are available to access a composer service. This includes
+support for the *lorax-composer* [#lorax-github]_ API, and as
+such can serve as drop-in replacement for lorax-composer.
+
+You can control a composer instance either directly via the provided APIs, or
+through higher-level user-interfaces from external projects. This, for
+instance, includes a *Cockpit Module* [#cockpit-composer]_ or using the
+*composer-cli* [#composer-cli]_ command-line tool.
+
+Frontends
+---------
+
+*Composer* does not ship with frontends itself. However, several external
+frontends for *Composer* already exist. These include:
+
+**Cockpit Composer**
+    This module for *Cockpit* [#cockpit]_ allows a great level of control of a
+    *Composer* instance running on a cockpit-managed machine.
+
+**Composer CLI**
+    This command-line tool originated in the *lorax* [#lorax-github]_ project,
+    but can be used with *Composer* just as well.
+
+RUNNING
+=======
+
+To deploy a composer instance, all you need is to 
+
+    |
+    | # systemctl start osbuild-composer.socket
+    |
+
+Now you can access the service using `composer-cli`, for example:
+
+    |
+    | # composer-cli status show
+    |
+
+or using *Cockpit* with the *Cockpit Composer* module from a
+browser: `http://localhost:9090`
+
+SEE ALSO
+========
+
+``osbuild``\(1), ``osbuild-manifest``\(5)
+
+NOTES
+=====
+
+.. [#osbuild] OSBuild:
+              https://www.osbuild.org
+.. [#lorax-github] Lorax Composer:
+                   https://github.com/weldr/lorax
+.. [#cockpit-composer] Cockpit Composer:
+                       https://github.com/weldr/cockpit-composer
+.. [#composer-cli] Composer CLI:
+                   https://weldr.io/lorax/composer-cli.html
+.. [#cockpit] Cockpit Project:
+              https://www.cockpit-project.org/
diff --git a/internal/rcm/api.go b/internal/rcm/api.go
index da4216776..de0978e82 100644
--- a/internal/rcm/api.go
+++ b/internal/rcm/api.go
@@ -205,9 +205,7 @@ func (api *API) submit(writer http.ResponseWriter, request *http.Request, _ http
 	}
 
 	// Push the requested compose to the store
-	composeUUID := uuid.New()
-	// nil is used as an upload target, because LocalTarget is already used in the PushCompose function
-	err = api.store.PushCompose(distro, arch, imageType, composeUUID, &blueprint.Blueprint{}, repoConfigs, packages, buildPackages, 0, nil)
+	composeID, err := api.store.PushCompose(imageType, &blueprint.Blueprint{}, repoConfigs, packages, buildPackages, 0, nil)
 	if err != nil {
 		if api.logger != nil {
 			api.logger.Println("RCM API failed to push compose:", err)
@@ -223,7 +221,7 @@ func (api *API) submit(writer http.ResponseWriter, request *http.Request, _ http
 	var reply struct {
 		UUID uuid.UUID `json:"compose_id"`
 	}
-	reply.UUID = composeUUID
+	reply.UUID = composeID
 	// TODO: handle error
 	_ = json.NewEncoder(writer).Encode(reply)
 }
diff --git a/internal/store/store.go b/internal/store/store.go
index d2a6d192d..a073eab59 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -553,11 +553,13 @@ func (s *Store) getImageBuildDirectory(composeID uuid.UUID, imageBuildID int) st
 	return fmt.Sprintf("%s/%d", s.getComposeDirectory(composeID), imageBuildID)
 }
 
-func (s *Store) PushCompose(distro distro.Distro, arch distro.Arch, imageType distro.ImageType, composeID uuid.UUID, bp *blueprint.Blueprint, repos []rpmmd.RepoConfig, packages, buildPackages []rpmmd.PackageSpec, size uint64, targets []*target.Target) error {
+func (s *Store) PushCompose(imageType distro.ImageType, bp *blueprint.Blueprint, repos []rpmmd.RepoConfig, packages, buildPackages []rpmmd.PackageSpec, size uint64, targets []*target.Target) (uuid.UUID, error) {
 	if targets == nil {
 		targets = []*target.Target{}
 	}
 
+	composeID := uuid.New()
+
 	// Compatibility layer for image types in Weldr API v0
 	imageTypeCommon, exists := common.ImageTypeFromCompatString(imageType.Name())
 	if !exists {
@@ -569,18 +571,13 @@ func (s *Store) PushCompose(distro distro.Distro, arch distro.Arch, imageType di
 
 		err := os.MkdirAll(outputDir, 0755)
 		if err != nil {
-			return fmt.Errorf("cannot create output directory for job %v: %#v", composeID, err)
+			return uuid.Nil, fmt.Errorf("cannot create output directory for job %v: %#v", composeID, err)
 		}
 	}
 
-	allRepos := append([]rpmmd.RepoConfig{}, repos...)
-	for _, source := range s.Sources {
-		allRepos = append(allRepos, source.RepoConfig())
-	}
-
-	manifestStruct, err := imageType.Manifest(bp.Customizations, allRepos, packages, buildPackages, imageType.Size(size))
+	manifestStruct, err := imageType.Manifest(bp.Customizations, repos, packages, buildPackages, imageType.Size(size))
 	if err != nil {
-		return err
+		return uuid.Nil, err
 	}
 	// FIXME: handle or comment this possible error
 	_ = s.change(func() error {
@@ -606,7 +603,7 @@ func (s *Store) PushCompose(distro distro.Distro, arch distro.Arch, imageType di
 		Targets:      targets,
 	}
 
-	return nil
+	return composeID, nil
 }
 
 // DeleteCompose deletes the compose from the state file and also removes all files on disk that are
diff --git a/internal/weldr/api.go b/internal/weldr/api.go
index 68cdf9e54..b4696d89e 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -1458,12 +1458,7 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 		return
 	}
 
-	reply := ComposeReply{
-		BuildID: uuid.New(),
-		Status:  true,
-	}
-
-	err = api.store.PushCompose(api.distro, api.arch, imageType, reply.BuildID, bp, api.allRepositories(), packages, buildPackages, cr.Size, targets)
+	composeID, err := api.store.PushCompose(imageType, bp, api.allRepositories(), packages, buildPackages, cr.Size, targets)
 
 	// TODO: we should probably do some kind of blueprint validation in future
 	// for now, let's just 500 and bail out
@@ -1477,7 +1472,10 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 		return
 	}
 
-	err = json.NewEncoder(writer).Encode(reply)
+	err = json.NewEncoder(writer).Encode(ComposeReply{
+		BuildID: composeID,
+		Status:  true,
+	})
 	common.PanicOnError(err)
 }
 


Test patch:
diff --git a/internal/jobqueue/api_test.go b/internal/jobqueue/api_test.go
index 85bcb0656..382dccbe0 100644
--- a/internal/jobqueue/api_test.go
+++ b/internal/jobqueue/api_test.go
@@ -39,7 +39,6 @@ func TestBasic(t *testing.T) {
 }
 
 func TestCreate(t *testing.T) {
-	id := uuid.MustParse("ffffffff-ffff-ffff-ffff-ffffffffffff")
 	distroStruct := fedoratest.New()
 	arch, err := distroStruct.GetArch("x86_64")
 	if err != nil {
@@ -52,17 +51,16 @@ func TestCreate(t *testing.T) {
 	store := store.New(nil)
 	api := jobqueue.New(nil, store)
 
-	err = store.PushCompose(distroStruct, arch, imageType, id, &blueprint.Blueprint{}, nil, nil, nil, 0, nil)
+	id, err := store.PushCompose(imageType, &blueprint.Blueprint{}, nil, nil, nil, 0, nil)
 	if err != nil {
 		t.Fatalf("error pushing compose: %v", err)
 	}
 
 	test.TestRoute(t, api, false, "POST", "/job-queue/v1/jobs", `{}`, http.StatusCreated,
-		`{"id":"ffffffff-ffff-ffff-ffff-ffffffffffff","image_build_id":0,"manifest":{"sources":{},"pipeline":{}},"targets":[]}`, "created", "uuid")
+		`{"id":"`+id.String()+`","image_build_id":0,"manifest":{"sources":{},"pipeline":{}},"targets":[]}`, "created")
 }
 
 func testUpdateTransition(t *testing.T, from, to string, expectedStatus int, expectedResponse string) {
-	id := uuid.MustParse("ffffffff-ffff-ffff-ffff-ffffffffffff")
 	distroStruct := fedoratest.New()
 	arch, err := distroStruct.GetArch("x86_64")
 	if err != nil {
@@ -75,20 +73,22 @@ func testUpdateTransition(t *testing.T, from, to string, expectedStatus int, exp
 	store := store.New(nil)
 	api := jobqueue.New(nil, store)
 
+	id := uuid.Nil
 	if from != "VOID" {
-		err := store.PushCompose(distroStruct, arch, imageType, id, &blueprint.Blueprint{}, nil, nil, nil, 0, nil)
+		id, err = store.PushCompose(imageType, &blueprint.Blueprint{}, nil, nil, nil, 0, nil)
 		if err != nil {
 			t.Fatalf("error pushing compose: %v", err)
 		}
+
 		if from != "WAITING" {
 			test.SendHTTP(api, false, "POST", "/job-queue/v1/jobs", `{}`)
 			if from != "RUNNING" {
-				test.SendHTTP(api, false, "PATCH", "/job-queue/v1/jobs/ffffffff-ffff-ffff-ffff-ffffffffffff/builds/0", `{"status":"`+from+`"}`)
+				test.SendHTTP(api, false, "PATCH", "/job-queue/v1/jobs/"+id.String()+"/builds/0", `{"status":"`+from+`"}`)
 			}
 		}
 	}
 
-	test.TestNonJsonRoute(t, api, false, "PATCH", "/job-queue/v1/jobs/ffffffff-ffff-ffff-ffff-ffffffffffff/builds/0", `{"status":"`+to+`"}`, expectedStatus, expectedResponse)
+	test.TestNonJsonRoute(t, api, false, "PATCH", "/job-queue/v1/jobs/"+id.String()+"/builds/0", `{"status":"`+to+`"}`, expectedStatus, expectedResponse)
 }
 
 func TestUpdate(t *testing.T) {
diff --git a/internal/osbuild/rpm_stage_test.go b/internal/osbuild/rpm_stage_test.go
new file mode 100644
index 000000000..caa4e90cd
--- /dev/null
+++ b/internal/osbuild/rpm_stage_test.go
@@ -0,0 +1,16 @@
+package osbuild
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewRPMStage(t *testing.T) {
+	expectedStage := &Stage{
+		Name:    "org.osbuild.rpm",
+		Options: &RPMStageOptions{},
+	}
+	actualStage := NewRPMStage(&RPMStageOptions{})
+	assert.Equal(t, expectedStage, actualStage)
+}
diff --git a/internal/osbuild/script_stage_test.go b/internal/osbuild/script_stage_test.go
new file mode 100644
index 000000000..1c181df7d
--- /dev/null
+++ b/internal/osbuild/script_stage_test.go
@@ -0,0 +1,24 @@
+package osbuild
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewScriptStageOptions(t *testing.T) {
+	expectedOptions := &ScriptStageOptions{
+		Script: "/root/test.sh",
+	}
+	actualOptions := NewScriptStageOptions("/root/test.sh")
+	assert.Equal(t, expectedOptions, actualOptions)
+}
+
+func TestNewScriptStage(t *testing.T) {
+	expectedStage := &Stage{
+		Name:    "org.osbuild.script",
+		Options: &ScriptStageOptions{},
+	}
+	actualStage := NewScriptStage(&ScriptStageOptions{})
+	assert.Equal(t, expectedStage, actualStage)
+}
diff --git a/internal/osbuild/selinux_stage_test.go b/internal/osbuild/selinux_stage_test.go
new file mode 100644
index 000000000..9aaf56209
--- /dev/null
+++ b/internal/osbuild/selinux_stage_test.go
@@ -0,0 +1,24 @@
+package osbuild
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewSELinuxStageOptions(t *testing.T) {
+	expectedOptions := &SELinuxStageOptions{
+		FileContexts: "etc/selinux/targeted/contexts/files/file_contexts",
+	}
+	actualOptions := NewSELinuxStageOptions("etc/selinux/targeted/contexts/files/file_contexts")
+	assert.Equal(t, expectedOptions, actualOptions)
+}
+
+func TestNewSELinuxStage(t *testing.T) {
+	expectedStage := &Stage{
+		Name:    "org.osbuild.selinux",
+		Options: &SELinuxStageOptions{},
+	}
+	actualStage := NewSELinuxStage(&SELinuxStageOptions{})
+	assert.Equal(t, expectedStage, actualStage)
+}
diff --git a/internal/osbuild/timezone_stage_test.go b/internal/osbuild/timezone_stage_test.go
new file mode 100644
index 000000000..350705c21
--- /dev/null
+++ b/internal/osbuild/timezone_stage_test.go
@@ -0,0 +1,16 @@
+package osbuild
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewTimezoneStage(t *testing.T) {
+	expectedStage := &Stage{
+		Name:    "org.osbuild.timezone",
+		Options: &TimezoneStageOptions{},
+	}
+	actualStage := NewTimezoneStage(&TimezoneStageOptions{})
+	assert.Equal(t, expectedStage, actualStage)
+}
diff --git a/internal/osbuild/users_stage_test.go b/internal/osbuild/users_stage_test.go
new file mode 100644
index 000000000..1b24b4774
--- /dev/null
+++ b/internal/osbuild/users_stage_test.go
@@ -0,0 +1,16 @@
+package osbuild
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewUsersStage(t *testing.T) {
+	expectedStage := &Stage{
+		Name:    "org.osbuild.users",
+		Options: &UsersStageOptions{},
+	}
+	actualStage := NewUsersStage(&UsersStageOptions{})
+	assert.Equal(t, expectedStage, actualStage)
+}