Repository: osbuild/osbuild-composer
Pull request #235 (2020-02-14 13:25:44)

Couple of small changes
as I dig around and try to find my way through the existing sources.


Patch:
diff --git a/.gitignore b/.gitignore
index 32d0b5a4d..85d481647 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ __pycache__
 /osbuild-pipeline
 /osbuild-upload-azure
 /osbuild-upload-aws
+/osbuild-tests
 
 /*.tar.gz
 /*.src.rpm
diff --git a/Makefile b/Makefile
index f0d099378..e88a46199 100644
--- a/Makefile
+++ b/Makefile
@@ -8,6 +8,7 @@ build:
 	go build -o osbuild-pipeline ./cmd/osbuild-pipeline/
 	go build -o osbuild-upload-azure ./cmd/osbuild-upload-azure/
 	go build -o osbuild-upload-aws ./cmd/osbuild-upload-aws/
+	go build -o osbuild-tests ./cmd/osbuild-tests
 
 .PHONY: install
 install:
diff --git a/cmd/osbuild-pipeline/main.go b/cmd/osbuild-pipeline/main.go
index ce894ebb7..5832a689b 100644
--- a/cmd/osbuild-pipeline/main.go
+++ b/cmd/osbuild-pipeline/main.go
@@ -4,10 +4,11 @@ import (
 	"encoding/json"
 	"flag"
 	"fmt"
-	"github.com/osbuild/osbuild-composer/internal/common"
 	"io/ioutil"
 	"os"
 
+	"github.com/osbuild/osbuild-composer/internal/common"
+
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/distro"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
@@ -29,7 +30,7 @@ func main() {
 		flag.Usage()
 		return
 	}
-	
+
 	// Validate architecture
 	if !common.ArchitectureExists(archArg) {
 		_, _ = fmt.Fprintf(os.Stderr, "The provided architecture (%s) is not supported. Use one of these:\n", archArg)
@@ -47,7 +48,7 @@ func main() {
 		}
 		return
 	}
-	
+
 	// Validate image type
 
 	blueprint := &blueprint.Blueprint{}
@@ -80,13 +81,28 @@ func main() {
 		}
 	}
 
+	pkgs, exclude_pkgs, err := d.BasePackages(imageType, archArg)
+	if err != nil {
+		panic("could not get base packages: " + err.Error())
+	}
+	packages = append(pkgs, packages...)
+
 	rpmmd := rpmmd.NewRPMMD()
-	_, checksums, err := rpmmd.Depsolve(packages, d.Repositories(archArg), true)
+	packageSpecs, checksums, err := rpmmd.Depsolve(packages, exclude_pkgs, d.Repositories(archArg), d.ModulePlatformID(), false)
 	if err != nil {
-		panic(err.Error())
+		panic("Could not depsolve: " + err.Error())
+	}
+
+	buildPkgs, err := d.BuildPackages(archArg)
+	if err != nil {
+		panic("Could not get build packages: " + err.Error())
+	}
+	buildPackageSpecs, _, err := rpmmd.Depsolve(buildPkgs, nil, d.Repositories(archArg), d.ModulePlatformID(), false)
+	if err != nil {
+		panic("Could not depsolve build packages: " + err.Error())
 	}
 
-	pipeline, err := d.Pipeline(blueprint, nil, checksums, archArg, imageType, 0)
+	pipeline, err := d.Pipeline(blueprint, nil, packageSpecs, buildPackageSpecs, checksums, archArg, imageType, 0)
 	if err != nil {
 		panic(err.Error())
 	}
diff --git a/dnf-json b/dnf-json
index 80a4981ad..4731675c4 100644
--- a/dnf-json
+++ b/dnf-json
@@ -3,9 +3,11 @@
 import datetime
 import dnf
 import hashlib
+import hawkey
 import json
 import shutil
 import sys
+import tempfile
 
 DNF_ERROR_EXIT_CODE = 10
 
@@ -36,8 +38,13 @@ def dnfrepo(desc, parent_conf=None):
     return repo
 
 
-def create_base(repos, clean=False):
+def create_base(repos, module_platform_id, persistdir, cachedir, clean=False):
     base = dnf.Base()
+    base.conf.module_platform_id = module_platform_id
+    base.conf.config_file_path = "/dev/null"
+    base.conf.persistdir = persistdir
+    if cachedir:
+        base.conf.cachedir = cachedir
 
     if clean:
         shutil.rmtree(base.conf.cachedir, ignore_errors=True)
@@ -80,53 +87,60 @@ def repo_checksums(base):
 
 call = json.load(sys.stdin)
 command = call["command"]
-arguments = call.get("arguments", {})
-
-if command == "dump":
-    base = create_base(arguments.get("repos", {}), arguments.get("clean", False))
-    packages = []
-    for package in base.sack.query().available():
-        packages.append({
-            "name": package.name,
-            "summary": package.summary,
-            "description": package.description,
-            "url": package.url,
-            "epoch": package.epoch,
-            "version": package.version,
-            "release": package.release,
-            "arch": package.arch,
-            "buildtime": timestamp_to_rfc3339(package.buildtime),
-            "license": package.license
-        })
-    json.dump({
-        "checksums": repo_checksums(base),
-        "packages": packages
-    }, sys.stdout)
-
-elif command == "depsolve":
-    base = create_base(arguments.get("repos", {}), arguments.get("clean", False))
-    errors = []
-
-    try:
-        base.install_specs(arguments["package-specs"])
-    except dnf.exceptions.MarkingErrors as e:
-        exit_with_dnf_error("MarkingErrors", f"Error occurred when marking packages for installation: {e}")
-
-    try:
-        base.resolve()
-    except dnf.exceptions.DepsolveError as e:
-        exit_with_dnf_error("DepsolveError", f"There was a problem depsolving {arguments['package-specs']}: {e}")
-
-    dependencies = []
-    for package in base.transaction.install_set:
-        dependencies.append({
-            "name": package.name,
-            "epoch": package.epoch,
-            "version": package.version,
-            "release": package.release,
-            "arch": package.arch
-        })
-    json.dump({
-        "checksums": repo_checksums(base),
-        "dependencies": dependencies
-    }, sys.stdout)
+arguments = call["arguments"]
+repos = arguments.get("repos", {})
+clean = arguments.get("clean", False)
+cachedir = arguments.get("cachedir", None)
+module_platform_id = arguments["module_platform_id"]
+
+with tempfile.TemporaryDirectory() as persistdir:
+    base = create_base(repos, module_platform_id, persistdir, cachedir, clean)
+
+    if command == "dump":
+        packages = []
+        for package in base.sack.query().available():
+            packages.append({
+                "name": package.name,
+                "summary": package.summary,
+                "description": package.description,
+                "url": package.url,
+                "epoch": package.epoch,
+                "version": package.version,
+                "release": package.release,
+                "arch": package.arch,
+                "buildtime": timestamp_to_rfc3339(package.buildtime),
+                "license": package.license
+            })
+        json.dump({
+            "checksums": repo_checksums(base),
+            "packages": packages
+        }, sys.stdout)
+
+    elif command == "depsolve":
+        errors = []
+
+        try:
+            base.install_specs(arguments["package-specs"], exclude=arguments.get("exclude-specs", []))
+        except dnf.exceptions.MarkingErrors as e:
+            exit_with_dnf_error("MarkingErrors", f"Error occurred when marking packages for installation: {e}")
+
+        try:
+            base.resolve()
+        except dnf.exceptions.DepsolveError as e:
+            exit_with_dnf_error("DepsolveError", f"There was a problem depsolving {arguments['package-specs']}: {e}")
+
+        dependencies = []
+        for package in base.transaction.install_set:
+            dependencies.append({
+                "name": package.name,
+                "epoch": package.epoch,
+                "version": package.version,
+                "release": package.release,
+                "arch": package.arch,
+                "remote_location": package.remote_location(),
+                "checksum": f"{hawkey.chksum_name(package.chksum[0])}:{package.chksum[1].hex()}",
+            })
+        json.dump({
+            "checksums": repo_checksums(base),
+            "dependencies": dependencies
+        }, sys.stdout)
diff --git a/internal/compose/compose.go b/internal/compose/compose.go
index d08c56b0a..547a2fcd1 100644
--- a/internal/compose/compose.go
+++ b/internal/compose/compose.go
@@ -3,11 +3,12 @@
 package compose
 
 import (
+	"time"
+
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/common"
-	"github.com/osbuild/osbuild-composer/internal/pipeline"
+	"github.com/osbuild/osbuild-composer/internal/osbuild"
 	"github.com/osbuild/osbuild-composer/internal/target"
-	"time"
 )
 
 type StateTransitionError struct {
@@ -31,7 +32,7 @@ type ImageBuild struct {
 	Distro      common.Distribution    `json:"distro"`
 	QueueStatus common.ImageBuildState `json:"queue_status"`
 	ImageType   common.ImageType       `json:"image_type"`
-	Pipeline    *pipeline.Pipeline     `json:"pipeline"`
+	Pipeline    *osbuild.Pipeline      `json:"pipeline"`
 	Targets     []*target.Target       `json:"targets"`
 	JobCreated  time.Time              `json:"job_created"`
 	JobStarted  time.Time              `json:"job_started"`
@@ -47,7 +48,7 @@ func (ib *ImageBuild) DeepCopy() ImageBuild {
 		imageCopy := *ib.Image
 		newImagePtr = &imageCopy
 	}
-	var newPipelinePtr *pipeline.Pipeline = nil
+	var newPipelinePtr *osbuild.Pipeline = nil
 	if ib.Pipeline != nil {
 		pipelineCopy := *ib.Pipeline
 		newPipelinePtr = &pipelineCopy
diff --git a/internal/distro/distro.go b/internal/distro/distro.go
index 4b25cdd32..cf74fa813 100644
--- a/internal/distro/distro.go
+++ b/internal/distro/distro.go
@@ -3,13 +3,14 @@ package distro
 import (
 	"bufio"
 	"errors"
-	"github.com/osbuild/osbuild-composer/internal/common"
 	"io"
 	"os"
 	"strings"
 
+	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/osbuild"
+
 	"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"
@@ -24,6 +25,10 @@ type Distro interface {
 	// Return strong-typed distribution
 	Distribution() common.Distribution
 
+	// Returns the module platform id of the distro. This is used by DNF
+	// for modularity support.
+	ModulePlatformID() string
+
 	// Returns a list of repositories from which this distribution gets its
 	// content.
 	Repositories(arch string) []rpmmd.RepoConfig
@@ -39,10 +44,20 @@ type Distro interface {
 	// is 0 the default value for the format will be returned.
 	GetSizeForOutputType(outputFormat string, size uint64) uint64
 
+	// Returns the base packages for a given output type and architecture
+	BasePackages(outputFormat, outputArchitecture string) ([]string, []string, error)
+
+	// Returns the build packages for a given output architecture
+	BuildPackages(outputArchitecture 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.
-	Pipeline(b *blueprint.Blueprint, additionalRepos []rpmmd.RepoConfig, checksums map[string]string, outputArchitecture, outputFormat string, size uint64) (*pipeline.Pipeline, error)
+	Pipeline(b *blueprint.Blueprint, additionalRepos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, checksums map[string]string, outputArchitecture, outputFormat string, size uint64) (*osbuild.Pipeline, error)
+
+	// Returns an osbuild sources object that is required for building the
+	// corresponding pipeline containing the given packages.
+	Sources(packages []rpmmd.PackageSpec) *osbuild.Sources
 
 	// Returns a osbuild runner that can be used on this distro.
 	Runner() string
diff --git a/internal/distro/fedora30/distro.go b/internal/distro/fedora30/distro.go
index 062d4b878..7387c95ee 100644
--- a/internal/distro/fedora30/distro.go
+++ b/internal/distro/fedora30/distro.go
@@ -2,22 +2,24 @@ package fedora30
 
 import (
 	"errors"
-	"github.com/osbuild/osbuild-composer/internal/common"
 	"log"
 	"sort"
 	"strconv"
 
+	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/osbuild"
+
 	"github.com/google/uuid"
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/crypt"
-	"github.com/osbuild/osbuild-composer/internal/pipeline"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
 
 type Fedora30 struct {
-	arches  map[string]arch
-	outputs map[string]output
+	arches        map[string]arch
+	outputs       map[string]output
+	buildPackages []string
 }
 
 type arch struct {
@@ -38,10 +40,11 @@ type output struct {
 	KernelOptions    string
 	Bootable         bool
 	DefaultSize      uint64
-	Assembler        func(uefi bool, size uint64) *pipeline.Assembler
+	Assembler        func(uefi bool, size uint64) *osbuild.Assembler
 }
 
 const Distro = common.Fedora30
+const ModulePlatformID = "platform:f30"
 
 func New(confPaths []string) *Fedora30 {
 	const GigaByte = 1024 * 1024 * 1024
@@ -49,6 +52,15 @@ func New(confPaths []string) *Fedora30 {
 	r := Fedora30{
 		arches:  map[string]arch{},
 		outputs: map[string]output{},
+		buildPackages: []string{
+			"dnf",
+			"dosfstools",
+			"e2fsprogs",
+			"policycoreutils",
+			"qemu-img",
+			"systemd",
+			"tar",
+		},
 	}
 
 	repoMap, err := rpmmd.LoadRepositories(confPaths, r.Name())
@@ -115,7 +127,7 @@ func New(confPaths []string) *Fedora30 {
 		KernelOptions: "ro no_timer_check console=ttyS0,115200n8 console=tty1 biosdevname=0 net.ifnames=0 console=ttyS0,115200",
 		Bootable:      true,
 		DefaultSize:   6 * GigaByte,
-		Assembler: func(uefi bool, size uint64) *pipeline.Assembler {
+		Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
 			return r.qemuAssembler("raw.xz", "image.raw.xz", uefi, size)
 		},
 	}
@@ -137,7 +149,7 @@ func New(confPaths []string) *Fedora30 {
 		KernelOptions: "ro biosdevname=0 net.ifnames=0",
 		Bootable:      false,
 		DefaultSize:   2 * GigaByte,
-		Assembler:     func(uefi bool, size uint64) *pipeline.Assembler { return r.rawFSAssembler("filesystem.img", size) },
+		Assembler:     func(uefi bool, size uint64) *osbuild.Assembler { return r.rawFSAssembler("filesystem.img", size) },
 	}
 
 	r.outputs["partitioned-disk"] = output{
@@ -157,7 +169,7 @@ func New(confPaths []string) *Fedora30 {
 		KernelOptions: "ro biosdevname=0 net.ifnames=0",
 		Bootable:      true,
 		DefaultSize:   2 * GigaByte,
-		Assembler: func(uefi bool, size uint64) *pipeline.Assembler {
+		Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
 			return r.qemuAssembler("raw", "disk.img", uefi, size)
 		},
 	}
@@ -184,7 +196,7 @@ func New(confPaths []string) *Fedora30 {
 		KernelOptions: "ro biosdevname=0 net.ifnames=0",
 		Bootable:      true,
 		DefaultSize:   2 * GigaByte,
-		Assembler: func(uefi bool, size uint64) *pipeline.Assembler {
+		Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
 			return r.qemuAssembler("qcow2", "disk.qcow2", uefi, size)
 		},
 	}
@@ -210,7 +222,7 @@ func New(confPaths []string) *Fedora30 {
 		KernelOptions: "ro biosdevname=0 net.ifnames=0",
 		Bootable:      true,
 		DefaultSize:   2 * GigaByte,
-		Assembler: func(uefi bool, size uint64) *pipeline.Assembler {
+		Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
 			return r.qemuAssembler("qcow2", "disk.qcow2", uefi, size)
 		},
 	}
@@ -232,7 +244,7 @@ func New(confPaths []string) *Fedora30 {
 		KernelOptions: "ro biosdevname=0 net.ifnames=0",
 		Bootable:      false,
 		DefaultSize:   2 * GigaByte,
-		Assembler:     func(uefi bool, size uint64) *pipeline.Assembler { return r.tarAssembler("root.tar.xz", "xz") },
+		Assembler:     func(uefi bool, size uint64) *osbuild.Assembler { return r.tarAssembler("root.tar.xz", "xz") },
 	}
 
 	r.outputs["vhd"] = output{
@@ -267,7 +279,7 @@ func New(confPaths []string) *Fedora30 {
 		KernelOptions: "ro biosdevname=0 rootdelay=300 console=ttyS0 earlyprintk=ttyS0 net.ifnames=0",
 		Bootable:      true,
 		DefaultSize:   2 * GigaByte,
-		Assembler: func(uefi bool, size uint64) *pipeline.Assembler {
+		Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
 			return r.qemuAssembler("vpc", "disk.vhd", uefi, size)
 		},
 	}
@@ -290,7 +302,7 @@ func New(confPaths []string) *Fedora30 {
 		KernelOptions: "ro biosdevname=0 net.ifnames=0",
 		Bootable:      true,
 		DefaultSize:   2 * GigaByte,
-		Assembler: func(uefi bool, size uint64) *pipeline.Assembler {
+		Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
 			return r.qemuAssembler("vmdk", "disk.vmdk", uefi, size)
 		},
 	}
@@ -310,6 +322,10 @@ func (r *Fedora30) Distribution() common.Distribution {
 	return Distro
 }
 
+func (r *Fedora30) ModulePlatformID() string {
+	return ModulePlatformID
+}
+
 func (r *Fedora30) Repositories(arch string) []rpmmd.RepoConfig {
 	return r.arches[arch].Repositories
 }
@@ -342,7 +358,35 @@ func (r *Fedora30) GetSizeForOutputType(outputFormat string, size uint64) uint64
 	return size
 }
 
-func (r *Fedora30) Pipeline(b *blueprint.Blueprint, additionalRepos []rpmmd.RepoConfig, checksums map[string]string, outputArchitecture, outputFormat string, size uint64) (*pipeline.Pipeline, error) {
+func (r *Fedora30) BasePackages(outputFormat string, outputArchitecture string) ([]string, []string, error) {
+	output, exists := r.outputs[outputFormat]
+	if !exists {
+		return nil, nil, errors.New("invalid output format: " + outputFormat)
+	}
+
+	packages := output.Packages
+	if output.Bootable {
+		arch, exists := r.arches[outputArchitecture]
+		if !exists {
+			return nil, nil, errors.New("invalid architecture: " + outputArchitecture)
+		}
+
+		packages = append(packages, arch.BootloaderPackages...)
+	}
+
+	return packages, output.ExcludedPackages, nil
+}
+
+func (r *Fedora30) BuildPackages(outputArchitecture string) ([]string, error) {
+	arch, exists := r.arches[outputArchitecture]
+	if !exists {
+		return nil, errors.New("invalid architecture: " + outputArchitecture)
+	}
+
+	return append(r.buildPackages, arch.BuildPackages...), nil
+}
+
+func (r *Fedora30) Pipeline(b *blueprint.Blueprint, additionalRepos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, checksums map[string]string, outputArchitecture, outputFormat string, size uint64) (*osbuild.Pipeline, error) {
 	output, exists := r.outputs[outputFormat]
 	if !exists {
 		return nil, errors.New("invalid output format: " + outputFormat)
@@ -353,42 +397,44 @@ func (r *Fedora30) Pipeline(b *blueprint.Blueprint, additionalRepos []rpmmd.Repo
 		return nil, errors.New("invalid architecture: " + outputArchitecture)
 	}
 
-	p := &pipeline.Pipeline{}
+	p := &osbuild.Pipeline{}
 	p.SetBuild(r.buildPipeline(arch, checksums), "org.osbuild.fedora30")
 
-	packages := append(output.Packages, b.GetPackages()...)
-	if output.Bootable {
-		packages = append(packages, arch.BootloaderPackages...)
+	packages, excludedPackages, err := r.BasePackages(outputFormat, outputArchitecture)
+	if err != nil {
+		return nil, err
 	}
-	p.AddStage(pipeline.NewDNFStage(r.dnfStageOptions(arch, additionalRepos, checksums, packages, output.ExcludedPackages)))
-	p.AddStage(pipeline.NewFixBLSStage())
+	packages = append(packages, b.GetPackages()...)
+
+	p.AddStage(osbuild.NewDNFStage(r.dnfStageOptions(arch, additionalRepos, checksums, packages, excludedPackages)))
+	p.AddStage(osbuild.NewFixBLSStage())
 
 	// TODO support setting all languages and install corresponding langpack-* package
 	language, keyboard := b.GetPrimaryLocale()
 
 	if language != nil {
-		p.AddStage(pipeline.NewLocaleStage(&pipeline.LocaleStageOptions{*language}))
+		p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{*language}))
 	} else {
-		p.AddStage(pipeline.NewLocaleStage(&pipeline.LocaleStageOptions{"en_US"}))
+		p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{"en_US"}))
 	}
 
 	if keyboard != nil {
-		p.AddStage(pipeline.NewKeymapStage(&pipeline.KeymapStageOptions{*keyboard}))
+		p.AddStage(osbuild.NewKeymapStage(&osbuild.KeymapStageOptions{*keyboard}))
 	}
 
 	if hostname := b.GetHostname(); hostname != nil {
-		p.AddStage(pipeline.NewHostnameStage(&pipeline.HostnameStageOptions{*hostname}))
+		p.AddStage(osbuild.NewHostnameStage(&osbuild.HostnameStageOptions{*hostname}))
 	}
 
 	timezone, ntpServers := b.GetTimezoneSettings()
 
 	// TODO install chrony when this is set?
 	if timezone != nil {
-		p.AddStage(pipeline.NewTimezoneStage(&pipeline.TimezoneStageOptions{*timezone}))
+		p.AddStage(osbuild.NewTimezoneStage(&osbuild.TimezoneStageOptions{*timezone}))
 	}
 
 	if len(ntpServers) > 0 {
-		p.AddStage(pipeline.NewChronyStage(&pipeline.ChronyStageOptions{ntpServers}))
+		p.AddStage(osbuild.NewChronyStage(&osbuild.ChronyStageOptions{ntpServers}))
 	}
 
 	if users := b.GetUsers(); len(users) > 0 {
@@ -396,62 +442,61 @@ func (r *Fedora30) Pipeline(b *blueprint.Blueprint, additionalRepos []rpmmd.Repo
 		if err != nil {
 			return nil, err
 		}
-		p.AddStage(pipeline.NewUsersStage(options))
+		p.AddStage(osbuild.NewUsersStage(options))
 	}
 
 	if groups := b.GetGroups(); len(groups) > 0 {
-		p.AddStage(pipeline.NewGroupsStage(r.groupStageOptions(groups)))
+		p.AddStage(osbuild.NewGroupsStage(r.groupStageOptions(groups)))
 	}
 
 	if output.Bootable {
-		p.AddStage(pipeline.NewFSTabStage(r.fsTabStageOptions(arch.UEFI)))
+		p.AddStage(osbuild.NewFSTabStage(r.fsTabStageOptions(arch.UEFI)))
 	}
-	p.AddStage(pipeline.NewGRUB2Stage(r.grub2StageOptions(output.KernelOptions, b.GetKernel(), arch.UEFI)))
+	p.AddStage(osbuild.NewGRUB2Stage(r.grub2StageOptions(output.KernelOptions, b.GetKernel(), arch.UEFI)))
 
 	if services := b.GetServices(); services != nil || output.EnabledServices != nil {
-		p.AddStage(pipeline.NewSystemdStage(r.systemdStageOptions(output.EnabledServices, output.DisabledServices, services)))
+		p.AddStage(osbuild.NewSystemdStage(r.systemdStageOptions(output.EnabledServices, output.DisabledServices, services)))
 	}
 
 	if firewall := b.GetFirewall(); firewall != nil {
-		p.AddStage(pipeline.NewFirewallStage(r.firewallStageOptions(firewall)))
+		p.AddStage(osbuild.NewFirewallStage(r.firewallStageOptions(firewall)))
 	}
 
-	p.AddStage(pipeline.NewSELinuxStage(r.selinuxStageOptions()))
+	p.AddStage(osbuild.NewSELinuxStage(r.selinuxStageOptions()))
 
 	p.Assembler = output.Assembler(arch.UEFI, size)
 
 	return p, nil
 }
 
+func (r *Fedora30) Sources(packages []rpmmd.PackageSpec) *osbuild.Sources {
+	return &osbuild.Sources{}
+}
+
 func (r *Fedora30) Runner() string {
 	return "org.osbuild.fedora30"
 }
 
-func (r *Fedora30) buildPipeline(arch arch, checksums map[string]string) *pipeline.Pipeline {
-	packages := []string{
-		"dnf",
-		"dosfstools",
-		"e2fsprogs",
-		"policycoreutils",
-		"qemu-img",
-		"systemd",
-		"tar",
-	}
-	packages = append(packages, arch.BuildPackages...)
-	p := &pipeline.Pipeline{}
-	p.AddStage(pipeline.NewDNFStage(r.dnfStageOptions(arch, nil, checksums, packages, nil)))
+func (r *Fedora30) buildPipeline(arch arch, checksums map[string]string) *osbuild.Pipeline {
+	packages, err := r.BuildPackages(arch.Name)
+	if err != nil {
+		panic("impossibly invalid arch")
+	}
+
+	p := &osbuild.Pipeline{}
+	p.AddStage(osbuild.NewDNFStage(r.dnfStageOptions(arch, nil, checksums, packages, nil)))
 	return p
 }
 
-func (r *Fedora30) dnfStageOptions(arch arch, additionalRepos []rpmmd.RepoConfig, checksums map[string]string, packages, excludedPackages []string) *pipeline.DNFStageOptions {
-	options := &pipeline.DNFStageOptions{
+func (r *Fedora30) dnfStageOptions(arch arch, additionalRepos []rpmmd.RepoConfig, checksums map[string]string, packages, excludedPackages []string) *osbuild.DNFStageOptions {
+	options := &osbuild.DNFStageOptions{
 		ReleaseVersion:   "30",
 		BaseArchitecture: arch.Name,
-		ModulePlatformId: "platform:f30",
+		ModulePlatformId: ModulePlatformID,
 	}
 
 	for _, repo := range append(arch.Repositories, additionalRepos...) {
-		options.AddRepository(&pipeline.DNFRepository{
+		options.AddRepository(&osbuild.DNFRepository{
 			BaseURL:    repo.BaseURL,
 			MetaLink:   repo.Metalink,
 			MirrorList: repo.MirrorList,
@@ -473,9 +518,9 @@ func (r *Fedora30) dnfStageOptions(arch arch, additionalRepos []rpmmd.RepoConfig
 	return options
 }
 
-func (r *Fedora30) userStageOptions(users []blueprint.UserCustomization) (*pipeline.UsersStageOptions, error) {
-	options := pipeline.UsersStageOptions{
-		Users: make(map[string]pipeline.UsersStageOptionsUser),
+func (r *Fedora30) userStageOptions(users []blueprint.UserCustomization) (*osbuild.UsersStageOptions, error) {
+	options := osbuild.UsersStageOptions{
+		Users: make(map[string]osbuild.UsersStageOptionsUser),
 	}
 
 	for _, c := range users {
@@ -488,7 +533,7 @@ func (r *Fedora30) userStageOptions(users []blueprint.UserCustomization) (*pipel
 			c.Password = &cryptedPassword
 		}
 
-		user := pipeline.UsersStageOptionsUser{
+		user := osbuild.UsersStageOptionsUser{
 			Groups:      c.Groups,
 			Description: c.Description,
 			Home:        c.Home,
@@ -513,13 +558,13 @@ func (r *Fedora30) userStageOptions(users []blueprint.UserCustomization) (*pipel
 	return &options, nil
 }
 
-func (r *Fedora30) groupStageOptions(groups []blueprint.GroupCustomization) *pipeline.GroupsStageOptions {
-	options := pipeline.GroupsStageOptions{
-		Groups: map[string]pipeline.GroupsStageOptionsGroup{},
+func (r *Fedora30) groupStageOptions(groups []blueprint.GroupCustomization) *osbuild.GroupsStageOptions {
+	options := osbuild.GroupsStageOptions{
+		Groups: map[string]osbuild.GroupsStageOptionsGroup{},
 	}
 
 	for _, group := range groups {
-		groupData := pipeline.GroupsStageOptionsGroup{
+		groupData := osbuild.GroupsStageOptionsGroup{
 			Name: group.Name,
 		}
 		if group.GID != nil {
@@ -533,8 +578,8 @@ func (r *Fedora30) groupStageOptions(groups []blueprint.GroupCustomization) *pip
 	return &options
 }
 
-func (r *Fedora30) firewallStageOptions(firewall *blueprint.FirewallCustomization) *pipeline.FirewallStageOptions {
-	options := pipeline.FirewallStageOptions{
+func (r *Fedora30) firewallStageOptions(firewall *blueprint.FirewallCustomization) *osbuild.FirewallStageOptions {
+	options := osbuild.FirewallStageOptions{
 		Ports: firewall.Ports,
 	}
 
@@ -546,19 +591,19 @@ func (r *Fedora30) firewallStageOptions(firewall *blueprint.FirewallCustomizatio
 	return &options
 }
 
-func (r *Fedora30) systemdStageOptions(enabledServices, disabledServices []string, s *blueprint.ServicesCustomization) *pipeline.SystemdStageOptions {
+func (r *Fedora30) systemdStageOptions(enabledServices, disabledServices []string, s *blueprint.ServicesCustomization) *osbuild.SystemdStageOptions {
 	if s != nil {
 		enabledServices = append(enabledServices, s.Enabled...)
 		disabledServices = append(disabledServices, s.Disabled...)
 	}
-	return &pipeline.SystemdStageOptions{
+	return &osbuild.SystemdStageOptions{
 		EnabledServices:  enabledServices,
 		DisabledServices: disabledServices,
 	}
 }
 
-func (r *Fedora30) fsTabStageOptions(uefi bool) *pipeline.FSTabStageOptions {
-	options := pipeline.FSTabStageOptions{}
+func (r *Fedora30) fsTabStageOptions(uefi bool) *osbuild.FSTabStageOptions {
+	options := osbuild.FSTabStageOptions{}
 	options.AddFilesystem("76a22bf4-f153-4541-b6c7-0332c0dfaeac", "ext4", "/", "defaults", 1, 1)
 	if uefi {
 		options.AddFilesystem("46BB-8120", "vfat", "/boot/efi", "umask=0077,shortname=winnt", 0, 2)
@@ -566,7 +611,7 @@ func (r *Fedora30) fsTabStageOptions(uefi bool) *pipeline.FSTabStageOptions {
 	return &options
 }
 
-func (r *Fedora30) grub2StageOptions(kernelOptions string, kernel *blueprint.KernelCustomization, uefi bool) *pipeline.GRUB2StageOptions {
+func (r *Fedora30) grub2StageOptions(kernelOptions string, kernel *blueprint.KernelCustomization, uefi bool) *osbuild.GRUB2StageOptions {
 	id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
 	if err != nil {
 		panic("invalid UUID")
@@ -576,14 +621,14 @@ func (r *Fedora30) grub2StageOptions(kernelOptions string, kernel *blueprint.Ker
 		kernelOptions += " " + kernel.Append
 	}
 
-	var uefiOptions *pipeline.GRUB2UEFI
+	var uefiOptions *osbuild.GRUB2UEFI
 	if uefi {
-		uefiOptions = &pipeline.GRUB2UEFI{
+		uefiOptions = &osbuild.GRUB2UEFI{
 			Vendor: "fedora",
 		}
 	}
 
-	return &pipeline.GRUB2StageOptions{
+	return &osbuild.GRUB2StageOptions{
 		RootFilesystemUUID: id,
 		KernelOptions:      kernelOptions,
 		Legacy:             !uefi,
@@ -591,28 +636,28 @@ func (r *Fedora30) grub2StageOptions(kernelOptions string, kernel *blueprint.Ker
 	}
 }
 
-func (r *Fedora30) selinuxStageOptions() *pipeline.SELinuxStageOptions {
-	return &pipeline.SELinuxStageOptions{
+func (r *Fedora30) selinuxStageOptions() *osbuild.SELinuxStageOptions {
+	return &osbuild.SELinuxStageOptions{
 		FileContexts: "etc/selinux/targeted/contexts/files/file_contexts",
 	}
 }
 
-func (r *Fedora30) qemuAssembler(format string, filename string, uefi bool, size uint64) *pipeline.Assembler {
-	var options pipeline.QEMUAssemblerOptions
+func (r *Fedora30) qemuAssembler(format string, filename string, uefi bool, size uint64) *osbuild.Assembler {
+	var options osbuild.QEMUAssemblerOptions
 	if uefi {
 		fstype := uuid.MustParse("C12A7328-F81F-11D2-BA4B-00A0C93EC93B")
-		options = pipeline.QEMUAssemblerOptions{
+		options = osbuild.QEMUAssemblerOptions{
 			Format:   format,
 			Filename: filename,
 			Size:     size,
 			PTUUID:   "8DFDFF87-C96E-EA48-A3A6-9408F1F6B1EF",
 			PTType:   "gpt",
-			Partitions: []pipeline.QEMUPartition{
+			Partitions: []osbuild.QEMUPartition{
 				{
 					Start: 2048,
 					Size:  972800,
 					Type:  &fstype,
-					Filesystem: pipeline.QEMUFilesystem{
+					Filesystem: osbuild.QEMUFilesystem{
 						Type:       "vfat",
 						UUID:       "46BB-8120",
 						Label:      "EFI System Partition",
@@ -621,7 +666,7 @@ func (r *Fedora30) qemuAssembler(format string, filename string, uefi bool, size
 				},
 				{
 					Start: 976896,
-					Filesystem: pipeline.QEMUFilesystem{
+					Filesystem: osbuild.QEMUFilesystem{
 						Type:       "ext4",
 						UUID:       "76a22bf4-f153-4541-b6c7-0332c0dfaeac",
 						Mountpoint: "/",
@@ -630,17 +675,17 @@ func (r *Fedora30) qemuAssembler(format string, filename string, uefi bool, size
 			},
 		}
 	} else {
-		options = pipeline.QEMUAssemblerOptions{
+		options = osbuild.QEMUAssemblerOptions{
 			Format:   format,
 			Filename: filename,
 			Size:     size,
 			PTUUID:   "0x14fc63d2",
 			PTType:   "mbr",
-			Partitions: []pipeline.QEMUPartition{
+			Partitions: []osbuild.QEMUPartition{
 				{
 					Start:    2048,
 					Bootable: true,
-					Filesystem: pipeline.QEMUFilesystem{
+					Filesystem: osbuild.QEMUFilesystem{
 						Type:       "ext4",
 						UUID:       "76a22bf4-f153-4541-b6c7-0332c0dfaeac",
 						Mountpoint: "/",
@@ -649,24 +694,24 @@ func (r *Fedora30) qemuAssembler(format string, filename string, uefi bool, size
 			},
 		}
 	}
-	return pipeline.NewQEMUAssembler(&options)
+	return osbuild.NewQEMUAssembler(&options)
 }
 
-func (r *Fedora30) tarAssembler(filename, compression string) *pipeline.Assembler {
-	return pipeline.NewTarAssembler(
-		&pipeline.TarAssemblerOptions{
+func (r *Fedora30) tarAssembler(filename, compression string) *osbuild.Assembler {
+	return osbuild.NewTarAssembler(
+		&osbuild.TarAssemblerOptions{
 			Filename:    filename,
 			Compression: compression,
 		})
 }
 
-func (r *Fedora30) rawFSAssembler(filename string, size uint64) *pipeline.Assembler {
+func (r *Fedora30) rawFSAssembler(filename string, size uint64) *osbuild.Assembler {
 	id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
 	if err != nil {
 		panic("invalid UUID")
 	}
-	return pipeline.NewRawFSAssembler(
-		&pipeline.RawFSAssemblerOptions{
+	return osbuild.NewRawFSAssembler(
+		&osbuild.RawFSAssemblerOptions{
 			Filename:           filename,
 			RootFilesystemUUDI: id,
 			Size:               size,
diff --git a/internal/distro/rhel82/distro.go b/internal/distro/rhel82/distro.go
index 77662ba68..657e97fb2 100644
--- a/internal/distro/rhel82/distro.go
+++ b/internal/distro/rhel82/distro.go
@@ -2,22 +2,24 @@ package rhel82
 
 import (
 	"errors"
-	"github.com/osbuild/osbuild-composer/internal/common"
 	"log"
 	"sort"
 	"strconv"
 
+	"github.com/osbuild/osbuild-composer/internal/common"
+	"github.com/osbuild/osbuild-composer/internal/osbuild"
+
 	"github.com/google/uuid"
 
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/crypt"
-	"github.com/osbuild/osbuild-composer/internal/pipeline"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
 
 type RHEL82 struct {
-	arches  map[string]arch
-	outputs map[string]output
+	arches        map[string]arch
+	outputs       map[string]output
+	buildPackages []string
 }
 
 type arch struct {
@@ -39,10 +41,11 @@ type output struct {
 	DefaultTarget    string
 	KernelOptions    string
 	DefaultSize      uint64
-	Assembler        func(uefi bool, size uint64) *pipeline.Assembler
+	Assembler        func(uefi bool, size uint64) *osbuild.Assembler
 }
 
 const Distro = common.RHEL82
+const ModulePlatformID = "platform:el8"
 
 func New(confPaths []string) *RHEL82 {
 	const GigaByte = 1024 * 1024 * 1024
@@ -50,6 +53,19 @@ func New(confPaths []string) *RHEL82 {
 	r := RHEL82{
 		arches:  map[string]arch{},
 		outputs: map[string]output{},
+		buildPackages: []string{
+			"dnf",
+			"dosfstools",
+			"dracut-config-generic",
+			"e2fsprogs",
+			"glibc",
+			"policycoreutils",
+			"python36",
+			"qemu-img",
+			"systemd",
+			"tar",
+			"xfsprogs",
+		},
 	}
 
 	repoMap, err := rpmmd.LoadRepositories(confPaths, r.Name())
@@ -164,7 +180,7 @@ func New(confPaths []string) *RHEL82 {
 		Bootable:      true,
 		KernelOptions: "ro console=ttyS0,115200n8 console=tty0 net.ifnames=0 rd.blacklist=nouveau nvme_core.io_timeout=4294967295 crashkernel=auto",
 		DefaultSize:   6 * GigaByte,
-		Assembler: func(uefi bool, size uint64) *pipeline.Assembler {
+		Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
 			return r.qemuAssembler("raw.xz", "image.raw.xz", uefi, size)
 		},
 	}
@@ -191,7 +207,7 @@ func New(confPaths []string) *RHEL82 {
 		Bootable:      false,
 		KernelOptions: "ro net.ifnames=0",
 		DefaultSize:   2 * GigaByte,
-		Assembler:     func(uefi bool, size uint64) *pipeline.Assembler { return r.rawFSAssembler("filesystem.img", size) },
+		Assembler:     func(uefi bool, size uint64) *osbuild.Assembler { return r.rawFSAssembler("filesystem.img", size) },
 	}
 
 	r.outputs["partitioned-disk"] = output{
@@ -216,7 +232,7 @@ func New(confPaths []string) *RHEL82 {
 		Bootable:      true,
 		KernelOptions: "ro net.ifnames=0",
 		DefaultSize:   2 * GigaByte,
-		Assembler: func(uefi bool, size uint64) *pipeline.Assembler {
+		Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
 			return r.qemuAssembler("raw", "disk.img", uefi, size)
 		},
 	}
@@ -300,7 +316,7 @@ func New(confPaths []string) *RHEL82 {
 		Bootable:      true,
 		KernelOptions: "console=ttyS0 console=ttyS0,115200n8 no_timer_check crashkernel=auto net.ifnames=0",
 		DefaultSize:   2 * GigaByte,
-		Assembler: func(uefi bool, size uint64) *pipeline.Assembler {
+		Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
 			return r.qemuAssembler("qcow2", "disk.qcow2", uefi, size)
 		},
 	}
@@ -330,7 +346,7 @@ func New(confPaths []string) *RHEL82 {
 		Bootable:      true,
 		KernelOptions: "ro net.ifnames=0",
 		DefaultSize:   2 * GigaByte,
-		Assembler: func(uefi bool, size uint64) *pipeline.Assembler {
+		Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
 			return r.qemuAssembler("qcow2", "disk.qcow2", uefi, size)
 		},
 	}
@@ -356,7 +372,7 @@ func New(confPaths []string) *RHEL82 {
 		},
 		Bootable:      false,
 		KernelOptions: "ro net.ifnames=0",
-		Assembler:     func(uefi bool, size uint64) *pipeline.Assembler { return r.tarAssembler("root.tar.xz", "xz") },
+		Assembler:     func(uefi bool, size uint64) *osbuild.Assembler { return r.tarAssembler("root.tar.xz", "xz") },
 	}
 
 	r.outputs["vhd"] = output{
@@ -397,7 +413,7 @@ func New(confPaths []string) *RHEL82 {
 		Bootable:      true,
 		KernelOptions: "ro biosdevname=0 rootdelay=300 console=ttyS0 earlyprintk=ttyS0 net.ifnames=0",
 		DefaultSize:   2 * GigaByte,
-		Assembler: func(uefi bool, size uint64) *pipeline.Assembler {
+		Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
 			return r.qemuAssembler("vpc", "disk.vhd", uefi, size)
 		},
 	}
@@ -425,7 +441,7 @@ func New(confPaths []string) *RHEL82 {
 		Bootable:      true,
 		KernelOptions: "ro net.ifnames=0",
 		DefaultSize:   2 * GigaByte,
-		Assembler: func(uefi bool, size uint64) *pipeline.Assembler {
+		Assembler: func(uefi bool, size uint64) *osbuild.Assembler {
 			return r.qemuAssembler("vmdk", "disk.vmdk", uefi, size)
 		},
 	}
@@ -445,6 +461,10 @@ func (r *RHEL82) Distribution() common.Distribution {
 	return Distro
 }
 
+func (r *RHEL82) ModulePlatformID() string {
+	return ModulePlatformID
+}
+
 func (r *RHEL82) Repositories(arch string) []rpmmd.RepoConfig {
 	return r.arches[arch].Repositories
 }
@@ -477,7 +497,35 @@ func (r *RHEL82) GetSizeForOutputType(outputFormat string, size uint64) uint64 {
 	return size
 }
 
-func (r *RHEL82) Pipeline(b *blueprint.Blueprint, additionalRepos []rpmmd.RepoConfig, checksums map[string]string, outputArchitecture, outputFormat string, size uint64) (*pipeline.Pipeline, error) {
+func (r *RHEL82) BasePackages(outputFormat string, outputArchitecture string) ([]string, []string, error) {
+	output, exists := r.outputs[outputFormat]
+	if !exists {
+		return nil, nil, errors.New("invalid output format: " + outputFormat)
+	}
+
+	packages := output.Packages
+	if output.Bootable {
+		arch, exists := r.arches[outputArchitecture]
+		if !exists {
+			return nil, nil, errors.New("invalid architecture: " + outputArchitecture)
+		}
+
+		packages = append(packages, arch.BootloaderPackages...)
+	}
+
+	return packages, output.ExcludedPackages, nil
+}
+
+func (r *RHEL82) BuildPackages(outputArchitecture string) ([]string, error) {
+	arch, exists := r.arches[outputArchitecture]
+	if !exists {
+		return nil, errors.New("invalid architecture: " + outputArchitecture)
+	}
+
+	return append(r.buildPackages, arch.BuildPackages...), nil
+}
+
+func (r *RHEL82) Pipeline(b *blueprint.Blueprint, additionalRepos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, checksums map[string]string, outputArchitecture, outputFormat string, size uint64) (*osbuild.Pipeline, error) {
 	output, exists := r.outputs[outputFormat]
 	if !exists {
 		return nil, errors.New("invalid output format: " + outputFormat)
@@ -488,52 +536,53 @@ func (r *RHEL82) Pipeline(b *blueprint.Blueprint, additionalRepos []rpmmd.RepoCo
 		return nil, errors.New("invalid architecture: " + outputArchitecture)
 	}
 
-	p := &pipeline.Pipeline{}
+	p := &osbuild.Pipeline{}
 	p.SetBuild(r.buildPipeline(arch, checksums), "org.osbuild.rhel82")
 
-	packages := append(output.Packages, b.GetPackages()...)
-	if output.Bootable {
-		packages = append(packages, arch.BootloaderPackages...)
+	packages, excludedPackages, err := r.BasePackages(outputFormat, outputArchitecture)
+	if err != nil {
+		return nil, err
 	}
-	p.AddStage(pipeline.NewDNFStage(r.dnfStageOptions(arch, additionalRepos, checksums, packages, output.ExcludedPackages)))
-	p.AddStage(pipeline.NewFixBLSStage())
+	packages = append(packages, b.GetPackages()...)
+	p.AddStage(osbuild.NewDNFStage(r.dnfStageOptions(arch, additionalRepos, checksums, packages, excludedPackages)))
+	p.AddStage(osbuild.NewFixBLSStage())
 
 	if output.Bootable {
-		p.AddStage(pipeline.NewFSTabStage(r.fsTabStageOptions(arch.UEFI)))
+		p.AddStage(osbuild.NewFSTabStage(r.fsTabStageOptions(arch.UEFI)))
 	}
 
 	kernelOptions := output.KernelOptions
 	if kernel := b.GetKernel(); kernel != nil {
 		kernelOptions += " " + kernel.Append
 	}
-	p.AddStage(pipeline.NewGRUB2Stage(r.grub2StageOptions(kernelOptions, arch.UEFI)))
+	p.AddStage(osbuild.NewGRUB2Stage(r.grub2StageOptions(kernelOptions, arch.UEFI)))
 
 	// TODO support setting all languages and install corresponding langpack-* package
 	language, keyboard := b.GetPrimaryLocale()
 
 	if language != nil {
-		p.AddStage(pipeline.NewLocaleStage(&pipeline.LocaleStageOptions{*language}))
+		p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{*language}))
 	} else {
-		p.AddStage(pipeline.NewLocaleStage(&pipeline.LocaleStageOptions{"en_US"}))
+		p.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{"en_US"}))
 	}
 
 	if keyboard != nil {
-		p.AddStage(pipeline.NewKeymapStage(&pipeline.KeymapStageOptions{*keyboard}))
+		p.AddStage(osbuild.NewKeymapStage(&osbuild.KeymapStageOptions{*keyboard}))
 	}
 
 	if hostname := b.GetHostname(); hostname != nil {
-		p.AddStage(pipeline.NewHostnameStage(&pipeline.HostnameStageOptions{*hostname}))
+		p.AddStage(osbuild.NewHostnameStage(&osbuild.HostnameStageOptions{*hostname}))
 	}
 
 	timezone, ntpServers := b.GetTimezoneSettings()
 
 	// TODO install chrony when this is set?
 	if timezone != nil {
-		p.AddStage(pipeline.NewTimezoneStage(&pipeline.TimezoneStageOptions{*timezone}))
+		p.AddStage(osbuild.NewTimezoneStage(&osbuild.TimezoneStageOptions{*timezone}))
 	}
 
 	if len(ntpServers) > 0 {
-		p.AddStage(pipeline.NewChronyStage(&pipeline.ChronyStageOptions{ntpServers}))
+		p.AddStage(osbuild.NewChronyStage(&osbuild.ChronyStageOptions{ntpServers}))
 	}
 
 	if users := b.GetUsers(); len(users) > 0 {
@@ -541,60 +590,55 @@ func (r *RHEL82) Pipeline(b *blueprint.Blueprint, additionalRepos []rpmmd.RepoCo
 		if err != nil {
 			return nil, err
 		}
-		p.AddStage(pipeline.NewUsersStage(options))
+		p.AddStage(osbuild.NewUsersStage(options))
 	}
 
 	if groups := b.GetGroups(); len(groups) > 0 {
-		p.AddStage(pipeline.NewGroupsStage(r.groupStageOptions(groups)))
+		p.AddStage(osbuild.NewGroupsStage(r.groupStageOptions(groups)))
 	}
 
 	if services := b.GetServices(); services != nil || output.EnabledServices != nil {
-		p.AddStage(pipeline.NewSystemdStage(r.systemdStageOptions(output.EnabledServices, output.DisabledServices, services, output.DefaultTarget)))
+		p.AddStage(osbuild.NewSystemdStage(r.systemdStageOptions(output.EnabledServices, output.DisabledServices, services, output.DefaultTarget)))
 	}
 
 	if firewall := b.GetFirewall(); firewall != nil {
-		p.AddStage(pipeline.NewFirewallStage(r.firewallStageOptions(firewall)))
+		p.AddStage(osbuild.NewFirewallStage(r.firewallStageOptions(firewall)))
 	}
 
-	p.AddStage(pipeline.NewSELinuxStage(r.selinuxStageOptions()))
+	p.AddStage(osbuild.NewSELinuxStage(r.selinuxStageOptions()))
 
 	p.Assembler = output.Assembler(arch.UEFI, size)
 
 	return p, nil
 }
 
+func (r *RHEL82) Sources(packages []rpmmd.PackageSpec) *osbuild.Sources {
+	return &osbuild.Sources{}
+}
+
 func (r *RHEL82) Runner() string {
 	return "org.osbuild.rhel82"
 }
 
-func (r *RHEL82) buildPipeline(arch arch, checksums map[string]string) *pipeline.Pipeline {
-	packages := []string{
-		"dnf",
-		"dosfstools",
-		"dracut-config-generic",
-		"e2fsprogs",
-		"glibc",
-		"policycoreutils",
-		"python36",
-		"qemu-img",
-		"systemd",
-		"tar",
-		"xfsprogs",
-	}
-	packages = append(packages, arch.BuildPackages...)
-	p := &pipeline.Pipeline{}
-	p.AddStage(pipeline.NewDNFStage(r.dnfStageOptions(arch, nil, checksums, packages, nil)))
+func (r *RHEL82) buildPipeline(arch arch, checksums map[string]string) *osbuild.Pipeline {
+	packages, err := r.BuildPackages(arch.Name)
+	if err != nil {
+		panic("impossibly invalid arch")
+	}
+
+	p := &osbuild.Pipeline{}
+	p.AddStage(osbuild.NewDNFStage(r.dnfStageOptions(arch, nil, checksums, packages, nil)))
 	return p
 }
 
-func (r *RHEL82) dnfStageOptions(arch arch, additionalRepos []rpmmd.RepoConfig, checksums map[string]string, packages, excludedPackages []string) *pipeline.DNFStageOptions {
-	options := &pipeline.DNFStageOptions{
+func (r *RHEL82) dnfStageOptions(arch arch, additionalRepos []rpmmd.RepoConfig, checksums map[string]string, packages, excludedPackages []string) *osbuild.DNFStageOptions {
+	options := &osbuild.DNFStageOptions{
 		ReleaseVersion:   "8",
 		BaseArchitecture: arch.Name,
-		ModulePlatformId: "platform:el8",
+		ModulePlatformId: ModulePlatformID,
 	}
 	for _, repo := range append(arch.Repositories, additionalRepos...) {
-		options.AddRepository(&pipeline.DNFRepository{
+		options.AddRepository(&osbuild.DNFRepository{
 			BaseURL:    repo.BaseURL,
 			MetaLink:   repo.Metalink,
 			MirrorList: repo.MirrorList,
@@ -615,9 +659,9 @@ func (r *RHEL82) dnfStageOptions(arch arch, additionalRepos []rpmmd.RepoConfig,
 	return options
 }
 
-func (r *RHEL82) userStageOptions(users []blueprint.UserCustomization) (*pipeline.UsersStageOptions, error) {
-	options := pipeline.UsersStageOptions{
-		Users: make(map[string]pipeline.UsersStageOptionsUser),
+func (r *RHEL82) userStageOptions(users []blueprint.UserCustomization) (*osbuild.UsersStageOptions, error) {
+	options := osbuild.UsersStageOptions{
+		Users: make(map[string]osbuild.UsersStageOptionsUser),
 	}
 
 	for _, c := range users {
@@ -630,7 +674,7 @@ func (r *RHEL82) userStageOptions(users []blueprint.UserCustomization) (*pipelin
 			c.Password = &cryptedPassword
 		}
 
-		user := pipeline.UsersStageOptionsUser{
+		user := osbuild.UsersStageOptionsUser{
 			Groups:      c.Groups,
 			Description: c.Description,
 			Home:        c.Home,
@@ -655,13 +699,13 @@ func (r *RHEL82) userStageOptions(users []blueprint.UserCustomization) (*pipelin
 	return &options, nil
 }
 
-func (r *RHEL82) groupStageOptions(groups []blueprint.GroupCustomization) *pipeline.GroupsStageOptions {
-	options := pipeline.GroupsStageOptions{
-		Groups: map[string]pipeline.GroupsStageOptionsGroup{},
+func (r *RHEL82) groupStageOptions(groups []blueprint.GroupCustomization) *osbuild.GroupsStageOptions {
+	options := osbuild.GroupsStageOptions{
+		Groups: map[string]osbuild.GroupsStageOptionsGroup{},
 	}
 
 	for _, group := range groups {
-		groupData := pipeline.GroupsStageOptionsGroup{
+		groupData := osbuild.GroupsStageOptionsGroup{
 			Name: group.Name,
 		}
 		if group.GID != nil {
@@ -675,8 +719,8 @@ func (r *RHEL82) groupStageOptions(groups []blueprint.GroupCustomization) *pipel
 	return &options
 }
 
-func (r *RHEL82) firewallStageOptions(firewall *blueprint.FirewallCustomization) *pipeline.FirewallStageOptions {
-	options := pipeline.FirewallStageOptions{
+func (r *RHEL82) firewallStageOptions(firewall *blueprint.FirewallCustomization) *osbuild.FirewallStageOptions {
+	options := osbuild.FirewallStageOptions{
 		Ports: firewall.Ports,
 	}
 
@@ -688,20 +732,20 @@ func (r *RHEL82) firewallStageOptions(firewall *blueprint.FirewallCustomization)
 	return &options
 }
 
-func (r *RHEL82) systemdStageOptions(enabledServices, disabledServices []string, s *blueprint.ServicesCustomization, target string) *pipeline.SystemdStageOptions {
+func (r *RHEL82) systemdStageOptions(enabledServices, disabledServices []string, s *blueprint.ServicesCustomization, target string) *osbuild.SystemdStageOptions {
 	if s != nil {
 		enabledServices = append(enabledServices, s.Enabled...)
 		disabledServices = append(disabledServices, s.Disabled...)
 	}
-	return &pipeline.SystemdStageOptions{
+	return &osbuild.SystemdStageOptions{
 		EnabledServices:  enabledServices,
 		DisabledServices: disabledServices,
 		DefaultTarget:    target,
 	}
 }
 
-func (r *RHEL82) fsTabStageOptions(uefi bool) *pipeline.FSTabStageOptions {
-	options := pipeline.FSTabStageOptions{}
+func (r *RHEL82) fsTabStageOptions(uefi bool) *osbuild.FSTabStageOptions {
+	options := osbuild.FSTabStageOptions{}
 	options.AddFilesystem("0bd700f8-090f-4556-b797-b340297ea1bd", "xfs", "/", "defaults", 0, 0)
 	if uefi {
 		options.AddFilesystem("46BB-8120", "vfat", "/boot/efi", "umask=0077,shortname=winnt", 0, 2)
@@ -709,20 +753,20 @@ func (r *RHEL82) fsTabStageOptions(uefi bool) *pipeline.FSTabStageOptions {
 	return &options
 }
 
-func (r *RHEL82) grub2StageOptions(kernelOptions string, uefi bool) *pipeline.GRUB2StageOptions {
+func (r *RHEL82) grub2StageOptions(kernelOptions string, uefi bool) *osbuild.GRUB2StageOptions {
 	id, err := uuid.Parse("0bd700f8-090f-4556-b797-b340297ea1bd")
 	if err != nil {
 		panic("invalid UUID")
 	}
 
-	var uefiOptions *pipeline.GRUB2UEFI
+	var uefiOptions *osbuild.GRUB2UEFI
 	if uefi {
-		uefiOptions = &pipeline.GRUB2UEFI{
+		uefiOptions = &osbuild.GRUB2UEFI{
 			Vendor: "redhat",
 		}
 	}
 
-	return &pipeline.GRUB2StageOptions{
+	return &osbuild.GRUB2StageOptions{
 		RootFilesystemUUID: id,
 		KernelOptions:      kernelOptions,
 		Legacy:             !uefi,
@@ -730,28 +774,28 @@ func (r *RHEL82) grub2StageOptions(kernelOptions string, uefi bool) *pipeline.GR
 	}
 }
 
-func (r *RHEL82) selinuxStageOptions() *pipeline.SELinuxStageOptions {
-	return &pipeline.SELinuxStageOptions{
+func (r *RHEL82) selinuxStageOptions() *osbuild.SELinuxStageOptions {
+	return &osbuild.SELinuxStageOptions{
 		FileContexts: "etc/selinux/targeted/contexts/files/file_contexts",
 	}
 }
 
-func (r *RHEL82) qemuAssembler(format string, filename string, uefi bool, size uint64) *pipeline.Assembler {
-	var options pipeline.QEMUAssemblerOptions
+func (r *RHEL82) qemuAssembler(format string, filename string, uefi bool, size uint64) *osbuild.Assembler {
+	var options osbuild.QEMUAssemblerOptions
 	if uefi {
 		fstype := uuid.MustParse("C12A7328-F81F-11D2-BA4B-00A0C93EC93B")
-		options = pipeline.QEMUAssemblerOptions{
+		options = osbuild.QEMUAssemblerOptions{
 			Format:   format,
 			Filename: filename,
 			Size:     size,
 			PTUUID:   "8DFDFF87-C96E-EA48-A3A6-9408F1F6B1EF",
 			PTType:   "gpt",
-			Partitions: []pipeline.QEMUPartition{
+			Partitions: []osbuild.QEMUPartition{
 				{
 					Start: 2048,
 					Size:  972800,
 					Type:  &fstype,
-					Filesystem: pipeline.QEMUFilesystem{
+					Filesystem: osbuild.QEMUFilesystem{
 						Type:       "vfat",
 						UUID:       "46BB-8120",
 						Label:      "EFI System Partition",
@@ -760,7 +804,7 @@ func (r *RHEL82) qemuAssembler(format string, filename string, uefi bool, size u
 				},
 				{
 					Start: 976896,
-					Filesystem: pipeline.QEMUFilesystem{
+					Filesystem: osbuild.QEMUFilesystem{
 						Type:       "xfs",
 						UUID:       "0bd700f8-090f-4556-b797-b340297ea1bd",
 						Mountpoint: "/",
@@ -769,17 +813,17 @@ func (r *RHEL82) qemuAssembler(format string, filename string, uefi bool, size u
 			},
 		}
 	} else {
-		options = pipeline.QEMUAssemblerOptions{
+		options = osbuild.QEMUAssemblerOptions{
 			Format:   format,
 			Filename: filename,
 			Size:     size,
 			PTUUID:   "0x14fc63d2",
 			PTType:   "mbr",
-			Partitions: []pipeline.QEMUPartition{
+			Partitions: []osbuild.QEMUPartition{
 				{
 					Start:    2048,
 					Bootable: true,
-					Filesystem: pipeline.QEMUFilesystem{
+					Filesystem: osbuild.QEMUFilesystem{
 						Type:       "xfs",
 						UUID:       "0bd700f8-090f-4556-b797-b340297ea1bd",
 						Mountpoint: "/",
@@ -788,24 +832,24 @@ func (r *RHEL82) qemuAssembler(format string, filename string, uefi bool, size u
 			},
 		}
 	}
-	return pipeline.NewQEMUAssembler(&options)
+	return osbuild.NewQEMUAssembler(&options)
 }
 
-func (r *RHEL82) tarAssembler(filename, compression string) *pipeline.Assembler {
-	return pipeline.NewTarAssembler(
-		&pipeline.TarAssemblerOptions{
+func (r *RHEL82) tarAssembler(filename, compression string) *osbuild.Assembler {
+	return osbuild.NewTarAssembler(
+		&osbuild.TarAssemblerOptions{
 			Filename:    filename,
 			Compression: compression,
 		})
 }
 
-func (r *RHEL82) rawFSAssembler(filename string, size uint64) *pipeline.Assembler {
+func (r *RHEL82) rawFSAssembler(filename string, size uint64) *osbuild.Assembler {
 	id, err := uuid.Parse("0bd700f8-090f-4556-b797-b340297ea1bd")
 	if err != nil {
 		panic("invalid UUID")
 	}
-	return pipeline.NewRawFSAssembler(
-		&pipeline.RawFSAssemblerOptions{
+	return osbuild.NewRawFSAssembler(
+		&osbuild.RawFSAssemblerOptions{
 			Filename:           filename,
 			RootFilesystemUUDI: id,
 			Size:               size,
diff --git a/internal/jobqueue/job.go b/internal/jobqueue/job.go
index 03fe78fe3..d0c8cb93c 100644
--- a/internal/jobqueue/job.go
+++ b/internal/jobqueue/job.go
@@ -7,22 +7,24 @@ import (
 	"os"
 	"os/exec"
 
+	"github.com/osbuild/osbuild-composer/internal/compose"
+	"github.com/osbuild/osbuild-composer/internal/osbuild"
+
 	"github.com/google/uuid"
 
 	"github.com/osbuild/osbuild-composer/internal/common"
 	"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"
 )
 
 type Job struct {
-	ID           uuid.UUID          `json:"id"`
-	ImageBuildID int                `json:"image_build_id"`
-	Distro       string             `json:"distro"`
-	Pipeline     *pipeline.Pipeline `json:"pipeline"`
-	Targets      []*target.Target   `json:"targets"`
-	OutputType   string             `json:"output_type"`
+	ID           uuid.UUID         `json:"id"`
+	ImageBuildID int               `json:"image_build_id"`
+	Distro       string            `json:"distro"`
+	Pipeline     *osbuild.Pipeline `json:"pipeline"`
+	Targets      []*target.Target  `json:"targets"`
+	OutputType   string            `json:"output_type"`
 }
 
 type JobStatus struct {
@@ -51,7 +53,7 @@ func (job *Job) Run(uploader LocalTargetUploader) (*common.ComposeResult, error)
 		return nil, fmt.Errorf("unknown distro: %s", job.Distro)
 	}
 
-	build := pipeline.Build{
+	build := osbuild.Build{
 		Runner: d.Runner(),
 	}
 
diff --git a/internal/mocks/rpmmd/rpmmd_mock.go b/internal/mocks/rpmmd/rpmmd_mock.go
index d5d3162fb..3bc8fe40e 100644
--- a/internal/mocks/rpmmd/rpmmd_mock.go
+++ b/internal/mocks/rpmmd/rpmmd_mock.go
@@ -6,14 +6,14 @@ import (
 )
 
 type fetchPackageList struct {
-	ret rpmmd.PackageList
+	ret       rpmmd.PackageList
 	checksums map[string]string
-	err error
+	err       error
 }
 type depsolve struct {
-	ret []rpmmd.PackageSpec
+	ret       []rpmmd.PackageSpec
 	checksums map[string]string
-	err error
+	err       error
 }
 
 type Fixture struct {
@@ -30,10 +30,10 @@ func NewRPMMDMock(fixture Fixture) rpmmd.RPMMD {
 	return &rpmmdMock{Fixture: fixture}
 }
 
-func (r *rpmmdMock) FetchPackageList(repos []rpmmd.RepoConfig) (rpmmd.PackageList, map[string]string, error) {
+func (r *rpmmdMock) FetchPackageList(repos []rpmmd.RepoConfig, modulePlatformID string) (rpmmd.PackageList, map[string]string, error) {
 	return r.Fixture.fetchPackageList.ret, r.Fixture.fetchPackageList.checksums, r.Fixture.fetchPackageList.err
 }
 
-func (r *rpmmdMock) Depsolve(specs []string, repos []rpmmd.RepoConfig, clean bool) ([]rpmmd.PackageSpec, map[string]string, error) {
+func (r *rpmmdMock) Depsolve(specs, excludeSpecs []string, repos []rpmmd.RepoConfig, modulePlatformID string, clean bool) ([]rpmmd.PackageSpec, map[string]string, error) {
 	return r.Fixture.depsolve.ret, r.Fixture.fetchPackageList.checksums, r.Fixture.depsolve.err
 }
diff --git a/internal/pipeline/assembler.go b/internal/osbuild/assembler.go
similarity index 98%
rename from internal/pipeline/assembler.go
rename to internal/osbuild/assembler.go
index bbf5c2c7a..44e8324c7 100644
--- a/internal/pipeline/assembler.go
+++ b/internal/osbuild/assembler.go
@@ -1,4 +1,4 @@
-package pipeline
+package osbuild
 
 import (
 	"encoding/json"
diff --git a/internal/pipeline/chrony_stage.go b/internal/osbuild/chrony_stage.go
similarity index 93%
rename from internal/pipeline/chrony_stage.go
rename to internal/osbuild/chrony_stage.go
index bcbd334ef..1be51c2dc 100644
--- a/internal/pipeline/chrony_stage.go
+++ b/internal/osbuild/chrony_stage.go
@@ -1,4 +1,4 @@
-package pipeline
+package osbuild
 
 type ChronyStageOptions struct {
 	Timeservers []string `json:"timeservers"`
diff --git a/internal/pipeline/dnf_stage.go b/internal/osbuild/dnf_stage.go
similarity index 97%
rename from internal/pipeline/dnf_stage.go
rename to internal/osbuild/dnf_stage.go
index aadeb3d9a..d8a26ccd0 100644
--- a/internal/pipeline/dnf_stage.go
+++ b/internal/osbuild/dnf_stage.go
@@ -1,4 +1,4 @@
-package pipeline
+package osbuild
 
 // The DNFStageOptions describe the operations of the DNF stage.
 //
@@ -13,7 +13,7 @@ type DNFStageOptions struct {
 	ExcludedPackages []string         `json:"exclude_packages,omitempty"`
 	ReleaseVersion   string           `json:"releasever"`
 	BaseArchitecture string           `json:"basearch"`
-	ModulePlatformId string		  `json:"module_platform_id,omitempty"`
+	ModulePlatformId string           `json:"module_platform_id,omitempty"`
 }
 
 func (DNFStageOptions) isStageOptions() {}
diff --git a/internal/osbuild/files_source.go b/internal/osbuild/files_source.go
new file mode 100644
index 000000000..b3b946873
--- /dev/null
+++ b/internal/osbuild/files_source.go
@@ -0,0 +1,8 @@
+package osbuild
+
+// The FilesSourceOptions specifies a custom script to run in the image
+type FilesSource struct {
+	URLs map[string]string `json:"urls"`
+}
+
+func (FilesSource) isSource() {}
diff --git a/internal/pipeline/firewall_stage.go b/internal/osbuild/firewall_stage.go
similarity index 95%
rename from internal/pipeline/firewall_stage.go
rename to internal/osbuild/firewall_stage.go
index 0479f388c..fd74fb150 100644
--- a/internal/pipeline/firewall_stage.go
+++ b/internal/osbuild/firewall_stage.go
@@ -1,4 +1,4 @@
-package pipeline
+package osbuild
 
 type FirewallStageOptions struct {
 	Ports            []string `json:"ports,omitempty"`
diff --git a/internal/pipeline/fix_bls_stage.go b/internal/osbuild/fix_bls_stage.go
similarity index 97%
rename from internal/pipeline/fix_bls_stage.go
rename to internal/osbuild/fix_bls_stage.go
index 769514226..b96074c6b 100644
--- a/internal/pipeline/fix_bls_stage.go
+++ b/internal/osbuild/fix_bls_stage.go
@@ -1,4 +1,4 @@
-package pipeline
+package osbuild
 
 // A FixBLSStageOptions struct is empty, as the stage takes no options.
 //
diff --git a/internal/pipeline/fstab_stage.go b/internal/osbuild/fstab_stage.go
similarity index 98%
rename from internal/pipeline/fstab_stage.go
rename to internal/osbuild/fstab_stage.go
index 70abc468f..8c0d6fc1c 100644
--- a/internal/pipeline/fstab_stage.go
+++ b/internal/osbuild/fstab_stage.go
@@ -1,4 +1,4 @@
-package pipeline
+package osbuild
 
 // The FSTabStageOptions describe the content of the /etc/fstab file.
 //
diff --git a/internal/pipeline/groups_stage.go b/internal/osbuild/groups_stage.go
similarity index 95%
rename from internal/pipeline/groups_stage.go
rename to internal/osbuild/groups_stage.go
index 42f3724b1..0d900fbbb 100644
--- a/internal/pipeline/groups_stage.go
+++ b/internal/osbuild/groups_stage.go
@@ -1,4 +1,4 @@
-package pipeline
+package osbuild
 
 type GroupsStageOptions struct {
 	Groups map[string]GroupsStageOptionsGroup `json:"groups"`
diff --git a/internal/pipeline/grub2_stage.go b/internal/osbuild/grub2_stage.go
similarity index 98%
rename from internal/pipeline/grub2_stage.go
rename to internal/osbuild/grub2_stage.go
index 64fd69eee..5c3cbf981 100644
--- a/internal/pipeline/grub2_stage.go
+++ b/internal/osbuild/grub2_stage.go
@@ -1,4 +1,4 @@
-package pipeline
+package osbuild
 
 import "github.com/google/uuid"
 
diff --git a/internal/pipeline/hostname_stage.go b/internal/osbuild/hostname_stage.go
similarity index 93%
rename from internal/pipeline/hostname_stage.go
rename to internal/osbuild/hostname_stage.go
index 095474b31..71d7a44a4 100644
--- a/internal/pipeline/hostname_stage.go
+++ b/internal/osbuild/hostname_stage.go
@@ -1,4 +1,4 @@
-package pipeline
+package osbuild
 
 type HostnameStageOptions struct {
 	Hostname string `json:"hostname"`
diff --git a/internal/pipeline/keymap_stage.go b/internal/osbuild/keymap_stage.go
similarity index 93%
rename from internal/pipeline/keymap_stage.go
rename to internal/osbuild/keymap_stage.go
index 8c5e33c64..9fd4fb700 100644
--- a/internal/pipeline/keymap_stage.go
+++ b/internal/osbuild/keymap_stage.go
@@ -1,4 +1,4 @@
-package pipeline
+package osbuild
 
 type KeymapStageOptions struct {
 	Keymap string `json:"keymap"`
diff --git a/internal/pipeline/locale_stage.go b/internal/osbuild/locale_stage.go
similarity index 97%
rename from internal/pipeline/locale_stage.go
rename to internal/osbuild/locale_stage.go
index 411431e20..0454ef8a9 100644
--- a/internal/pipeline/locale_stage.go
+++ b/internal/osbuild/locale_stage.go
@@ -1,4 +1,4 @@
-package pipeline
+package osbuild
 
 // The LocaleStageOptions describes the image's locale.
 //
diff --git a/internal/pipeline/pipeline.go b/internal/osbuild/osbuild.go
similarity index 89%
rename from internal/pipeline/pipeline.go
rename to internal/osbuild/osbuild.go
index 7a838f79d..aa282c986 100644
--- a/internal/pipeline/pipeline.go
+++ b/internal/osbuild/osbuild.go
@@ -1,6 +1,6 @@
-// Package pipeline provides primitives for representing and (un)marshalling
-// OSBuild pipelines.
-package pipeline
+// Package osbuild provides primitives for representing and (un)marshalling
+// OSBuild types.
+package osbuild
 
 // A Pipeline represents an OSBuild pipeline
 type Pipeline struct {
@@ -25,7 +25,7 @@ type Build struct {
 func (p *Pipeline) SetBuild(pipeline *Pipeline, runner string) {
 	p.Build = &Build{
 		Pipeline: pipeline,
-		Runner: runner,
+		Runner:   runner,
 	}
 }
 
diff --git a/internal/pipeline/qemu_assembler.go b/internal/osbuild/qemu_assembler.go
similarity index 98%
rename from internal/pipeline/qemu_assembler.go
rename to internal/osbuild/qemu_assembler.go
index d478f31aa..22699e6cb 100644
--- a/internal/pipeline/qemu_assembler.go
+++ b/internal/osbuild/qemu_assembler.go
@@ -1,4 +1,4 @@
-package pipeline
+package osbuild
 
 import "github.com/google/uuid"
 
diff --git a/internal/pipeline/rawfs_assembler.go b/internal/osbuild/rawfs_assembler.go
similarity index 97%
rename from internal/pipeline/rawfs_assembler.go
rename to internal/osbuild/rawfs_assembler.go
index b6408ca4c..5bb0c0817 100644
--- a/internal/pipeline/rawfs_assembler.go
+++ b/internal/osbuild/rawfs_assembler.go
@@ -1,4 +1,4 @@
-package pipeline
+package osbuild
 
 import "github.com/google/uuid"
 
diff --git a/internal/osbuild/rpm_stage.go b/internal/osbuild/rpm_stage.go
new file mode 100644
index 000000000..37f8e54bf
--- /dev/null
+++ b/internal/osbuild/rpm_stage.go
@@ -0,0 +1,21 @@
+package osbuild
+
+// The RPMStageOptions describe the operations of the RPM stage.
+//
+// The RPM stage installs a given set of packages, identified by their
+// content hash. This ensures that given a set of RPM stage options,
+// the output is be reproducible, if the underlying tools are.
+type RPMStageOptions struct {
+	GPGKeys  []string `json:"gpgkeys,omitempty"`
+	Packages []string `json:"packages"`
+}
+
+func (RPMStageOptions) isStageOptions() {}
+
+// NewRPMStage creates a new RPM stage.
+func NewRPMStage(options *RPMStageOptions) *Stage {
+	return &Stage{
+		Name:    "org.osbuild.rpm",
+		Options: options,
+	}
+}
diff --git a/internal/pipeline/script_stage.go b/internal/osbuild/script_stage.go
similarity index 97%
rename from internal/pipeline/script_stage.go
rename to internal/osbuild/script_stage.go
index a6b194a07..4d2bc766e 100644
--- a/internal/pipeline/script_stage.go
+++ b/internal/osbuild/script_stage.go
@@ -1,4 +1,4 @@
-package pipeline
+package osbuild
 
 // The ScriptStageOptions specifies a custom script to run in the image
 type ScriptStageOptions struct {
diff --git a/internal/pipeline/selinux_stage.go b/internal/osbuild/selinux_stage.go
similarity index 97%
rename from internal/pipeline/selinux_stage.go
rename to internal/osbuild/selinux_stage.go
index 747169e30..80aa93b1f 100644
--- a/internal/pipeline/selinux_stage.go
+++ b/internal/osbuild/selinux_stage.go
@@ -1,4 +1,4 @@
-package pipeline
+package osbuild
 
 // The SELinuxStageOptions describe how to apply selinux labels.
 //
diff --git a/internal/osbuild/source.go b/internal/osbuild/source.go
new file mode 100644
index 000000000..ca763210a
--- /dev/null
+++ b/internal/osbuild/source.go
@@ -0,0 +1,43 @@
+package osbuild
+
+import (
+	"encoding/json"
+	"errors"
+)
+
+// A Sources map contains all the sources made available to an osbuild run
+type Sources map[string]Source
+
+// Source specifies the operations of a given source-type.
+type Source interface {
+	isSource()
+}
+
+type rawSources map[string]json.RawMessage
+
+// UnmarshalJSON unmarshals JSON into a Source object. Each type of source has
+// a custom unmarshaller for its options, selected based on the source name.
+func (sources *Sources) UnmarshalJSON(data []byte) error {
+	var rawSources rawSources
+	err := json.Unmarshal(data, &rawSources)
+	if err != nil {
+		return err
+	}
+	*sources = make(map[string]Source)
+	for name, rawSource := range rawSources {
+		var source Source
+		switch name {
+		case "org.osbuild.files":
+			source = new(FilesSource)
+		default:
+			return errors.New("unexpected suorce name" + name)
+		}
+		err = json.Unmarshal(rawSource, source)
+		if err != nil {
+			return err
+		}
+		(*sources)[name] = source
+	}
+
+	return nil
+}
diff --git a/internal/pipeline/stage.go b/internal/osbuild/stage.go
similarity index 96%
rename from internal/pipeline/stage.go
rename to internal/osbuild/stage.go
index 3f6e2c56d..571bbb488 100644
--- a/internal/pipeline/stage.go
+++ b/internal/osbuild/stage.go
@@ -1,4 +1,4 @@
-package pipeline
+package osbuild
 
 import (
 	"encoding/json"
@@ -62,6 +62,8 @@ func (stage *Stage) UnmarshalJSON(data []byte) error {
 		options = new(KeymapStageOptions)
 	case "org.osbuild.firewall":
 		options = new(FirewallStageOptions)
+	case "org.osbuild.rpm":
+		options = new(RPMStageOptions)
 	case "org.osbuild.systemd":
 		options = new(SystemdStageOptions)
 	case "org.osbuild.script":
diff --git a/internal/pipeline/systemd_stage.go b/internal/osbuild/systemd_stage.go
similarity index 96%
rename from internal/pipeline/systemd_stage.go
rename to internal/osbuild/systemd_stage.go
index cc4aeb57f..b0c6043ee 100644
--- a/internal/pipeline/systemd_stage.go
+++ b/internal/osbuild/systemd_stage.go
@@ -1,4 +1,4 @@
-package pipeline
+package osbuild
 
 type SystemdStageOptions struct {
 	EnabledServices  []string `json:"enabled_services,omitempty"`
diff --git a/internal/pipeline/tar_assembler.go b/internal/osbuild/tar_assembler.go
similarity index 98%
rename from internal/pipeline/tar_assembler.go
rename to internal/osbuild/tar_assembler.go
index 993572e0d..01f735af9 100644
--- a/internal/pipeline/tar_assembler.go
+++ b/internal/osbuild/tar_assembler.go
@@ -1,4 +1,4 @@
-package pipeline
+package osbuild
 
 // TarAssemblerOptions desrcibe how to assemble a tree into a tar ball.
 //
diff --git a/internal/pipeline/timezone_stage.go b/internal/osbuild/timezone_stage.go
similarity index 93%
rename from internal/pipeline/timezone_stage.go
rename to internal/osbuild/timezone_stage.go
index 08db9dc3d..130ac5f16 100644
--- a/internal/pipeline/timezone_stage.go
+++ b/internal/osbuild/timezone_stage.go
@@ -1,4 +1,4 @@
-package pipeline
+package osbuild
 
 type TimezoneStageOptions struct {
 	Zone string `json:"zone"`
diff --git a/internal/pipeline/users_stage.go b/internal/osbuild/users_stage.go
similarity index 97%
rename from internal/pipeline/users_stage.go
rename to internal/osbuild/users_stage.go
index f9072e8b0..dc1ac5fbe 100644
--- a/internal/pipeline/users_stage.go
+++ b/internal/osbuild/users_stage.go
@@ -1,4 +1,4 @@
-package pipeline
+package osbuild
 
 type UsersStageOptions struct {
 	Users map[string]UsersStageOptionsUser `json:"users"`
diff --git a/internal/rcm/api.go b/internal/rcm/api.go
index da77035aa..aecdf9c14 100644
--- a/internal/rcm/api.go
+++ b/internal/rcm/api.go
@@ -130,7 +130,7 @@ func (api *API) submit(writer http.ResponseWriter, request *http.Request, _ http
 	composeUUID := uuid.New()
 	// nil is used as an upload target, because LocalTarget is already used in the PushCompose function
 	// TODO: replace this with generalized version of push compose
-	err = api.store.PushCompose(composeUUID, &blueprint.Blueprint{}, make(map[string]string), composeRequest.Architectures[0], composeRequest.ImageTypes[0], 0, nil)
+	err = api.store.PushCompose(composeUUID, &blueprint.Blueprint{}, nil, nil, make(map[string]string), composeRequest.Architectures[0], composeRequest.ImageTypes[0], 0, nil)
 	if err != nil {
 		if api.logger != nil {
 			api.logger.Println("RCM API failed to push compose:", err)
diff --git a/internal/rpmmd/repository.go b/internal/rpmmd/repository.go
index 04f747666..5227a8ee9 100644
--- a/internal/rpmmd/repository.go
+++ b/internal/rpmmd/repository.go
@@ -62,11 +62,13 @@ func (pkg Package) ToPackageInfo() PackageInfo {
 }
 
 type PackageSpec struct {
-	Name    string `json:"name"`
-	Epoch   uint   `json:"epoch"`
-	Version string `json:"version,omitempty"`
-	Release string `json:"release,omitempty"`
-	Arch    string `json:"arch,omitempty"`
+	Name           string `json:"name"`
+	Epoch          uint   `json:"epoch"`
+	Version        string `json:"version,omitempty"`
+	Release        string `json:"release,omitempty"`
+	Arch           string `json:"arch,omitempty"`
+	RemoteLocation string `json:"remote_location,omitempty"`
+	Checksum       string `json:"checksum,omitempty"`
 }
 
 type PackageSource struct {
@@ -93,8 +95,8 @@ type PackageInfo struct {
 }
 
 type RPMMD interface {
-	FetchPackageList(repos []RepoConfig) (PackageList, map[string]string, error)
-	Depsolve(specs []string, repos []RepoConfig, clean bool) ([]PackageSpec, map[string]string, error)
+	FetchPackageList(repos []RepoConfig, modulePlatformID string) (PackageList, map[string]string, error)
+	Depsolve(specs, excludeSpecs []string, repos []RepoConfig, modulePlatformID string, clean bool) ([]PackageSpec, map[string]string, error)
 }
 
 type DNFError struct {
@@ -197,10 +199,11 @@ func NewRPMMD() RPMMD {
 	return &rpmmdImpl{}
 }
 
-func (*rpmmdImpl) FetchPackageList(repos []RepoConfig) (PackageList, map[string]string, error) {
+func (*rpmmdImpl) FetchPackageList(repos []RepoConfig, modulePlatformID string) (PackageList, map[string]string, error) {
 	var arguments = struct {
-		Repos []RepoConfig `json:"repos"`
-	}{repos}
+		Repos            []RepoConfig `json:"repos"`
+		ModulePlatformID string       `json:"module_platform_id"`
+	}{repos, modulePlatformID}
 	var reply struct {
 		Checksums map[string]string `json:"checksums"`
 		Packages  PackageList       `json:"packages"`
@@ -212,12 +215,14 @@ func (*rpmmdImpl) FetchPackageList(repos []RepoConfig) (PackageList, map[string]
 	return reply.Packages, reply.Checksums, err
 }
 
-func (*rpmmdImpl) Depsolve(specs []string, repos []RepoConfig, clean bool) ([]PackageSpec, map[string]string, error) {
+func (*rpmmdImpl) Depsolve(specs, excludeSpecs []string, repos []RepoConfig, modulePlatformID string, clean bool) ([]PackageSpec, map[string]string, error) {
 	var arguments = struct {
-		PackageSpecs []string     `json:"package-specs"`
-		Repos        []RepoConfig `json:"repos"`
-		Clean        bool         `json:"clean,omitempty"`
-	}{specs, repos, clean}
+		PackageSpecs     []string     `json:"package-specs"`
+		ExcludSpecs      []string     `json:"exclude-specs"`
+		Repos            []RepoConfig `json:"repos"`
+		ModulePlatformID string       `json:"module_platform_id"`
+		Clean            bool         `json:"clean,omitempty"`
+	}{specs, excludeSpecs, repos, modulePlatformID, clean}
 	var reply struct {
 		Checksums    map[string]string `json:"checksums"`
 		Dependencies []PackageSpec     `json:"dependencies"`
@@ -278,7 +283,7 @@ func (packages PackageList) ToPackageInfos() []PackageInfo {
 	return results
 }
 
-func (pkg *PackageInfo) FillDependencies(rpmmd RPMMD, repos []RepoConfig) (err error) {
-	pkg.Dependencies, _, err = rpmmd.Depsolve([]string{pkg.Name}, repos, false)
+func (pkg *PackageInfo) FillDependencies(rpmmd RPMMD, repos []RepoConfig, modulePlatformID string) (err error) {
+	pkg.Dependencies, _, err = rpmmd.Depsolve([]string{pkg.Name}, nil, repos, modulePlatformID, false)
 	return
 }
diff --git a/internal/store/store.go b/internal/store/store.go
index 4ef5b67fa..db157348e 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -10,7 +10,6 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"github.com/osbuild/osbuild-composer/internal/compose"
 	"io"
 	"io/ioutil"
 	"log"
@@ -22,10 +21,12 @@ import (
 	"sync"
 	"time"
 
+	"github.com/osbuild/osbuild-composer/internal/compose"
+	"github.com/osbuild/osbuild-composer/internal/osbuild"
+
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/common"
 	"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"
 
@@ -54,7 +55,7 @@ type Job struct {
 	ComposeID    uuid.UUID
 	ImageBuildID int
 	Distro       string
-	Pipeline     *pipeline.Pipeline
+	Pipeline     *osbuild.Pipeline
 	Targets      []*target.Target
 	ImageType    string
 }
@@ -505,7 +506,7 @@ func (s *Store) getImageBuildDirectory(composeID uuid.UUID, imageBuildID int) st
 	return fmt.Sprintf("%s/%d", s.getComposeDirectory(composeID), imageBuildID)
 }
 
-func (s *Store) PushCompose(composeID uuid.UUID, bp *blueprint.Blueprint, checksums map[string]string, arch, composeType string, size uint64, uploadTarget *target.Target) error {
+func (s *Store) PushCompose(composeID uuid.UUID, bp *blueprint.Blueprint, packages, buildPackages []rpmmd.PackageSpec, checksums map[string]string, arch, composeType string, size uint64, uploadTarget *target.Target) error {
 	targets := []*target.Target{}
 
 	// Compatibility layer for image types in Weldr API v0
@@ -538,7 +539,7 @@ func (s *Store) PushCompose(composeID uuid.UUID, bp *blueprint.Blueprint, checks
 		repos = append(repos, source.RepoConfig())
 	}
 
-	pipelineStruct, err := s.distro.Pipeline(bp, repos, checksums, arch, composeType, size)
+	pipelineStruct, err := s.distro.Pipeline(bp, repos, packages, buildPackages, checksums, arch, composeType, size)
 	if err != nil {
 		return err
 	}
@@ -607,7 +608,7 @@ func (s *Store) PushComposeRequest(request common.ComposeRequest) error {
 		if !exists {
 			panic("fatal error, image type should exist but it does not")
 		}
-		pipelineStruct, err := distroStruct.Pipeline(&request.Blueprint, request.Repositories, nil, arch, imgTypeCompatStr, 0)
+		pipelineStruct, err := distroStruct.Pipeline(&request.Blueprint, request.Repositories, nil, nil, nil, arch, imgTypeCompatStr, 0)
 		if err != nil {
 			return err
 		}
diff --git a/internal/weldr/api.go b/internal/weldr/api.go
index 80287a9f5..cd1b09179 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -615,7 +615,7 @@ func (api *API) modulesInfoHandler(writer http.ResponseWriter, request *http.Req
 
 	if modulesRequested {
 		for i, _ := range packageInfos {
-			err := packageInfos[i].FillDependencies(api.rpmmd, api.distro.Repositories(api.arch))
+			err := packageInfos[i].FillDependencies(api.rpmmd, api.distro.Repositories(api.arch), api.distro.ModulePlatformID())
 			if err != nil {
 				errors := responseError{
 					ID:  errorId,
@@ -645,7 +645,7 @@ func (api *API) projectsDepsolveHandler(writer http.ResponseWriter, request *htt
 
 	names := strings.Split(params.ByName("projects"), ",")
 
-	packages, _, err := api.rpmmd.Depsolve(names, api.distro.Repositories(api.arch), false)
+	packages, _, err := api.rpmmd.Depsolve(names, nil, api.distro.Repositories(api.arch), api.distro.ModulePlatformID(), false)
 
 	if err != nil {
 		errors := responseError{
@@ -827,7 +827,7 @@ func (api *API) blueprintsDepsolveHandler(writer http.ResponseWriter, request *h
 			return
 		}
 
-		dependencies, _, err := api.depsolveBlueprint(blueprint, false)
+		dependencies, _, _, err := api.depsolveBlueprint(blueprint, "", api.arch, false)
 
 		if err != nil {
 			errors := responseError{
@@ -889,7 +889,7 @@ func (api *API) blueprintsFreezeHandler(writer http.ResponseWriter, request *htt
 			break
 		}
 
-		dependencies, _, err := api.depsolveBlueprint(blueprint, false)
+		dependencies, _, _, err := api.depsolveBlueprint(blueprint, "", api.arch, false)
 		if err != nil {
 			rerr := responseError{
 				ID:  "BlueprintsError",
@@ -1263,7 +1263,7 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 	}
 
 	if bp != nil {
-		_, checksums, err := api.depsolveBlueprint(bp, true)
+		packages, buildPackages, checksums, err := api.depsolveBlueprint(bp, cr.ComposeType, api.arch, true)
 		if err != nil {
 			errors := responseError{
 				ID:  "DepsolveError",
@@ -1273,7 +1273,7 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 			return
 		}
 
-		err = api.store.PushCompose(reply.BuildID, bp, checksums, api.arch, cr.ComposeType, size, uploadTarget)
+		err = api.store.PushCompose(reply.BuildID, bp, packages, buildPackages, checksums, api.arch, cr.ComposeType, size, uploadTarget)
 
 		// TODO: we should probably do some kind of blueprint validation in future
 		// for now, let's just 500 and bail out
@@ -1792,11 +1792,18 @@ func (api *API) fetchPackageList() (rpmmd.PackageList, error) {
 		repos = append(repos, source.RepoConfig())
 	}
 
-	packages, _, err := api.rpmmd.FetchPackageList(repos)
+	packages, _, err := api.rpmmd.FetchPackageList(repos, api.distro.ModulePlatformID())
 	return packages, err
 }
 
-func (api *API) depsolveBlueprint(bp *blueprint.Blueprint, clean bool) ([]rpmmd.PackageSpec, map[string]string, error) {
+func (api *API) depsolveBlueprint(bp *blueprint.Blueprint, outputType, arch string, clean bool) ([]rpmmd.PackageSpec, []rpmmd.PackageSpec, map[string]string, error) {
+	var repos []rpmmd.RepoConfig
+	for _, repo := range api.distro.Repositories(api.arch) {
+		repos = append(repos, repo)
+	}
+	for _, source := range api.store.GetAllSources() {
+		repos = append(repos, source.RepoConfig())
+	}
 	specs := make([]string, len(bp.Packages))
 	for i, pkg := range bp.Packages {
 		specs[i] = pkg.Name
@@ -1808,16 +1815,33 @@ func (api *API) depsolveBlueprint(bp *blueprint.Blueprint, clean bool) ([]rpmmd.
 			specs[i] += "-*-*.*"
 		}
 	}
+	excludeSpecs := []string{}
+	if outputType != "" {
+		// When the output type is known, include the base packages in the depsolve
+		// transaction.
+		packages, excludePackages, err := api.distro.BasePackages(outputType, arch)
+		if err != nil {
+			return nil, nil, nil, err
+		}
+		specs = append(specs, packages...)
+		excludeSpecs = append(excludePackages, excludeSpecs...)
+	}
 
-	var repos []rpmmd.RepoConfig
-	for _, repo := range api.distro.Repositories(api.arch) {
-		repos = append(repos, repo)
+	packages, checksums, err := api.rpmmd.Depsolve(specs, excludeSpecs, repos, api.distro.ModulePlatformID(), clean)
+	if err != nil {
+		return nil, nil, nil, err
 	}
-	for _, source := range api.store.GetAllSources() {
-		repos = append(repos, source.RepoConfig())
+
+	buildPackages := []rpmmd.PackageSpec{}
+	if outputType != "" {
+		buildSpecs, err := api.distro.BuildPackages(arch)
+		if err != nil {
+			return nil, nil, nil, err
+		}
+		buildPackages, _, err = api.rpmmd.Depsolve(buildSpecs, nil, repos, api.distro.ModulePlatformID(), false)
 	}
 
-	return api.rpmmd.Depsolve(specs, repos, clean)
+	return packages, buildPackages, checksums, err
 }
 
 func (api *API) uploadsScheduleHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
diff --git a/repositories/rhel-8.2.json b/repositories/rhel-8.2.json
index 5df7aa943..4e6f383cd 100644
--- a/repositories/rhel-8.2.json
+++ b/repositories/rhel-8.2.json
@@ -3,24 +3,24 @@
         {
             "id": "baseos",
             "name": "BaseOS",
-            "BaseURL": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/aarch64/os"
+            "BaseURL": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/aarch64/os"
         },
         {
             "id": "appstream",
             "name": "AppStream",
-            "BaseURL": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/aarch64/os"
+            "BaseURL": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/aarch64/os"
         }
     ],
     "x86_64": [
         {
             "id": "baseos",
             "name": "BaseOS",
-            "BaseURL": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os"
+            "BaseURL": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os"
         },
         {
             "id": "appstream",
             "name": "AppStream",
-            "BaseURL": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os"
+            "BaseURL": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os"
         }
     ]
 }


Test patch:
diff --git a/internal/distro/distro_test.go b/internal/distro/distro_test.go
index ba4f578fc..26ae738a0 100644
--- a/internal/distro/distro_test.go
+++ b/internal/distro/distro_test.go
@@ -8,7 +8,7 @@ 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/osbuild"
 )
 
 func TestDistro_Pipeline(t *testing.T) {
@@ -26,8 +26,8 @@ func TestDistro_Pipeline(t *testing.T) {
 			Blueprint    *blueprint.Blueprint `json:"blueprint"`
 		}
 		var tt struct {
-			Compose  *compose           `json:"compose"`
-			Pipeline *pipeline.Pipeline `json:"pipeline,omitempty"`
+			Compose  *compose          `json:"compose"`
+			Pipeline *osbuild.Pipeline `json:"pipeline,omitempty"`
 		}
 		file, err := ioutil.ReadFile(pipelinePath + fileInfo.Name())
 		if err != nil {
@@ -49,7 +49,7 @@ func TestDistro_Pipeline(t *testing.T) {
 				return
 			}
 			size := d.GetSizeForOutputType(tt.Compose.OutputFormat, 0)
-			got, err := d.Pipeline(tt.Compose.Blueprint, nil, tt.Compose.Checksums, tt.Compose.Arch, tt.Compose.OutputFormat, size)
+			got, err := d.Pipeline(tt.Compose.Blueprint, nil, nil, nil, tt.Compose.Checksums, tt.Compose.Arch, tt.Compose.OutputFormat, size)
 			if (err != nil) != (tt.Pipeline == nil) {
 				t.Errorf("distro.Pipeline() error = %v", err)
 				return
diff --git a/internal/distro/fedoratest/distro.go b/internal/distro/fedoratest/distro.go
index 43e3a01f9..7ab90d28a 100644
--- a/internal/distro/fedoratest/distro.go
+++ b/internal/distro/fedoratest/distro.go
@@ -2,14 +2,17 @@ package fedoratest
 
 import (
 	"errors"
+
 	"github.com/osbuild/osbuild-composer/internal/blueprint"
 	"github.com/osbuild/osbuild-composer/internal/common"
-	"github.com/osbuild/osbuild-composer/internal/pipeline"
+	"github.com/osbuild/osbuild-composer/internal/osbuild"
 	"github.com/osbuild/osbuild-composer/internal/rpmmd"
 )
 
 type FedoraTestDistro struct{}
 
+const ModulePlatformID = "platform:f30"
+
 func New() *FedoraTestDistro {
 	return &FedoraTestDistro{}
 }
@@ -22,6 +25,10 @@ func (d *FedoraTestDistro) Distribution() common.Distribution {
 	return common.Fedora30
 }
 
+func (d *FedoraTestDistro) ModulePlatformID() string {
+	return ModulePlatformID
+}
+
 func (d *FedoraTestDistro) Repositories(arch string) []rpmmd.RepoConfig {
 	return []rpmmd.RepoConfig{
 		{
@@ -48,15 +55,26 @@ func (r *FedoraTestDistro) GetSizeForOutputType(outputFormat string, size uint64
 	return 0
 }
 
-func (d *FedoraTestDistro) Pipeline(b *blueprint.Blueprint, additionalRepos []rpmmd.RepoConfig, checksums map[string]string, outputArch, outputFormat string, size uint64) (*pipeline.Pipeline, error) {
+func (d *FedoraTestDistro) BasePackages(outputFormat string, outputArchitecture string) ([]string, []string, error) {
+	return nil, nil, nil
+}
+
+func (d *FedoraTestDistro) BuildPackages(outputArchitecture string) ([]string, error) {
+	return nil, nil
+}
+
+func (d *FedoraTestDistro) Pipeline(b *blueprint.Blueprint, additionalRepos []rpmmd.RepoConfig, buildPackages, basePackages []rpmmd.PackageSpec, checksums map[string]string, outputArch, outputFormat string, size uint64) (*osbuild.Pipeline, error) {
 	if outputFormat == "qcow2" && outputArch == "x86_64" {
-		return &pipeline.Pipeline{}, nil
+		return &osbuild.Pipeline{}, nil
 	} else {
 		return nil, errors.New("invalid output format or arch: " + outputFormat + " @ " + outputArch)
 	}
 }
 
+func (r *FedoraTestDistro) Sources(packages []rpmmd.PackageSpec) *osbuild.Sources {
+	return &osbuild.Sources{}
+}
+
 func (d *FedoraTestDistro) Runner() string {
 	return "org.osbuild.test"
 }
-
diff --git a/internal/distro/test/distro.go b/internal/distro/test/distro.go
new file mode 100644
index 000000000..92fda8bde
--- /dev/null
+++ b/internal/distro/test/distro.go
@@ -0,0 +1,72 @@
+package test
+
+import (
+	"errors"
+
+	"github.com/osbuild/osbuild-composer/internal/blueprint"
+	"github.com/osbuild/osbuild-composer/internal/osbuild"
+	"github.com/osbuild/osbuild-composer/internal/rpmmd"
+)
+
+type TestDistro struct{}
+
+const Name = "test-distro"
+const ModulePlatformID = "platform:test"
+
+func New() *TestDistro {
+	return &TestDistro{}
+}
+
+func (d *TestDistro) Name() string {
+	return Name
+}
+
+func (d *TestDistro) ModulePlatformID() string {
+	return ModulePlatformID
+}
+
+func (d *TestDistro) Repositories(arch string) []rpmmd.RepoConfig {
+	return []rpmmd.RepoConfig{
+		{
+			Id:      "test-id",
+			Name:    "Test Name",
+			BaseURL: "http://example.com/test/os/" + arch,
+		},
+	}
+}
+
+func (d *TestDistro) ListOutputFormats() []string {
+	return []string{"test_format"}
+}
+
+func (d *TestDistro) FilenameFromType(outputFormat string) (string, string, error) {
+	if outputFormat == "test_format" {
+		return "test.img", "application/x-test", nil
+	}
+
+	return "", "", errors.New("invalid output format: " + outputFormat)
+}
+
+func (d *TestDistro) BasePackages(outputFormat, outputArchitecture string) ([]string, []string, error) {
+	return nil, nil, nil
+}
+
+func (d *TestDistro) BuildPackages(outputArchitecture string) ([]string, error) {
+	return nil, nil
+}
+
+func (d *TestDistro) Pipeline(b *blueprint.Blueprint, additionalRepos []rpmmd.RepoConfig, packageSpecs, buildPackageSpecs []rpmmd.PackageSpec, checksums map[string]string, outputArch, outputFormat string, size uint64) (*osbuild.Pipeline, error) {
+	if outputFormat == "test_output" && outputArch == "test_arch" {
+		return &osbuild.Pipeline{}, nil
+	}
+
+	return nil, errors.New("invalid output format or arch: " + outputFormat + " @ " + outputArch)
+}
+
+func (d *TestDistro) Sources(packages []rpmmd.PackageSpec) *osbuild.Sources {
+	return &osbuild.Sources{}
+}
+
+func (d *TestDistro) Runner() string {
+	return "org.osbuild.test"
+}
diff --git a/internal/jobqueue/api_test.go b/internal/jobqueue/api_test.go
index 39787be11..8fc3ed205 100644
--- a/internal/jobqueue/api_test.go
+++ b/internal/jobqueue/api_test.go
@@ -48,7 +48,7 @@ func TestCreate(t *testing.T) {
 	store := store.New(nil, distroStruct, *registry)
 	api := jobqueue.New(nil, store)
 
-	err := store.PushCompose(id, &blueprint.Blueprint{}, 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)
 	}
@@ -65,7 +65,7 @@ func testUpdateTransition(t *testing.T, from, to string, expectedStatus int, exp
 	api := jobqueue.New(nil, store)
 
 	if from != "VOID" {
-		err := store.PushCompose(id, &blueprint.Blueprint{}, map[string]string{"test": "test:foo"}, "x86_64", "qcow2", 0, nil)
+		err := store.PushCompose(id, &blueprint.Blueprint{}, nil, nil, map[string]string{"test": "test:foo"}, "x86_64", "qcow2", 0, nil)
 		if err != nil {
 			t.Fatalf("error pushing compose: %v", err)
 		}
diff --git a/internal/osbuild/source_test.go b/internal/osbuild/source_test.go
new file mode 100644
index 000000000..f24b434cb
--- /dev/null
+++ b/internal/osbuild/source_test.go
@@ -0,0 +1,97 @@
+package osbuild
+
+import (
+	"bytes"
+	"encoding/json"
+	"reflect"
+	"testing"
+)
+
+func TestSource_UnmarshalJSON(t *testing.T) {
+	type fields struct {
+		Name   string
+		Source Source
+	}
+	type args struct {
+		data []byte
+	}
+	tests := []struct {
+		name    string
+		fields  fields
+		args    args
+		wantErr bool
+	}{
+		{
+			name: "invalid json",
+			args: args{
+				data: []byte(`{"name":"org.osbuild.foo","options":{"bar":null}`),
+			},
+			wantErr: true,
+		},
+		{
+			name: "unknown source",
+			args: args{
+				data: []byte(`{"name":"org.osbuild.foo","options":{"bar":null}}`),
+			},
+			wantErr: true,
+		},
+		{
+			name: "missing options",
+			args: args{
+				data: []byte(`{"name":"org.osbuild.files"}`),
+			},
+			wantErr: true,
+		},
+		{
+			name: "missing name",
+			args: args{
+				data: []byte(`{"foo":null,"options":{"bar":null}}`),
+			},
+			wantErr: true,
+		},
+		{
+			name: "files-empty",
+			fields: fields{
+				Name:   "org.osbuild.files",
+				Source: &FilesSource{URLs: map[string]string{}},
+			},
+			args: args{
+				data: []byte(`{"org.osbuild.files":{"urls":{}}}`),
+			},
+		},
+		{
+			name: "files",
+			fields: fields{
+				Name:   "org.osbuild.files",
+				Source: &FilesSource{URLs: map[string]string{"checksum1": "url1", "checksum2": "url2"}},
+			},
+			args: args{
+				data: []byte(`{"org.osbuild.files":{"urls":{"checksum1":"url1","checksum2":"url2"}}}`),
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			sources := &Sources{
+				tt.fields.Name: tt.fields.Source,
+			}
+			var gotSources Sources
+			if err := gotSources.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
+				t.Errorf("Sources.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
+			}
+			if tt.wantErr {
+				return
+			}
+			gotBytes, err := json.Marshal(sources)
+			if err != nil {
+				t.Errorf("Could not marshal source: %v", err)
+			}
+			if bytes.Compare(gotBytes, tt.args.data) != 0 {
+				t.Errorf("Expected '%v', got '%v'", string(tt.args.data), string(gotBytes))
+			}
+			if !reflect.DeepEqual(&gotSources, sources) {
+				t.Errorf("got '%v', expected '%v'", &gotSources, sources)
+			}
+		})
+	}
+}
diff --git a/internal/pipeline/stage_test.go b/internal/osbuild/stage_test.go
similarity index 88%
rename from internal/pipeline/stage_test.go
rename to internal/osbuild/stage_test.go
index 33423dd0b..e8be19c3d 100644
--- a/internal/pipeline/stage_test.go
+++ b/internal/osbuild/stage_test.go
@@ -1,4 +1,4 @@
-package pipeline
+package osbuild
 
 import (
 	"bytes"
@@ -10,7 +10,7 @@ import (
 )
 
 func TestStage_UnmarshalJSON(t *testing.T) {
-	null_uuid := uuid.MustParse("00000000-0000-0000-0000-000000000000")
+	nullUUID := uuid.MustParse("00000000-0000-0000-0000-000000000000")
 	type fields struct {
 		Name    string
 		Options StageOptions
@@ -117,7 +117,7 @@ func TestStage_UnmarshalJSON(t *testing.T) {
 			fields: fields{
 				Name: "org.osbuild.grub2",
 				Options: &GRUB2StageOptions{
-					RootFilesystemUUID: null_uuid,
+					RootFilesystemUUID: nullUUID,
 				},
 			},
 			args: args{
@@ -129,7 +129,7 @@ func TestStage_UnmarshalJSON(t *testing.T) {
 			fields: fields{
 				Name: "org.osbuild.grub2",
 				Options: &GRUB2StageOptions{
-					RootFilesystemUUID: null_uuid,
+					RootFilesystemUUID: nullUUID,
 					UEFI: &GRUB2UEFI{
 						Vendor: "vendor",
 					},
@@ -144,8 +144,8 @@ func TestStage_UnmarshalJSON(t *testing.T) {
 			fields: fields{
 				Name: "org.osbuild.grub2",
 				Options: &GRUB2StageOptions{
-					RootFilesystemUUID: null_uuid,
-					BootFilesystemUUID: &null_uuid,
+					RootFilesystemUUID: nullUUID,
+					BootFilesystemUUID: &nullUUID,
 				},
 			},
 			args: args{
@@ -182,6 +182,29 @@ func TestStage_UnmarshalJSON(t *testing.T) {
 				data: []byte(`{"name":"org.osbuild.locale","options":{"language":""}}`),
 			},
 		},
+		{
+			name: "rpm-empty",
+			fields: fields{
+				Name:    "org.osbuild.rpm",
+				Options: &RPMStageOptions{},
+			},
+			args: args{
+				data: []byte(`{"name":"org.osbuild.rpm","options":{"packages":null}}`),
+			},
+		},
+		{
+			name: "rpm",
+			fields: fields{
+				Name: "org.osbuild.rpm",
+				Options: &RPMStageOptions{
+					GPGKeys:  []string{"key1", "key2"},
+					Packages: []string{"checksum1", "checksum2"},
+				},
+			},
+			args: args{
+				data: []byte(`{"name":"org.osbuild.rpm","options":{"gpgkeys":["key1","key2"],"packages":["checksum1","checksum2"]}}`),
+			},
+		},
 		{
 			name: "script",
 			fields: fields{
diff --git a/test/cases/rhel82_ami.json b/test/cases/rhel82_ami.json
index c37cb88fc..b6c7ec296 100644
--- a/test/cases/rhel82_ami.json
+++ b/test/cases/rhel82_ami.json
@@ -22,11 +22,11 @@
             "options": {
               "repos": [
                 {
-                  "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
+                  "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
                   "checksum": "sha256:3ce6f4ffa7d05b81fec2ba5a1c4917b351231c3433f2f5eed86ed5f8f97168a1"
                 },
                 {
-                  "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
+                  "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
                   "checksum": "sha256:662e1dc0e465dd5e8efec9f42ef24ac9fd8418294254c3c98ea0342d6e903aed"
                 }
               ],
@@ -59,11 +59,11 @@
         "options": {
           "repos": [
             {
-              "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
+              "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
               "checksum": "sha256:3ce6f4ffa7d05b81fec2ba5a1c4917b351231c3433f2f5eed86ed5f8f97168a1"
             },
             {
-              "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
+              "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
               "checksum": "sha256:662e1dc0e465dd5e8efec9f42ef24ac9fd8418294254c3c98ea0342d6e903aed"
             }
           ],
diff --git a/test/cases/rhel82_ext4_filesystem.json b/test/cases/rhel82_ext4_filesystem.json
index fa93db76b..bb8e99fde 100644
--- a/test/cases/rhel82_ext4_filesystem.json
+++ b/test/cases/rhel82_ext4_filesystem.json
@@ -28,11 +28,11 @@
             "options": {
               "repos": [
                 {
-                  "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
+                  "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
                   "checksum": "sha256:3ce6f4ffa7d05b81fec2ba5a1c4917b351231c3433f2f5eed86ed5f8f97168a1"
                 },
                 {
-                  "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
+                  "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
                   "checksum": "sha256:662e1dc0e465dd5e8efec9f42ef24ac9fd8418294254c3c98ea0342d6e903aed"
                 }
               ],
@@ -65,11 +65,11 @@
         "options": {
           "repos": [
             {
-              "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
+              "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
               "checksum": "sha256:3ce6f4ffa7d05b81fec2ba5a1c4917b351231c3433f2f5eed86ed5f8f97168a1"
             },
             {
-              "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
+              "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
               "checksum": "sha256:662e1dc0e465dd5e8efec9f42ef24ac9fd8418294254c3c98ea0342d6e903aed"
             }
           ],
diff --git a/test/cases/rhel82_groups.json b/test/cases/rhel82_groups.json
index 7da3cfc1c..6a15b7d36 100644
--- a/test/cases/rhel82_groups.json
+++ b/test/cases/rhel82_groups.json
@@ -33,11 +33,11 @@
             "options": {
               "repos": [
                 {
-                  "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
+                  "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
                   "checksum": "sha256:3ce6f4ffa7d05b81fec2ba5a1c4917b351231c3433f2f5eed86ed5f8f97168a1"
                 },
                 {
-                  "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
+                  "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
                   "checksum": "sha256:662e1dc0e465dd5e8efec9f42ef24ac9fd8418294254c3c98ea0342d6e903aed"
                 }
               ],
@@ -70,11 +70,11 @@
         "options": {
           "repos": [
             {
-              "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
+              "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
               "checksum": "sha256:3ce6f4ffa7d05b81fec2ba5a1c4917b351231c3433f2f5eed86ed5f8f97168a1"
             },
             {
-              "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
+              "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
               "checksum": "sha256:662e1dc0e465dd5e8efec9f42ef24ac9fd8418294254c3c98ea0342d6e903aed"
             }
           ],
diff --git a/test/cases/rhel82_openstack.json b/test/cases/rhel82_openstack.json
index 31acfb21d..171299c5e 100644
--- a/test/cases/rhel82_openstack.json
+++ b/test/cases/rhel82_openstack.json
@@ -22,11 +22,11 @@
             "options": {
               "repos": [
                 {
-                  "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
+                  "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
                   "checksum": "sha256:3ce6f4ffa7d05b81fec2ba5a1c4917b351231c3433f2f5eed86ed5f8f97168a1"
                 },
                 {
-                  "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
+                  "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
                   "checksum": "sha256:662e1dc0e465dd5e8efec9f42ef24ac9fd8418294254c3c98ea0342d6e903aed"
                 }
               ],
@@ -59,11 +59,11 @@
         "options": {
           "repos": [
             {
-              "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
+              "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
               "checksum": "sha256:3ce6f4ffa7d05b81fec2ba5a1c4917b351231c3433f2f5eed86ed5f8f97168a1"
             },
             {
-              "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
+              "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
               "checksum": "sha256:662e1dc0e465dd5e8efec9f42ef24ac9fd8418294254c3c98ea0342d6e903aed"
             }
           ],
diff --git a/test/cases/rhel82_partitioned_disk.json b/test/cases/rhel82_partitioned_disk.json
index 59ede4d56..87d8a7022 100644
--- a/test/cases/rhel82_partitioned_disk.json
+++ b/test/cases/rhel82_partitioned_disk.json
@@ -28,11 +28,11 @@
             "options": {
               "repos": [
                 {
-                  "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
+                  "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
                   "checksum": "sha256:3ce6f4ffa7d05b81fec2ba5a1c4917b351231c3433f2f5eed86ed5f8f97168a1"
                 },
                 {
-                  "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
+                  "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
                   "checksum": "sha256:662e1dc0e465dd5e8efec9f42ef24ac9fd8418294254c3c98ea0342d6e903aed"
                 }
               ],
@@ -65,11 +65,11 @@
         "options": {
           "repos": [
             {
-              "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
+              "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
               "checksum": "sha256:3ce6f4ffa7d05b81fec2ba5a1c4917b351231c3433f2f5eed86ed5f8f97168a1"
             },
             {
-              "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
+              "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
               "checksum": "sha256:662e1dc0e465dd5e8efec9f42ef24ac9fd8418294254c3c98ea0342d6e903aed"
             }
           ],
diff --git a/test/cases/rhel82_qcow2.json b/test/cases/rhel82_qcow2.json
index efd6b1c82..2cd69f598 100644
--- a/test/cases/rhel82_qcow2.json
+++ b/test/cases/rhel82_qcow2.json
@@ -22,11 +22,11 @@
             "options": {
               "repos": [
                 {
-                  "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
+                  "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
                   "checksum": "sha256:3ce6f4ffa7d05b81fec2ba5a1c4917b351231c3433f2f5eed86ed5f8f97168a1"
                 },
                 {
-                  "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
+                  "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
                   "checksum": "sha256:662e1dc0e465dd5e8efec9f42ef24ac9fd8418294254c3c98ea0342d6e903aed"
                 }
               ],
@@ -59,11 +59,11 @@
         "options": {
           "repos": [
             {
-              "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
+              "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
               "checksum": "sha256:3ce6f4ffa7d05b81fec2ba5a1c4917b351231c3433f2f5eed86ed5f8f97168a1"
             },
             {
-              "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
+              "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
               "checksum": "sha256:662e1dc0e465dd5e8efec9f42ef24ac9fd8418294254c3c98ea0342d6e903aed"
             }
           ],
diff --git a/test/cases/rhel82_tar.json b/test/cases/rhel82_tar.json
index e4de26071..b28819c93 100644
--- a/test/cases/rhel82_tar.json
+++ b/test/cases/rhel82_tar.json
@@ -28,11 +28,11 @@
             "options": {
               "repos": [
                 {
-                  "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
+                  "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
                   "checksum": "sha256:3ce6f4ffa7d05b81fec2ba5a1c4917b351231c3433f2f5eed86ed5f8f97168a1"
                 },
                 {
-                  "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
+                  "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
                   "checksum": "sha256:662e1dc0e465dd5e8efec9f42ef24ac9fd8418294254c3c98ea0342d6e903aed"
                 }
               ],
@@ -65,11 +65,11 @@
         "options": {
           "repos": [
             {
-              "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
+              "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
               "checksum": "sha256:3ce6f4ffa7d05b81fec2ba5a1c4917b351231c3433f2f5eed86ed5f8f97168a1"
             },
             {
-              "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
+              "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
               "checksum": "sha256:662e1dc0e465dd5e8efec9f42ef24ac9fd8418294254c3c98ea0342d6e903aed"
             }
           ],
diff --git a/test/cases/rhel82_vhd.json b/test/cases/rhel82_vhd.json
index c98c132bd..3426d4b49 100644
--- a/test/cases/rhel82_vhd.json
+++ b/test/cases/rhel82_vhd.json
@@ -22,11 +22,11 @@
             "options": {
               "repos": [
                 {
-                  "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
+                  "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
                   "checksum": "sha256:3ce6f4ffa7d05b81fec2ba5a1c4917b351231c3433f2f5eed86ed5f8f97168a1"
                 },
                 {
-                  "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
+                  "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
                   "checksum": "sha256:662e1dc0e465dd5e8efec9f42ef24ac9fd8418294254c3c98ea0342d6e903aed"
                 }
               ],
@@ -59,11 +59,11 @@
         "options": {
           "repos": [
             {
-              "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
+              "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
               "checksum": "sha256:3ce6f4ffa7d05b81fec2ba5a1c4917b351231c3433f2f5eed86ed5f8f97168a1"
             },
             {
-              "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
+              "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
               "checksum": "sha256:662e1dc0e465dd5e8efec9f42ef24ac9fd8418294254c3c98ea0342d6e903aed"
             }
           ],
diff --git a/test/cases/rhel82_vmdk.json b/test/cases/rhel82_vmdk.json
index e678cff0c..c997347e9 100644
--- a/test/cases/rhel82_vmdk.json
+++ b/test/cases/rhel82_vmdk.json
@@ -31,11 +31,11 @@
             "options": {
               "repos": [
                 {
-                  "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
+                  "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
                   "checksum": "sha256:3ce6f4ffa7d05b81fec2ba5a1c4917b351231c3433f2f5eed86ed5f8f97168a1"
                 },
                 {
-                  "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
+                  "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
                   "checksum": "sha256:662e1dc0e465dd5e8efec9f42ef24ac9fd8418294254c3c98ea0342d6e903aed"
                 }
               ],
@@ -68,11 +68,11 @@
         "options": {
           "repos": [
             {
-              "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
+              "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/BaseOS/x86_64/os",
               "checksum": "sha256:3ce6f4ffa7d05b81fec2ba5a1c4917b351231c3433f2f5eed86ed5f8f97168a1"
             },
             {
-              "baseurl": "http://download-ipv4.eng.brq.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
+              "baseurl": "http://download.devel.redhat.com/rhel-8/nightly/RHEL-8/latest-RHEL-8.2.0/compose/AppStream/x86_64/os",
               "checksum": "sha256:662e1dc0e465dd5e8efec9f42ef24ac9fd8418294254c3c98ea0342d6e903aed"
             }
           ],