Repository: osbuild/osbuild-composer
Pull request #132 (2019-11-28 23:11:40)

Rework to enable RHEL8
See individual commits.


Patch:
diff --git a/cmd/osbuild-composer/main.go b/cmd/osbuild-composer/main.go
index db884337b..37aae7909 100644
--- a/cmd/osbuild-composer/main.go
+++ b/cmd/osbuild-composer/main.go
@@ -34,19 +34,18 @@ func main() {
 	jobListener := listeners[1]
 
 	rpm := rpmmd.NewRPMMD()
-	distribution := distro.New("")
+
+	distribution, err := distro.FromHost()
+	if err != nil {
+		panic("cannot detect distro from host: " + err.Error())
+	}
 
 	var logger *log.Logger
 	if verbose {
 		logger = log.New(os.Stdout, "", 0)
 	}
 
-	err = os.MkdirAll("/var/lib/osbuild-composer", 0755)
-	if err != nil {
-		panic(err)
-	}
-
-	store := store.New(&stateFile)
+	store := store.New(&stateFile, distribution)
 
 	jobAPI := jobqueue.New(logger, store)
 	weldrAPI := weldr.New(rpm, distribution, logger, store)
diff --git a/cmd/osbuild-pipeline/main.go b/cmd/osbuild-pipeline/main.go
index c0299e6cf..ea108a859 100644
--- a/cmd/osbuild-pipeline/main.go
+++ b/cmd/osbuild-pipeline/main.go
@@ -7,10 +7,7 @@ import (
 	"os"
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
-
 	"github.com/osbuild/osbuild-composer/internal/distro"
-	_ "github.com/osbuild/osbuild-composer/internal/distro/fedora30"
-	_ "github.com/osbuild/osbuild-composer/internal/distro/rhel82"
 )
 
 func main() {
@@ -35,6 +32,10 @@ func main() {
 	}
 
 	d := distro.New(distroArg)
+	if d == nil {
+		panic("unknown distro: " + distroArg)
+	}
+
 	pipeline, err := d.Pipeline(blueprint, format)
 	if err != nil {
 		panic(err.Error())
diff --git a/cmd/osbuild-worker/main.go b/cmd/osbuild-worker/main.go
index 40b37650a..0824cf9ec 100644
--- a/cmd/osbuild-worker/main.go
+++ b/cmd/osbuild-worker/main.go
@@ -9,6 +9,7 @@ import (
 	"net"
 	"net/http"
 
+	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/jobqueue"
 )
 
@@ -74,7 +75,7 @@ func (c *ComposerClient) UpdateJob(job *jobqueue.Job, status string) error {
 	return nil
 }
 
-func handleJob(client *ComposerClient) {
+func handleJob(client *ComposerClient, distro distro.Distro) {
 	fmt.Println("Waiting for a new job...")
 	job, err := client.AddJob()
 	if err != nil {
@@ -84,7 +85,7 @@ func handleJob(client *ComposerClient) {
 	client.UpdateJob(job, "RUNNING")
 
 	fmt.Printf("Running job %s\n", job.ID.String())
-	err, errs := job.Run()
+	err, errs := job.Run(distro)
 	if err != nil {
 		client.UpdateJob(job, "FAILED")
 		return
@@ -101,9 +102,13 @@ func handleJob(client *ComposerClient) {
 }
 
 func main() {
-	client := NewClient()
+	distro, err := distro.FromHost()
+	if err != nil {
+		panic(err)
+	}
 
+	client := NewClient()
 	for {
-		handleJob(client)
+		handleJob(client, distro)
 	}
 }
diff --git a/internal/blueprint/blueprint.go b/internal/blueprint/blueprint.go
index 30b818687..1aecccbdd 100644
--- a/internal/blueprint/blueprint.go
+++ b/internal/blueprint/blueprint.go
@@ -1,16 +1,6 @@
 // Package blueprint contains primitives for representing weldr blueprints
 package blueprint
 
-// An InvalidOutputFormatError is returned when a requested output format is
-// not supported. The requested format is included as the error message.
-type InvalidOutputFormatError struct {
-	message string
-}
-
-func (e *InvalidOutputFormatError) Error() string {
-	return e.message
-}
-
 // A Blueprint is a high-level description of an image.
 type Blueprint struct {
 	Name           string          `json:"name"`
diff --git a/internal/distro/distro.go b/internal/distro/distro.go
index 2317c79a6..76a92aca9 100644
--- a/internal/distro/distro.go
+++ b/internal/distro/distro.go
@@ -4,13 +4,15 @@ import (
 	"bufio"
 	"errors"
 	"io"
-	"log"
 	"os"
 	"strings"
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/pipeline"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
+
+	"github.com/osbuild/osbuild-composer/internal/distro/fedora30"
+	"github.com/osbuild/osbuild-composer/internal/distro/rhel82"
 )
 
 type Distro interface {
@@ -22,43 +24,31 @@ type Distro interface {
 	ListOutputFormats() []string
 
 	// Returns the canonical filename and MIME type for a given output
-	// format
+	// format. `outputFormat` must be one returned by
 	FilenameFromType(outputFormat string) (string, string, error)
 
 	// Returns an osbuild pipeline that generates an image in the given
 	// output format with all packages and customizations specified in the
-	// given blueprint. `outputFormat` must be one returned by
-	// ListOutputFormats().
+	// given blueprint.
 	Pipeline(b *blueprint.Blueprint, outputFormat string) (*pipeline.Pipeline, error)
-}
-
-// An InvalidOutputFormatError is returned when a requested output format is
-// not supported. The requested format is included as the error message.
-type InvalidOutputFormatError struct {
-	Format string
-}
 
-func (e *InvalidOutputFormatError) Error() string {
-	return e.Format
+	// Returns a osbuild runner that can be used on this distro.
+	Runner() string
 }
 
-var registered = map[string]Distro{}
+var registered map[string]Distro
 
-func New(name string) Distro {
-	if name == "" {
-		distro, err := FromHost()
-		if err == nil {
-			return distro
-		} else {
-			log.Println("cannot detect distro from host: " + err.Error())
-			log.Println("falling back to 'fedora-30'")
-			return New("fedora-30")
-		}
+func init() {
+	registered = map[string]Distro{
+		"fedora-30": fedora30.New(),
+		"rhel-8.2":    rhel82.New(),
 	}
+}
 
+func New(name string) Distro {
 	distro, ok := registered[name]
 	if !ok {
-		panic("unknown distro: " + name)
+		return nil
 	}
 
 	return distro
@@ -78,11 +68,12 @@ func FromHost() (Distro, error) {
 
 	name := osrelease["ID"] + "-" + osrelease["VERSION_ID"]
 
-	distro, ok := registered[name]
-	if !ok {
+	d := New(name)
+	if d == nil {
 		return nil, errors.New("unknown distro: " + name)
 	}
-	return distro, nil
+
+	return d, nil
 }
 
 func Register(name string, distro Distro) {
diff --git a/internal/distro/fedora30/distro.go b/internal/distro/fedora30/distro.go
index 6452dd805..6e10b76b0 100644
--- a/internal/distro/fedora30/distro.go
+++ b/internal/distro/fedora30/distro.go
@@ -1,6 +1,7 @@
 package fedora30
 
 import (
+	"errors"
 	"sort"
 	"strconv"
 
@@ -8,7 +9,6 @@ import (
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/crypt"
-	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/pipeline"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
@@ -29,7 +29,7 @@ type output struct {
 	Assembler        *pipeline.Assembler
 }
 
-func init() {
+func New() *Fedora30 {
 	r := Fedora30{
 		outputs: map[string]output{},
 	}
@@ -212,7 +212,7 @@ func init() {
 		Assembler:     r.qemuAssembler("vmdk", "disk.vmdk"),
 	}
 
-	distro.Register("fedora-30", &r)
+	return &r
 }
 
 func (r *Fedora30) Repositories() []rpmmd.RepoConfig {
@@ -268,13 +268,13 @@ func (r *Fedora30) FilenameFromType(outputFormat string) (string, string, error)
 	if output, exists := r.outputs[outputFormat]; exists {
 		return output.Name, output.MimeType, nil
 	}
-	return "", "", &distro.InvalidOutputFormatError{outputFormat}
+	return "", "", errors.New("invalid output format: " + outputFormat)
 }
 
 func (r *Fedora30) Pipeline(b *blueprint.Blueprint, outputFormat string) (*pipeline.Pipeline, error) {
 	output, exists := r.outputs[outputFormat]
 	if !exists {
-		return nil, &distro.InvalidOutputFormatError{outputFormat}
+		return nil, errors.New("invalid output format: " + outputFormat)
 	}
 
 	p := &pipeline.Pipeline{}
@@ -343,6 +343,10 @@ func (r *Fedora30) Pipeline(b *blueprint.Blueprint, outputFormat string) (*pipel
 	return p, nil
 }
 
+func (r *Fedora30) Runner() string {
+	return "org.osbuild.fedora30"
+}
+
 func (r *Fedora30) buildPipeline() *pipeline.Pipeline {
 	packages := []string{
 		"dnf",
diff --git a/internal/distro/rhel82/distro.go b/internal/distro/rhel82/distro.go
index de8557b5c..507a7752c 100644
--- a/internal/distro/rhel82/distro.go
+++ b/internal/distro/rhel82/distro.go
@@ -1,6 +1,7 @@
 package rhel82
 
 import (
+	"errors"
 	"sort"
 	"strconv"
 
@@ -8,7 +9,6 @@ import (
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/crypt"
-	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/pipeline"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
@@ -23,37 +23,91 @@ type output struct {
 	Packages         []string
 	ExcludedPackages []string
 	IncludeFSTab     bool
+	DefaultTarget    string
+	KernelOptions    string
 	Assembler        *pipeline.Assembler
 }
 
-func init() {
+func New() *RHEL82 {
+	const GigaByte = 1024 * 1024 * 1024
+
 	r := RHEL82{
 		outputs: map[string]output{},
 	}
 
 	r.outputs["ami"] = output{
-		Name:     "image.ami",
-		MimeType: "application/x-qemu-disk",
+		Name:     "image.raw.xz",
+		MimeType: "application/octet-stream",
 		Packages: []string{
-			"@Core",
+			"checkpolicy",
 			"chrony",
-			"kernel",
-			"selinux-policy-targeted",
-			"grub2-pc",
-			"dracut-config-generic",
 			"cloud-init",
-			"checkpolicy",
+			"cloud-init",
+			"cloud-utils-growpart",
+			"@core",
+			"dhcp-client",
+			"dracut-config-generic",
+			"gdisk",
+			"grub2",
+			"insights-client",
+			"kernel",
+			"langpacks-en",
 			"net-tools",
+			"NetworkManager",
+			"redhat-release",
+			"redhat-release-eula",
+			"rng-tools",
+			"rsync",
+			"selinux-policy-targeted",
+			"tar",
+			"yum-utils",
+
+			// TODO this doesn't exist in BaseOS or AppStream
+			// "rh-amazon-rhui-client",
 		},
 		ExcludedPackages: []string{
+			"aic94xx-firmware",
+			"alsa-firmware",
+			"alsa-lib",
+			"alsa-tools-firmware",
+			"biosdevname",
 			"dracut-config-rescue",
+			"firewalld",
+			"iprutils",
+			"ivtv-firmware",
+			"iwl1000-firmware",
+			"iwl100-firmware",
+			"iwl105-firmware",
+			"iwl135-firmware",
+			"iwl2000-firmware",
+			"iwl2030-firmware",
+			"iwl3160-firmware",
+			"iwl3945-firmware",
+			"iwl4965-firmware",
+			"iwl5000-firmware",
+			"iwl5150-firmware",
+			"iwl6000-firmware",
+			"iwl6000g2a-firmware",
+			"iwl6000g2b-firmware",
+			"iwl6050-firmware",
+			"iwl7260-firmware",
+			"libertas-sd8686-firmware",
+			"libertas-sd8787-firmware",
+			"libertas-usb8388-firmware",
+			"plymouth",
+
+			// TODO this cannot be removed, because the kernel (?)
+			// depends on it. The ec2 kickstart force-removes it.
+			// "linux-firmware",
 
 			// TODO setfiles failes because of usr/sbin/timedatex. Exlude until
 			// https://errata.devel.redhat.com/advisory/47339 lands
 			"timedatex",
 		},
-		IncludeFSTab: true,
-		Assembler:    r.qemuAssembler("raw", "image.ami"),
+		DefaultTarget: "multi-user.target",
+		IncludeFSTab:  true,
+		KernelOptions: "ro console=ttyS0,115200n8 console=tty0 net.ifnames=0 rd.blacklist=nouveau nvme_core.io_timeout=4294967295 crashkernel=auto",
+		Assembler:     r.qemuAssembler("raw.xz", "image.raw.xz", 6 * GigaByte),
 	}
 
 	r.outputs["ext4-filesystem"] = output{
@@ -70,8 +124,9 @@ func init() {
 		ExcludedPackages: []string{
 			"dracut-config-rescue",
 		},
-		IncludeFSTab: false,
-		Assembler:    r.rawFSAssembler("filesystem.img"),
+		IncludeFSTab:  false,
+		KernelOptions: "ro net.ifnames=0",
+		Assembler:     r.rawFSAssembler("filesystem.img"),
 	}
 
 	r.outputs["partitioned-disk"] = output{
@@ -89,8 +144,9 @@ func init() {
 		ExcludedPackages: []string{
 			"dracut-config-rescue",
 		},
-		IncludeFSTab: true,
-		Assembler:    r.qemuAssembler("raw", "disk.img"),
+		IncludeFSTab:  true,
+		KernelOptions: "ro net.ifnames=0",
+		Assembler:     r.qemuAssembler("raw", "disk.img", 3*GigaByte),
 	}
 
 	r.outputs["qcow2"] = output{
@@ -112,8 +168,9 @@ func init() {
 			"gobject-introspection",
 			"plymouth",
 		},
-		IncludeFSTab: true,
-		Assembler:    r.qemuAssembler("qcow2", "image.qcow2"),
+		IncludeFSTab:  true,
+		KernelOptions: "ro net.ifnames=0",
+		Assembler:     r.qemuAssembler("qcow2", "image.qcow2", 3*GigaByte),
 	}
 
 	r.outputs["openstack"] = output{
@@ -135,8 +192,9 @@ func init() {
 		ExcludedPackages: []string{
 			"dracut-config-rescue",
 		},
-		IncludeFSTab: true,
-		Assembler:    r.qemuAssembler("qcow2", "image.qcow2"),
+		IncludeFSTab:  true,
+		KernelOptions: "ro net.ifnames=0",
+		Assembler:     r.qemuAssembler("qcow2", "image.qcow2", 3*GigaByte),
 	}
 
 	r.outputs["tar"] = output{
@@ -153,8 +211,9 @@ func init() {
 		ExcludedPackages: []string{
 			"dracut-config-rescue",
 		},
-		IncludeFSTab: false,
-		Assembler:    r.tarAssembler("root.tar.xz", "xz"),
+		IncludeFSTab:  false,
+		KernelOptions: "ro net.ifnames=0",
+		Assembler:     r.tarAssembler("root.tar.xz", "xz"),
 	}
 
 	r.outputs["vhd"] = output{
@@ -175,8 +234,9 @@ func init() {
 		ExcludedPackages: []string{
 			"dracut-config-rescue",
 		},
-		IncludeFSTab: true,
-		Assembler:    r.qemuAssembler("vhd", "image.vhd"),
+		IncludeFSTab:  true,
+		KernelOptions: "ro net.ifnames=0",
+		Assembler:     r.qemuAssembler("vhd", "image.vhd", 3*GigaByte),
 	}
 
 	r.outputs["vmdk"] = output{
@@ -195,11 +255,12 @@ func init() {
 		ExcludedPackages: []string{
 			"dracut-config-rescue",
 		},
-		IncludeFSTab: true,
-		Assembler:    r.qemuAssembler("vmdk", "disk.vmdk"),
+		IncludeFSTab:  true,
+		KernelOptions: "ro net.ifnames=0",
+		Assembler:     r.qemuAssembler("vmdk", "disk.vmdk", 3*GigaByte),
 	}
 
-	distro.Register("rhel-8.2", &r)
+	return &r
 }
 
 func (r *RHEL82) Repositories() []rpmmd.RepoConfig {
@@ -207,14 +268,14 @@ func (r *RHEL82) Repositories() []rpmmd.RepoConfig {
 		{
 			Id:       "baseos",
 			Name:     "BaseOS",
-			BaseURL:  "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/RHEL-8.2.0-20191117.n.0/compose/BaseOS/x86_64/os",
-			Checksum: "sha256:4699a755326e5af71cd069dc9d9289e7d0433ab0acc42ee33b93054fd0e980e7",
+			BaseURL:  "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/RHEL-8.2.0-20191125.n.1/compose/BaseOS/x86_64/os",
+			Checksum: "sha256:30b905ab1538243de69e019573443b2a1e4edad7c1f7d32aa5a4fb014ff98060",
 		},
 		{
 			Id:       "appstream",
 			Name:     "AppStream",
-			BaseURL:  "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/RHEL-8.2.0-20191117.n.0/compose/AppStream/x86_64/os",
-			Checksum: "sha256:212f10ee3fb8265f38837a1e867e4218556e9bc71fa1d38827a088413974a949",
+			BaseURL:  "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/RHEL-8.2.0-20191125.n.1/compose/AppStream/x86_64/os",
+			Checksum: "sha256:afd86d5b664ec87e209c5ff3cf011bcc6a40578394191c1d889b4ead17a072ae",
 		},
 	}
 }
@@ -232,13 +293,13 @@ func (r *RHEL82) FilenameFromType(outputFormat string) (string, string, error) {
 	if output, exists := r.outputs[outputFormat]; exists {
 		return output.Name, output.MimeType, nil
 	}
-	return "", "", &distro.InvalidOutputFormatError{outputFormat}
+	return "", "", errors.New("invalid output format: " + outputFormat)
 }
 
 func (r *RHEL82) Pipeline(b *blueprint.Blueprint, outputFormat string) (*pipeline.Pipeline, error) {
 	output, exists := r.outputs[outputFormat]
 	if !exists {
-		return nil, &distro.InvalidOutputFormatError{outputFormat}
+		return nil, errors.New("invalid output format: " + outputFormat)
 	}
 
 	p := &pipeline.Pipeline{}
@@ -251,7 +312,12 @@ func (r *RHEL82) Pipeline(b *blueprint.Blueprint, outputFormat string) (*pipelin
 	if output.IncludeFSTab {
 		p.AddStage(pipeline.NewFSTabStage(r.fsTabStageOptions()))
 	}
-	p.AddStage(pipeline.NewGRUB2Stage(r.grub2StageOptions(b.GetKernel())))
+
+	kernelOptions := output.KernelOptions
+	if kernel := b.GetKernel(); kernel != nil {
+		kernelOptions += " " + kernel.Append
+	}
+	p.AddStage(pipeline.NewGRUB2Stage(r.grub2StageOptions(kernelOptions)))
 
 	// TODO support setting all languages and install corresponding langpack-* package
 	language, keyboard := b.GetPrimaryLocale()
@@ -294,7 +360,7 @@ func (r *RHEL82) Pipeline(b *blueprint.Blueprint, outputFormat string) (*pipelin
 	}
 
 	if services := b.GetServices(); services != nil {
-		p.AddStage(pipeline.NewSystemdStage(r.systemdStageOptions(services)))
+		p.AddStage(pipeline.NewSystemdStage(r.systemdStageOptions(services, output.DefaultTarget)))
 	}
 
 	if firewall := b.GetFirewall(); firewall != nil {
@@ -307,6 +373,10 @@ func (r *RHEL82) Pipeline(b *blueprint.Blueprint, outputFormat string) (*pipelin
 	return p, nil
 }
 
+func (r *RHEL82) Runner() string {
+	return "org.osbuild.rhel82"
+}
+
 func (r *RHEL82) buildPipeline() *pipeline.Pipeline {
 	packages := []string{
 		"dnf",
@@ -425,10 +495,11 @@ func (r *RHEL82) firewallStageOptions(firewall *blueprint.FirewallCustomization)
 	return &options
 }
 
-func (r *RHEL82) systemdStageOptions(s *blueprint.ServicesCustomization) *pipeline.SystemdStageOptions {
+func (r *RHEL82) systemdStageOptions(s *blueprint.ServicesCustomization, target string) *pipeline.SystemdStageOptions {
 	return &pipeline.SystemdStageOptions{
 		EnabledServices:  s.Enabled,
 		DisabledServices: s.Disabled,
+		DefaultTarget:    target,
 	}
 }
 
@@ -438,21 +509,15 @@ func (r *RHEL82) fsTabStageOptions() *pipeline.FSTabStageOptions {
 		panic("invalid UUID")
 	}
 	options := pipeline.FSTabStageOptions{}
-	options.AddFilesystem(id, "xfs", "/", "defaults", 1, 1)
+	options.AddFilesystem(id, "xfs", "/", "defaults", 0, 0)
 	return &options
 }
 
-func (r *RHEL82) grub2StageOptions(kernel *blueprint.KernelCustomization) *pipeline.GRUB2StageOptions {
+func (r *RHEL82) grub2StageOptions(kernelOptions string) *pipeline.GRUB2StageOptions {
 	id, err := uuid.Parse("0bd700f8-090f-4556-b797-b340297ea1bd")
 	if err != nil {
 		panic("invalid UUID")
 	}
-	kernelOptions := "ro biosdevname=0 net.ifnames=0"
-
-	if kernel != nil {
-		kernelOptions += " " + kernel.Append
-	}
-
 	return &pipeline.GRUB2StageOptions{
 		RootFilesystemUUID: id,
 		KernelOptions:      kernelOptions,
@@ -465,7 +530,7 @@ func (r *RHEL82) selinuxStageOptions() *pipeline.SELinuxStageOptions {
 	}
 }
 
-func (r *RHEL82) qemuAssembler(format string, filename string) *pipeline.Assembler {
+func (r *RHEL82) qemuAssembler(format string, filename string, size uint64) *pipeline.Assembler {
 	id, err := uuid.Parse("0bd700f8-090f-4556-b797-b340297ea1bd")
 	if err != nil {
 		panic("invalid UUID")
@@ -476,7 +541,7 @@ func (r *RHEL82) qemuAssembler(format string, filename string) *pipeline.Assembl
 			Filename:           filename,
 			PTUUID:             "0x14fc63d2",
 			RootFilesystemUUDI: id,
-			Size:               3221225472,
+			Size:               size,
 			RootFilesystemType: "xfs",
 		})
 }
diff --git a/internal/jobqueue/job.go b/internal/jobqueue/job.go
index e220465a0..3c650d92a 100644
--- a/internal/jobqueue/job.go
+++ b/internal/jobqueue/job.go
@@ -3,10 +3,12 @@ package jobqueue
 import (
 	"encoding/json"
 	"fmt"
+	"io/ioutil"
 	"os"
 	"os/exec"
 
 	"github.com/google/uuid"
+	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/pipeline"
 	"github.com/osbuild/osbuild-composer/internal/target"
 	"github.com/osbuild/osbuild-composer/internal/upload/awsupload"
@@ -22,8 +24,28 @@ type JobStatus struct {
 	Status string `json:"status"`
 }
 
-func (job *Job) Run() (error, []error) {
-	cmd := exec.Command("osbuild", "--store", "/var/cache/osbuild-composer/store", "--json", "-")
+func (job *Job) Run(d distro.Distro) (error, []error) {
+	build := pipeline.Build{
+		Runner: d.Runner(),
+	}
+
+	buildFile, err := ioutil.TempFile("", "osbuild-worker-build-env-*")
+	if err != nil {
+		return err, nil
+	}
+	defer os.Remove(buildFile.Name())
+
+	err = json.NewEncoder(buildFile).Encode(build)
+	if err != nil {
+		return err, nil
+	}
+
+	cmd := exec.Command(
+		"osbuild",
+		"--store", "/var/cache/osbuild-composer/store",
+		"--build-env", buildFile.Name(),
+		"--json", "-",
+	)
 	cmd.Stderr = os.Stderr
 
 	stdin, err := cmd.StdinPipe()
diff --git a/internal/mocks/rpmmd/fixtures.go b/internal/mocks/rpmmd/fixtures.go
index 8e6912558..65c4662ff 100644
--- a/internal/mocks/rpmmd/fixtures.go
+++ b/internal/mocks/rpmmd/fixtures.go
@@ -2,12 +2,14 @@ package rpmmd_mock
 
 import (
 	"fmt"
+	"sort"
+	"time"
+
 	"github.com/google/uuid"
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
+	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 	"github.com/osbuild/osbuild-composer/internal/store"
-	"sort"
-	"time"
 )
 
 type FixtureGenerator func() Fixture
@@ -56,7 +58,8 @@ func createBaseStoreFixture() *store.Store {
 
 	var date = time.Date(2019, 11, 27, 13, 19, 0, 0, time.FixedZone("UTC+1", 60*60))
 
-	s := store.New(nil)
+	d := distro.New("fedora-30")
+	s := store.New(nil, d)
 
 	s.Blueprints[bName] = b
 	s.Composes = map[uuid.UUID]store.Compose{
diff --git a/internal/pipeline/systemd_stage.go b/internal/pipeline/systemd_stage.go
index 5c0d7aa97..cc4aeb57f 100644
--- a/internal/pipeline/systemd_stage.go
+++ b/internal/pipeline/systemd_stage.go
@@ -3,6 +3,7 @@ package pipeline
 type SystemdStageOptions struct {
 	EnabledServices  []string `json:"enabled_services,omitempty"`
 	DisabledServices []string `json:"disabled_services,omitempty"`
+	DefaultTarget    string   `json:"default_target,omitempty"`
 }
 
 func (SystemdStageOptions) isStageOptions() {}
diff --git a/internal/store/store.go b/internal/store/store.go
index bae7aecda..5950b835f 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -15,13 +15,11 @@ import (
 	"time"
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
+	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/pipeline"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 	"github.com/osbuild/osbuild-composer/internal/target"
 
-	"github.com/osbuild/osbuild-composer/internal/distro"
-	_ "github.com/osbuild/osbuild-composer/internal/distro/fedora30"
-
 	"github.com/google/uuid"
 )
 
@@ -37,6 +35,7 @@ type Store struct {
 	mu           sync.RWMutex // protects all fields
 	pendingJobs  chan Job
 	stateChannel chan []byte
+	distro       distro.Distro
 }
 
 // A Compose represent the task of building one image. It contains all the information
@@ -107,7 +106,7 @@ func (e *InvalidRequestError) Error() string {
 	return e.message
 }
 
-func New(stateFile *string) *Store {
+func New(stateFile *string, distro distro.Distro) *Store {
 	var s Store
 
 	if stateFile != nil {
@@ -151,6 +150,8 @@ func New(stateFile *string) *Store {
 	}
 	s.pendingJobs = make(chan Job, 200)
 
+	s.distro = distro
+
 	return &s
 }
 
@@ -450,8 +451,7 @@ func (s *Store) PushCompose(composeID uuid.UUID, bp *blueprint.Blueprint, compos
 			},
 		),
 	}
-	d := distro.New("")
-	pipeline, err := d.Pipeline(bp, composeType)
+	pipeline, err := s.distro.Pipeline(bp, composeType)
 	if err != nil {
 		return err
 	}
@@ -529,8 +529,7 @@ func (s *Store) GetImage(composeID uuid.UUID) (*Image, error) {
 		if compose.QueueStatus != "FINISHED" {
 			return nil, &InvalidRequestError{"compose was not finished"}
 		}
-		d := distro.New("")
-		name, mime, err := d.FilenameFromType(compose.OutputType)
+		name, mime, err := s.distro.FilenameFromType(compose.OutputType)
 		if err != nil {
 			panic("invalid output type")
 		}
diff --git a/internal/weldr/api.go b/internal/weldr/api.go
index 0e32758ef..1a9b84865 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -19,7 +19,6 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/store"
 
 	"github.com/osbuild/osbuild-composer/internal/distro"
-	_ "github.com/osbuild/osbuild-composer/internal/distro/fedora30"
 )
 
 type API struct {
@@ -1196,8 +1195,7 @@ func (api *API) composeTypesHandler(writer http.ResponseWriter, request *http.Re
 		Types []composeType `json:"types"`
 	}
 
-	d := distro.New("")
-	for _, format := range d.ListOutputFormats() {
+	for _, format := range api.distro.ListOutputFormats() {
 		reply.Types = append(reply.Types, composeType{format, true})
 	}
 


Test patch:
diff --git a/internal/blueprint/blueprint_test.go b/internal/blueprint/blueprint_test.go
deleted file mode 100644
index 2d81790f0..000000000
--- a/internal/blueprint/blueprint_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package blueprint_test
-
-import (
-	"testing"
-
-	"github.com/osbuild/osbuild-composer/internal/blueprint"
-)
-
-func TestInvalidOutputFormatError_Error(t *testing.T) {
-	tests := []struct {
-		name string
-		want string
-	}{
-		{
-			name: "basic",
-			want: "",
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			e := &blueprint.InvalidOutputFormatError{}
-			if got := e.Error(); got != tt.want {
-				t.Errorf("InvalidOutputFormatError.Error() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
diff --git a/internal/distro/distro_test.go b/internal/distro/distro_test.go
index 3869bc801..29a01ffe3 100644
--- a/internal/distro/distro_test.go
+++ b/internal/distro/distro_test.go
@@ -9,8 +9,6 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/pipeline"
-
-	_ "github.com/osbuild/osbuild-composer/internal/distro/fedora30"
 )
 
 func TestDistro_Pipeline(t *testing.T) {
diff --git a/internal/distro/fedora30/distro_test.go b/internal/distro/fedora30/distro_test.go
index 032053b00..7ed693861 100644
--- a/internal/distro/fedora30/distro_test.go
+++ b/internal/distro/fedora30/distro_test.go
@@ -5,7 +5,6 @@ import (
 	"testing"
 
 	"github.com/osbuild/osbuild-composer/internal/distro"
-	_ "github.com/osbuild/osbuild-composer/internal/distro/fedora30"
 )
 
 func TestListOutputFormats(t *testing.T) {
diff --git a/internal/distro/rhel82/distro_test.go b/internal/distro/rhel82/distro_test.go
index 6c459dd32..1deccdd79 100644
--- a/internal/distro/rhel82/distro_test.go
+++ b/internal/distro/rhel82/distro_test.go
@@ -5,7 +5,6 @@ import (
 	"testing"
 
 	"github.com/osbuild/osbuild-composer/internal/distro"
-	_ "github.com/osbuild/osbuild-composer/internal/distro/rhel82"
 )
 
 func TestListOutputFormats(t *testing.T) {
@@ -40,8 +39,8 @@ func TestFilenameFromType(t *testing.T) {
 		{
 			name:  "ami",
 			args:  args{"ami"},
-			want:  "image.ami",
-			want1: "application/x-qemu-disk",
+			want:  "image.raw.xz",
+			want1: "application/octet-stream",
 		},
 		{
 			name:  "ext4",
diff --git a/internal/distro/test/distro.go b/internal/distro/test/distro.go
index bc2c9b2b3..461e7f2b8 100644
--- a/internal/distro/test/distro.go
+++ b/internal/distro/test/distro.go
@@ -1,6 +1,8 @@
 package test
 
 import (
+	"errors"
+
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/pipeline"
@@ -28,9 +30,13 @@ func (d *TestDistro) ListOutputFormats() []string {
 }
 
 func (d *TestDistro) FilenameFromType(outputFormat string) (string, string, error) {
-	return "", "", &distro.InvalidOutputFormatError{outputFormat}
+	return "", "", errors.New("invalid output format: " + outputFormat)
 }
 
 func (d *TestDistro) Pipeline(b *blueprint.Blueprint, outputFormat string) (*pipeline.Pipeline, error) {
-	return nil, &distro.InvalidOutputFormatError{outputFormat}
+	return nil, errors.New("invalid output format: " + outputFormat)
+}
+
+func (d *TestDistro) Runner() string {
+	return "org.osbuild.test"
 }
diff --git a/internal/jobqueue/api_test.go b/internal/jobqueue/api_test.go
index edead4827..b74f767e0 100644
--- a/internal/jobqueue/api_test.go
+++ b/internal/jobqueue/api_test.go
@@ -5,6 +5,7 @@ import (
 	"testing"
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
+	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/jobqueue"
 	"github.com/osbuild/osbuild-composer/internal/store"
 	"github.com/osbuild/osbuild-composer/internal/test"
@@ -31,7 +32,7 @@ func TestBasic(t *testing.T) {
 	}
 
 	for _, c := range cases {
-		api := jobqueue.New(nil, store.New(nil))
+		api := jobqueue.New(nil, store.New(nil, distro.New("fedora-30")))
 
 		test.TestRoute(t, api, false, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
 	}
@@ -39,7 +40,7 @@ func TestBasic(t *testing.T) {
 
 func TestCreate(t *testing.T) {
 	id, _ := uuid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff")
-	store := store.New(nil)
+	store := store.New(nil, distro.New("fedora-30"))
 	api := jobqueue.New(nil, store)
 
 	err := store.PushCompose(id, &blueprint.Blueprint{}, "tar")
@@ -53,7 +54,7 @@ func TestCreate(t *testing.T) {
 
 func testUpdateTransition(t *testing.T, from, to string, expectedStatus int) {
 	id, _ := uuid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff")
-	store := store.New(nil)
+	store := store.New(nil, distro.New("fedora-30"))
 	api := jobqueue.New(nil, store)
 
 	if from != "VOID" {