Repository: osbuild/osbuild-composer
Pull request #24 (2019-10-07 23:14:21)

Blueprint
Hook up the output types with proper sample blueprint->pipeline translations.


Patch:
diff --git a/cmd/osbuild-pipeline/main.go b/cmd/osbuild-pipeline/main.go
new file mode 100644
index 000000000..b05429ad2
--- /dev/null
+++ b/cmd/osbuild-pipeline/main.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+	"encoding/json"
+	"flag"
+	"os"
+	"osbuild-composer/internal/blueprint"
+)
+
+func main() {
+	var format string
+	flag.StringVar(&format, "output-format", "qcow2", "output format")
+	flag.Parse()
+
+	blueprint := &blueprint.Blueprint{}
+	pipeline, err := blueprint.ToPipeline(format)
+	if err != nil {
+		panic(err.Error())
+	}
+
+	bytes, err := json.Marshal(pipeline)
+	if err != nil {
+		panic("could not marshal pipeline into JSON")
+	}
+
+	os.Stdout.Write(bytes)
+}
diff --git a/internal/blueprint/ami_output.go b/internal/blueprint/ami_output.go
index 239d05011..619f26065 100644
--- a/internal/blueprint/ami_output.go
+++ b/internal/blueprint/ami_output.go
@@ -5,13 +5,11 @@ import "osbuild-composer/internal/pipeline"
 type amiOutput struct{}
 
 func (t *amiOutput) translate(b *Blueprint) *pipeline.Pipeline {
-	p := &pipeline.Pipeline{}
-	p.SetAssembler(
-		pipeline.NewQEMUAssembler(
-			&pipeline.QEMUAssemblerOptions{
-				Format:   "qcow2",
-				Filename: t.getName(),
-			}))
+	p := getF30Pipeline()
+	addF30FSTabStage(p)
+	addF30GRUB2Stage(p)
+	addF30SELinuxStage(p)
+	addF30QemuAssembler(p, "qcow2", t.getName())
 	return p
 }
 
diff --git a/internal/blueprint/blueprint.go b/internal/blueprint/blueprint.go
index 458ced7c3..af98b2a44 100644
--- a/internal/blueprint/blueprint.go
+++ b/internal/blueprint/blueprint.go
@@ -7,6 +7,14 @@ import (
 	"sort"
 )
 
+type InvalidOutputFormatError struct {
+	message string
+}
+
+func (e *InvalidOutputFormatError) Error() string {
+	return e.message
+}
+
 // A Blueprint is a high-level description of an image.
 type Blueprint struct {
 	Name        string    `json:"name"`
@@ -53,13 +61,20 @@ func ListOutputFormats() []string {
 }
 
 // ToPipeline converts the blueprint to a pipeline for a given output format.
-func (b *Blueprint) ToPipeline(outputFormat string) *pipeline.Pipeline {
-	return outputs[outputFormat].translate(b)
+func (b *Blueprint) ToPipeline(outputFormat string) (*pipeline.Pipeline, error) {
+	if output, exists := outputs[outputFormat]; exists {
+		return output.translate(b), nil
+	}
+
+	return nil, &InvalidOutputFormatError{outputFormat}
 }
 
 // FilenameFromType gets the canonical filename and MIME type for a given
 // output format
-func FilenameFromType(outputFormat string) (string, string) {
-	translator := outputs[outputFormat]
-	return translator.getName(), translator.getMime()
+func FilenameFromType(outputFormat string) (string, string, error) {
+	if output, exists := outputs[outputFormat]; exists {
+		return output.getName(), output.getMime(), nil
+	}
+
+	return "", "", &InvalidOutputFormatError{outputFormat}
 }
diff --git a/internal/blueprint/disk_output.go b/internal/blueprint/disk_output.go
index 514a37e2a..c52ac9197 100644
--- a/internal/blueprint/disk_output.go
+++ b/internal/blueprint/disk_output.go
@@ -5,13 +5,11 @@ import "osbuild-composer/internal/pipeline"
 type diskOutput struct{}
 
 func (t *diskOutput) translate(b *Blueprint) *pipeline.Pipeline {
-	p := &pipeline.Pipeline{}
-	p.SetAssembler(
-		pipeline.NewQEMUAssembler(
-			&pipeline.QEMUAssemblerOptions{
-				Format:   "raw",
-				Filename: t.getName(),
-			}))
+	p := getF30Pipeline()
+	addF30FSTabStage(p)
+	addF30GRUB2Stage(p)
+	addF30SELinuxStage(p)
+	addF30QemuAssembler(p, "raw", t.getName())
 	return p
 }
 
diff --git a/internal/blueprint/ext4_output.go b/internal/blueprint/ext4_output.go
index 06ebe0733..3b2a89beb 100644
--- a/internal/blueprint/ext4_output.go
+++ b/internal/blueprint/ext4_output.go
@@ -5,13 +5,9 @@ import "osbuild-composer/internal/pipeline"
 type ext4Output struct{}
 
 func (t *ext4Output) translate(b *Blueprint) *pipeline.Pipeline {
-	p := &pipeline.Pipeline{}
-	p.SetAssembler(
-		pipeline.NewQEMUAssembler(
-			&pipeline.QEMUAssemblerOptions{
-				Format:   "raw",
-				Filename: t.getName(),
-			}))
+	p := getF30Pipeline()
+	addF30SELinuxStage(p)
+	addF30RawFSAssembler(p, t.getName())
 	return p
 }
 
diff --git a/internal/blueprint/f30_helpers.go b/internal/blueprint/f30_helpers.go
new file mode 100644
index 000000000..ed95abc28
--- /dev/null
+++ b/internal/blueprint/f30_helpers.go
@@ -0,0 +1,125 @@
+package blueprint
+
+import (
+	"osbuild-composer/internal/pipeline"
+
+	"github.com/google/uuid"
+)
+
+func getF30Repository() *pipeline.DNFRepository {
+	repo := pipeline.NewDNFRepository("https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch", "", "")
+	repo.SetChecksum("sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97")
+	repo.SetGPGKey("F1D8 EC98 F241 AAF2 0DF6  9420 EF3C 111F CFC6 59B9")
+	return repo
+}
+
+func getF30BuildPipeline() *pipeline.Pipeline {
+	p := &pipeline.Pipeline{}
+	options := &pipeline.DNFStageOptions{
+		ReleaseVersion:   "30",
+		BaseArchitecture: "x86_64",
+	}
+	options.AddRepository(getF30Repository())
+	options.AddPackage("dnf")
+	options.AddPackage("e2fsprogs")
+	options.AddPackage("policycoreutils")
+	options.AddPackage("qemu-img")
+	options.AddPackage("systemd")
+	options.AddPackage("grub2-pc")
+	options.AddPackage("tar")
+	p.AddStage(pipeline.NewDNFStage(options))
+	return p
+}
+
+func getF30Pipeline() *pipeline.Pipeline {
+	p := &pipeline.Pipeline{
+		BuildPipeline: getF30BuildPipeline(),
+	}
+	options := &pipeline.DNFStageOptions{
+		ReleaseVersion:   "30",
+		BaseArchitecture: "x86_64",
+	}
+	options.AddRepository(getF30Repository())
+	options.AddPackage("@Core")
+	options.AddPackage("chrony")
+	options.AddPackage("kernel")
+	options.AddPackage("selinux-policy-targeted")
+	options.AddPackage("grub2-pc")
+	options.AddPackage("spice-vdagent")
+	options.AddPackage("qemu-guest-agent")
+	options.AddPackage("xen-libs")
+	options.AddPackage("langpacks-en")
+	p.AddStage(pipeline.NewDNFStage(options))
+	p.AddStage(pipeline.NewFixBLSStage())
+	p.AddStage(pipeline.NewLocaleStage(
+		&pipeline.LocaleStageOptions{
+			Language: "en_US",
+		}))
+
+	return p
+}
+
+func addF30GRUB2Stage(p *pipeline.Pipeline) {
+	id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
+	if err != nil {
+		panic("invalid UUID")
+	}
+	p.AddStage(pipeline.NewGRUB2Stage(
+		&pipeline.GRUB2StageOptions{
+			RootFilesystemUUID: id,
+			KernelOptions:      "ro biosdevname=0 net.ifnames=0",
+		},
+	))
+}
+
+func addF30FSTabStage(p *pipeline.Pipeline) {
+	id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
+	if err != nil {
+		panic("invalid UUID")
+	}
+	options := &pipeline.FSTabStageOptions{}
+	options.AddFilesystem(id, "extf4", "/", "defaults", 1, 1)
+	p.AddStage(pipeline.NewFSTabStage(options))
+}
+
+func addF30SELinuxStage(p *pipeline.Pipeline) {
+	p.AddStage(pipeline.NewSELinuxStage(
+		&pipeline.SELinuxStageOptions{
+			FileContexts: "etc/selinux/targeted/contexts/files/file_contexts",
+		}))
+}
+
+func addF30QemuAssembler(p *pipeline.Pipeline, format string, filename string) {
+	id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
+	if err != nil {
+		panic("invalid UUID")
+	}
+	p.Assembler = pipeline.NewQEMUAssembler(
+		&pipeline.QEMUAssemblerOptions{
+			Format:             format,
+			Filename:           filename,
+			PTUUID:             "0x14fc63d2",
+			RootFilesystemUUDI: id,
+			Size:               3221225472,
+		})
+}
+
+func addF30TarAssembler(p *pipeline.Pipeline, filename string) {
+	p.Assembler = pipeline.NewTarAssembler(
+		&pipeline.TarAssemblerOptions{
+			Filename: filename,
+		})
+}
+
+func addF30RawFSAssembler(p *pipeline.Pipeline, filename string) {
+	id, err := uuid.Parse("76a22bf4-f153-4541-b6c7-0332c0dfaeac")
+	if err != nil {
+		panic("invalid UUID")
+	}
+	p.Assembler = pipeline.NewRawFSAssembler(
+		&pipeline.RawFSAssemblerOptions{
+			Filename:           filename,
+			RootFilesystemUUDI: id,
+			Size:               3221225472,
+		})
+}
diff --git a/internal/blueprint/liveiso_output.go b/internal/blueprint/liveiso_output.go
index c88616773..bde3966bc 100644
--- a/internal/blueprint/liveiso_output.go
+++ b/internal/blueprint/liveiso_output.go
@@ -5,13 +5,10 @@ import "osbuild-composer/internal/pipeline"
 type liveIsoOutput struct{}
 
 func (t *liveIsoOutput) translate(b *Blueprint) *pipeline.Pipeline {
-	p := &pipeline.Pipeline{}
-	p.SetAssembler(
-		pipeline.NewQEMUAssembler(
-			&pipeline.QEMUAssemblerOptions{
-				Format:   "raw",
-				Filename: t.getName(),
-			}))
+	// TODO!
+	p := getF30Pipeline()
+	addF30SELinuxStage(p)
+	addF30QemuAssembler(p, "raw", t.getName())
 	return p
 }
 
diff --git a/internal/blueprint/openstack_output.go b/internal/blueprint/openstack_output.go
index ce354a70e..c8775c7a4 100644
--- a/internal/blueprint/openstack_output.go
+++ b/internal/blueprint/openstack_output.go
@@ -5,13 +5,11 @@ import "osbuild-composer/internal/pipeline"
 type openstackOutput struct{}
 
 func (t *openstackOutput) translate(b *Blueprint) *pipeline.Pipeline {
-	p := &pipeline.Pipeline{}
-	p.SetAssembler(
-		pipeline.NewQEMUAssembler(
-			&pipeline.QEMUAssemblerOptions{
-				Format:   "qcow2",
-				Filename: t.getName(),
-			}))
+	p := getF30Pipeline()
+	addF30FSTabStage(p)
+	addF30GRUB2Stage(p)
+	addF30SELinuxStage(p)
+	addF30QemuAssembler(p, "qcow2", t.getName())
 	return p
 }
 
diff --git a/internal/blueprint/qcow2_output.go b/internal/blueprint/qcow2_output.go
index 0571e6e78..524b40357 100644
--- a/internal/blueprint/qcow2_output.go
+++ b/internal/blueprint/qcow2_output.go
@@ -5,13 +5,11 @@ import "osbuild-composer/internal/pipeline"
 type qcow2Output struct{}
 
 func (t *qcow2Output) translate(b *Blueprint) *pipeline.Pipeline {
-	p := &pipeline.Pipeline{}
-	p.SetAssembler(
-		pipeline.NewQEMUAssembler(
-			&pipeline.QEMUAssemblerOptions{
-				Format:   "qcow2",
-				Filename: t.getName(),
-			}))
+	p := getF30Pipeline()
+	addF30FSTabStage(p)
+	addF30GRUB2Stage(p)
+	addF30SELinuxStage(p)
+	addF30QemuAssembler(p, "qcow2", t.getName())
 	return p
 }
 
diff --git a/internal/blueprint/tar_output.go b/internal/blueprint/tar_output.go
index 4c11a21dc..5b6295cde 100644
--- a/internal/blueprint/tar_output.go
+++ b/internal/blueprint/tar_output.go
@@ -5,12 +5,9 @@ import "osbuild-composer/internal/pipeline"
 type tarOutput struct{}
 
 func (t *tarOutput) translate(b *Blueprint) *pipeline.Pipeline {
-	p := &pipeline.Pipeline{}
-	p.SetAssembler(
-		pipeline.NewTarAssembler(
-			&pipeline.TarAssemblerOptions{
-				Filename: "image.tar",
-			}))
+	p := getF30Pipeline()
+	addF30SELinuxStage(p)
+	addF30TarAssembler(p, t.getName())
 	return p
 }
 
diff --git a/internal/blueprint/vhd_output.go b/internal/blueprint/vhd_output.go
index 19501a484..808b94ae5 100644
--- a/internal/blueprint/vhd_output.go
+++ b/internal/blueprint/vhd_output.go
@@ -5,13 +5,11 @@ import "osbuild-composer/internal/pipeline"
 type vhdOutput struct{}
 
 func (t *vhdOutput) translate(b *Blueprint) *pipeline.Pipeline {
-	p := &pipeline.Pipeline{}
-	p.SetAssembler(
-		pipeline.NewQEMUAssembler(
-			&pipeline.QEMUAssemblerOptions{
-				Format:   "qcow2",
-				Filename: t.getName(),
-			}))
+	p := getF30Pipeline()
+	addF30FSTabStage(p)
+	addF30GRUB2Stage(p)
+	addF30SELinuxStage(p)
+	addF30QemuAssembler(p, "qcow2", t.getName())
 	return p
 }
 
diff --git a/internal/blueprint/vmdk_output.go b/internal/blueprint/vmdk_output.go
index ae9fd5ec0..738e25c53 100644
--- a/internal/blueprint/vmdk_output.go
+++ b/internal/blueprint/vmdk_output.go
@@ -5,13 +5,11 @@ import "osbuild-composer/internal/pipeline"
 type vmdkOutput struct{}
 
 func (t *vmdkOutput) translate(b *Blueprint) *pipeline.Pipeline {
-	p := &pipeline.Pipeline{}
-	p.SetAssembler(
-		pipeline.NewQEMUAssembler(
-			&pipeline.QEMUAssemblerOptions{
-				Format:   "vmdk",
-				Filename: t.getName(),
-			}))
+	p := getF30Pipeline()
+	addF30FSTabStage(p)
+	addF30GRUB2Stage(p)
+	addF30SELinuxStage(p)
+	addF30QemuAssembler(p, "vmdk", t.getName())
 	return p
 }
 
diff --git a/internal/pipeline/assembler.go b/internal/pipeline/assembler.go
index 558e3d7e5..0af5ca6f8 100644
--- a/internal/pipeline/assembler.go
+++ b/internal/pipeline/assembler.go
@@ -34,7 +34,7 @@ func (assembler *Assembler) UnmarshalJSON(data []byte) error {
 	switch rawAssembler.Name {
 	case "org.osbuild.tar":
 		options = new(TarAssemblerOptions)
-	case "org.osbuild.qcow2":
+	case "org.osbuild.qemu":
 		options = new(QEMUAssemblerOptions)
 	default:
 		return errors.New("unexpected assembler name")
diff --git a/internal/pipeline/dnf_stage.go b/internal/pipeline/dnf_stage.go
index 2b06b1039..571511e5e 100644
--- a/internal/pipeline/dnf_stage.go
+++ b/internal/pipeline/dnf_stage.go
@@ -8,10 +8,10 @@ package pipeline
 // metadata of the repository has changed since the stage options were
 // first generated, the stage may fail.
 type DNFStageOptions struct {
-	Repositories     map[string]*DNFRepository `json:"repos"`
-	Packages         []string                  `json:"packages"`
-	ReleaseVersion   string                    `json:"releasever"`
-	BaseArchitecture string                    `json:"basearch"`
+	Repositories     []*DNFRepository `json:"repos"`
+	Packages         []string         `json:"packages"`
+	ReleaseVersion   string           `json:"releasever"`
+	BaseArchitecture string           `json:"basearch"`
 }
 
 func (DNFStageOptions) isStageOptions() {}
@@ -30,7 +30,8 @@ type DNFRepository struct {
 // mandatory fields, but no repositories.
 func NewDNFStageOptions(releaseVersion string, baseArchitecture string) *DNFStageOptions {
 	return &DNFStageOptions{
-		Repositories:     make(map[string]*DNFRepository),
+		Repositories:     make([]*DNFRepository, 0),
+		Packages:         make([]string, 0),
 		ReleaseVersion:   releaseVersion,
 		BaseArchitecture: baseArchitecture,
 	}
@@ -50,8 +51,8 @@ func (options *DNFStageOptions) AddPackage(pkg string) {
 }
 
 // AddRepository adds a repository to a DNFStageOptions object.
-func (options *DNFStageOptions) AddRepository(name string, repo *DNFRepository) {
-	options.Repositories[name] = repo
+func (options *DNFStageOptions) AddRepository(repo *DNFRepository) {
+	options.Repositories = append(options.Repositories, repo)
 }
 
 // NewDNFRepository creates a new DNFRepository object. Exactly one of the
diff --git a/internal/pipeline/qcow2_assembler.go b/internal/pipeline/qemu_assembler.go
similarity index 97%
rename from internal/pipeline/qcow2_assembler.go
rename to internal/pipeline/qemu_assembler.go
index 73a2aed9a..dcb7ef3d7 100644
--- a/internal/pipeline/qcow2_assembler.go
+++ b/internal/pipeline/qemu_assembler.go
@@ -34,7 +34,7 @@ func NewQEMUAssemblerOptions(format string, ptUUID string, filename string, root
 // NewQEMUAssembler creates a new QEMU Assembler object.
 func NewQEMUAssembler(options *QEMUAssemblerOptions) *Assembler {
 	return &Assembler{
-		Name:    "org.osbuild.qcow2",
+		Name:    "org.osbuild.qemu",
 		Options: options,
 	}
 }
diff --git a/internal/pipeline/rawfs_assembler.go b/internal/pipeline/rawfs_assembler.go
new file mode 100644
index 000000000..8d95412b3
--- /dev/null
+++ b/internal/pipeline/rawfs_assembler.go
@@ -0,0 +1,21 @@
+package pipeline
+
+import "github.com/google/uuid"
+
+// RawFSAssemblerOptions desrcibe how to assemble a tree into a raw filesystem
+// image.
+type RawFSAssemblerOptions struct {
+	Filename           string    `json:"filename"`
+	RootFilesystemUUDI uuid.UUID `json:"root_fs_uuid"`
+	Size               uint64    `json:"size"`
+}
+
+func (RawFSAssemblerOptions) isAssemblerOptions() {}
+
+// NewRawFSAssembler creates a new RawFS Assembler object.
+func NewRawFSAssembler(options *RawFSAssemblerOptions) *Assembler {
+	return &Assembler{
+		Name:    "org.osbuild.rawfs",
+		Options: options,
+	}
+}
diff --git a/internal/store/store.go b/internal/store/store.go
index 8a825d94b..25d87206f 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -340,10 +340,14 @@ func (s *Store) DeleteBlueprintFromWorkspace(name string) {
 	})
 }
 
-func (s *Store) PushCompose(composeID uuid.UUID, bp *blueprint.Blueprint, composeType string) {
+func (s *Store) PushCompose(composeID uuid.UUID, bp *blueprint.Blueprint, composeType string) error {
 	targets := []*target.Target{
 		target.NewLocalTarget(target.NewLocalTargetOptions("/var/lib/osbuild-composer/outputs/" + composeID.String())),
 	}
+	pipeline, err := bp.ToPipeline(composeType)
+	if err != nil {
+		return &InvalidRequestError{"invalid output type: " + composeType}
+	}
 	s.change(func() error {
 		s.Composes[composeID] = Compose{
 			QueueStatus: "WAITING",
@@ -356,9 +360,11 @@ func (s *Store) PushCompose(composeID uuid.UUID, bp *blueprint.Blueprint, compos
 	})
 	s.pendingJobs <- Job{
 		ComposeID: composeID,
-		Pipeline:  bp.ToPipeline(composeType),
+		Pipeline:  pipeline,
 		Targets:   targets,
 	}
+
+	return nil
 }
 
 func (s *Store) PopCompose() Job {
@@ -408,7 +414,10 @@ func (s *Store) GetImage(composeID uuid.UUID) (*Image, error) {
 		if compose.QueueStatus != "FINISHED" {
 			return nil, &InvalidRequestError{"compose was not finished"}
 		}
-		name, mime := blueprint.FilenameFromType(compose.OutputType)
+		name, mime, err := blueprint.FilenameFromType(compose.OutputType)
+		if err != nil {
+			panic("invalid output type")
+		}
 		for _, t := range compose.Targets {
 			switch options := t.Options.(type) {
 			case *target.LocalTargetOptions:


Test patch:
diff --git a/internal/jobqueue/api_test.go b/internal/jobqueue/api_test.go
index a8dcd7618..4cd159d9c 100644
--- a/internal/jobqueue/api_test.go
+++ b/internal/jobqueue/api_test.go
@@ -104,7 +104,7 @@ func TestCreate(t *testing.T) {
 	store.PushCompose(id, &blueprint.Blueprint{}, "tar")
 
 	testRoute(t, api, "POST", "/job-queue/v1/jobs", `{}`, http.StatusCreated,
-		`{"id":"ffffffff-ffff-ffff-ffff-ffffffffffff","pipeline":{"assembler":{"name":"org.osbuild.tar","options":{"filename":"image.tar"}}},"targets":[{"name":"org.osbuild.local","options":{"location":"/var/lib/osbuild-composer/outputs/ffffffff-ffff-ffff-ffff-ffffffffffff"}}]}`)
+		`{"id":"ffffffff-ffff-ffff-ffff-ffffffffffff","pipeline":{"build":{"stages":[{"name":"org.osbuild.dnf","options":{"repos":[{"metalink":"https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever\u0026arch=$basearch","gpgkey":"F1D8 EC98 F241 AAF2 0DF6  9420 EF3C 111F CFC6 59B9","checksum":"sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97"}],"packages":["dnf","e2fsprogs","policycoreutils","qemu-img","systemd","grub2-pc","tar"],"releasever":"30","basearch":"x86_64"}}]},"stages":[{"name":"org.osbuild.dnf","options":{"repos":[{"metalink":"https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever\u0026arch=$basearch","gpgkey":"F1D8 EC98 F241 AAF2 0DF6  9420 EF3C 111F CFC6 59B9","checksum":"sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97"}],"packages":["@Core","chrony","kernel","selinux-policy-targeted","grub2-pc","spice-vdagent","qemu-guest-agent","xen-libs","langpacks-en"],"releasever":"30","basearch":"x86_64"}},{"name":"org.osbuild.fix-bls","options":{}},{"name":"org.osbuild.locale","options":{"language":"en_US"}},{"name":"org.osbuild.selinux","options":{"file_contexts":"etc/selinux/targeted/contexts/files/file_contexts"}}],"assembler":{"name":"org.osbuild.tar","options":{"filename":"image.tar"}}},"targets":[{"name":"org.osbuild.local","options":{"location":"/var/lib/osbuild-composer/outputs/ffffffff-ffff-ffff-ffff-ffffffffffff"}}]}`)
 }
 
 func testUpdateTransition(t *testing.T, from, to string, expectedStatus int) {