Repository: osbuild/osbuild-composer
Pull request #122 (2019-11-26 23:27:20)

Fixes to the worker
Make sure we don't panic, but correctly return FAILED state instead. Also, set the correct AMI name for the aws target type.


Patch:
diff --git a/cmd/osbuild-worker/main.go b/cmd/osbuild-worker/main.go
index ab05ab862..40b37650a 100644
--- a/cmd/osbuild-worker/main.go
+++ b/cmd/osbuild-worker/main.go
@@ -74,21 +74,36 @@ func (c *ComposerClient) UpdateJob(job *jobqueue.Job, status string) error {
 	return nil
 }
 
-func main() {
-	client := NewClient()
+func handleJob(client *ComposerClient) {
+	fmt.Println("Waiting for a new job...")
+	job, err := client.AddJob()
+	if err != nil {
+		panic(err)
+	}
 
-	for {
-		fmt.Println("Waiting for a new job...")
-		job, err := client.AddJob()
+	client.UpdateJob(job, "RUNNING")
+
+	fmt.Printf("Running job %s\n", job.ID.String())
+	err, errs := job.Run()
+	if err != nil {
+		client.UpdateJob(job, "FAILED")
+		return
+	}
+
+	for _, err := range errs {
 		if err != nil {
-			panic(err)
+			client.UpdateJob(job, "FAILED")
+			return
 		}
+	}
 
-		client.UpdateJob(job, "RUNNING")
+	client.UpdateJob(job, "FINISHED")
+}
 
-		fmt.Printf("Running job %s\n", job.ID.String())
-		job.Run()
+func main() {
+	client := NewClient()
 
-		client.UpdateJob(job, "FINISHED")
+	for {
+		handleJob(client)
 	}
 }
diff --git a/internal/jobqueue/job.go b/internal/jobqueue/job.go
index 09ca106a6..e220465a0 100644
--- a/internal/jobqueue/job.go
+++ b/internal/jobqueue/job.go
@@ -2,6 +2,7 @@ package jobqueue
 
 import (
 	"encoding/json"
+	"fmt"
 	"os"
 	"os/exec"
 
@@ -21,28 +22,28 @@ type JobStatus struct {
 	Status string `json:"status"`
 }
 
-func (job *Job) Run() error {
+func (job *Job) Run() (error, []error) {
 	cmd := exec.Command("osbuild", "--store", "/var/cache/osbuild-composer/store", "--json", "-")
 	cmd.Stderr = os.Stderr
 
 	stdin, err := cmd.StdinPipe()
 	if err != nil {
-		return err
+		return err, nil
 	}
 
 	stdout, err := cmd.StdoutPipe()
 	if err != nil {
-		return err
+		return err, nil
 	}
 
 	err = cmd.Start()
 	if err != nil {
-		return err
+		return err, nil
 	}
 
 	err = json.NewEncoder(stdin).Encode(job.Pipeline)
 	if err != nil {
-		return err
+		return err, nil
 	}
 	stdin.Close()
 
@@ -52,20 +53,23 @@ func (job *Job) Run() error {
 	}
 	err = json.NewDecoder(stdout).Decode(&result)
 	if err != nil {
-		return err
+		return err, nil
 	}
 
 	err = cmd.Wait()
 	if err != nil {
-		return err
+		return err, nil
 	}
 
+	var r []error
+
 	for _, t := range job.Targets {
 		switch options := t.Options.(type) {
 		case *target.LocalTargetOptions:
 			err = os.MkdirAll(options.Location, 0755)
 			if err != nil {
-				panic(err)
+				r = append(r, err)
+				continue
 			}
 
 			cp := exec.Command("cp", "-a", "-L", "/var/cache/osbuild-composer/store/refs/"+result.OutputID+"/.", options.Location)
@@ -73,29 +77,34 @@ func (job *Job) Run() error {
 			cp.Stdout = os.Stdout
 			err = cp.Run()
 			if err != nil {
-				panic(err)
+				r = append(r, err)
+				continue
 			}
 		case *target.AWSTargetOptions:
 			a, err := awsupload.New(options.Region, options.AccessKeyID, options.SecretAccessKey)
 			if err != nil {
-				panic(err)
+				r = append(r, err)
+				continue
 			}
 
 			_, err = a.Upload("/var/cache/osbuild-composer/store/refs/"+result.OutputID+"/image.ami", options.Bucket, options.Key)
 			if err != nil {
-				panic(err)
+				r = append(r, err)
+				continue
 			}
 
 			/* TODO: communicate back the AMI */
-			_, err = a.Register(result.OutputID, options.Bucket, options.Key)
+			_, err = a.Register(t.ImageName, options.Bucket, options.Key)
 			if err != nil {
-				panic(err)
+				r = append(r, err)
+				continue
 			}
 		case *target.AzureTargetOptions:
 		default:
-			panic("foo")
+			r = append(r, fmt.Errorf("invalid target type"))
 		}
+		r = append(r, nil)
 	}
 
-	return nil
+	return nil, r
 }
diff --git a/internal/target/target.go b/internal/target/target.go
index 65f727a41..f66d1f6fc 100644
--- a/internal/target/target.go
+++ b/internal/target/target.go
@@ -6,8 +6,9 @@ import (
 )
 
 type Target struct {
-	Name    string        `json:"name"`
-	Options TargetOptions `json:"options"`
+	ImageName string        `json:"image_name"`
+	Name      string        `json:"name"`
+	Options   TargetOptions `json:"options"`
 }
 
 type TargetOptions interface {


Test patch:
diff --git a/internal/jobqueue/api_test.go b/internal/jobqueue/api_test.go
index 75864532b..edead4827 100644
--- a/internal/jobqueue/api_test.go
+++ b/internal/jobqueue/api_test.go
@@ -48,7 +48,7 @@ func TestCreate(t *testing.T) {
 	}
 
 	test.TestRoute(t, api, false, "POST", "/job-queue/v1/jobs", `{}`, http.StatusCreated,
-		`{"id":"ffffffff-ffff-ffff-ffff-ffffffffffff","pipeline":{"build":{"pipeline":{"stages":[{"name":"org.osbuild.dnf","options":{"repos":[{"metalink":"https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever\u0026arch=$basearch","gpgkey":"-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm\nbbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75\nL+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy\nKJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R\nn7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO\n4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53\n2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc\nYKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq\nSDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ\ng0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3\nDlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB\ntDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac\ng9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8\nf79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va\nN9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D\nK07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ\nOx5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o\n8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml\nSMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7\n+zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7\nCxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O\npMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA==\n=BfZ/\n-----END PGP PUBLIC KEY BLOCK-----\n","checksum":"sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97"}],"packages":["dnf","e2fsprogs","policycoreutils","qemu-img","systemd","grub2-pc","tar"],"releasever":"30","basearch":"x86_64"}}]},"runner":"org.osbuild.fedora30"},"stages":[{"name":"org.osbuild.dnf","options":{"repos":[{"metalink":"https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever\u0026arch=$basearch","gpgkey":"-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm\nbbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75\nL+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy\nKJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R\nn7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO\n4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53\n2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc\nYKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq\nSDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ\ng0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3\nDlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB\ntDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac\ng9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8\nf79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va\nN9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D\nK07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ\nOx5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o\n8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml\nSMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7\n+zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7\nCxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O\npMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA==\n=BfZ/\n-----END PGP PUBLIC KEY BLOCK-----\n","checksum":"sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97"}],"packages":["policycoreutils","selinux-policy-targeted","kernel","firewalld","chrony","langpacks-en"],"exclude_packages":["dracut-config-rescue"],"releasever":"30","basearch":"x86_64"}},{"name":"org.osbuild.fix-bls","options":{}},{"name":"org.osbuild.locale","options":{"language":"en_US"}},{"name":"org.osbuild.grub2","options":{"root_fs_uuid":"76a22bf4-f153-4541-b6c7-0332c0dfaeac","boot_fs_uuid":"00000000-0000-0000-0000-000000000000","kernel_opts":"ro biosdevname=0 net.ifnames=0"}},{"name":"org.osbuild.selinux","options":{"file_contexts":"etc/selinux/targeted/contexts/files/file_contexts"}}],"assembler":{"name":"org.osbuild.tar","options":{"filename":"root.tar.xz"}}},"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":{"pipeline":{"stages":[{"name":"org.osbuild.dnf","options":{"repos":[{"metalink":"https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever\u0026arch=$basearch","gpgkey":"-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm\nbbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75\nL+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy\nKJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R\nn7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO\n4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53\n2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc\nYKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq\nSDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ\ng0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3\nDlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB\ntDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac\ng9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8\nf79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va\nN9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D\nK07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ\nOx5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o\n8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml\nSMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7\n+zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7\nCxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O\npMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA==\n=BfZ/\n-----END PGP PUBLIC KEY BLOCK-----\n","checksum":"sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97"}],"packages":["dnf","e2fsprogs","policycoreutils","qemu-img","systemd","grub2-pc","tar"],"releasever":"30","basearch":"x86_64"}}]},"runner":"org.osbuild.fedora30"},"stages":[{"name":"org.osbuild.dnf","options":{"repos":[{"metalink":"https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever\u0026arch=$basearch","gpgkey":"-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBFturGcBEACv0xBo91V2n0uEC2vh69ywCiSyvUgN/AQH8EZpCVtM7NyjKgKm\nbbY4G3R0M3ir1xXmvUDvK0493/qOiFrjkplvzXFTGpPTi0ypqGgxc5d0ohRA1M75\nL+0AIlXoOgHQ358/c4uO8X0JAA1NYxCkAW1KSJgFJ3RjukrfqSHWthS1d4o8fhHy\nKJKEnirE5hHqB50dafXrBfgZdaOs3C6ppRIePFe2o4vUEapMTCHFw0woQR8Ah4/R\nn7Z9G9Ln+0Cinmy0nbIDiZJ+pgLAXCOWBfDUzcOjDGKvcpoZharA07c0q1/5ojzO\n4F0Fh4g/BUmtrASwHfcIbjHyCSr1j/3Iz883iy07gJY5Yhiuaqmp0o0f9fgHkG53\n2xCU1owmACqaIBNQMukvXRDtB2GJMuKa/asTZDP6R5re+iXs7+s9ohcRRAKGyAyc\nYKIQKcaA+6M8T7/G+TPHZX6HJWqJJiYB+EC2ERblpvq9TPlLguEWcmvjbVc31nyq\nSDoO3ncFWKFmVsbQPTbP+pKUmlLfJwtb5XqxNR5GEXSwVv4I7IqBmJz1MmRafnBZ\ng0FJUtH668GnldO20XbnSVBr820F5SISMXVwCXDXEvGwwiB8Lt8PvqzXnGIFDAu3\nDlQI5sxSqpPVWSyw08ppKT2Tpmy8adiBotLfaCFl2VTHwOae48X2dMPBvQARAQAB\ntDFGZWRvcmEgKDMwKSA8ZmVkb3JhLTMwLXByaW1hcnlAZmVkb3JhcHJvamVjdC5v\ncmc+iQI4BBMBAgAiBQJbbqxnAhsPBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAK\nCRDvPBEfz8ZZudTnD/9170LL3nyTVUCFmBjT9wZ4gYnpwtKVPa/pKnxbbS+Bmmac\ng9TrT9pZbqOHrNJLiZ3Zx1Hp+8uxr3Lo6kbYwImLhkOEDrf4aP17HfQ6VYFbQZI8\nf79OFxWJ7si9+3gfzeh9UYFEqOQfzIjLWFyfnas0OnV/P+RMQ1Zr+vPRqO7AR2va\nN9wg+Xl7157dhXPCGYnGMNSoxCbpRs0JNlzvJMuAea5nTTznRaJZtK/xKsqLn51D\nK07k9MHVFXakOH8QtMCUglbwfTfIpO5YRq5imxlWbqsYWVQy1WGJFyW6hWC0+RcJ\nOx5zGtOfi4/dN+xJ+ibnbyvy/il7Qm+vyFhCYqIPyS5m2UVJUuao3eApE38k78/o\n8aQOTnFQZ+U1Sw+6woFTxjqRQBXlQm2+7Bt3bqGATg4sXXWPbmwdL87Ic+mxn/ml\nSMfQux/5k6iAu1kQhwkO2YJn9eII6HIPkW+2m5N1JsUyJQe4cbtZE5Yh3TRA0dm7\n+zoBRfCXkOW4krchbgww/ptVmzMMP7GINJdROrJnsGl5FVeid9qHzV7aZycWSma7\nCxBYB1J8HCbty5NjtD6XMYRrMLxXugvX6Q4NPPH+2NKjzX4SIDejS6JjgrP3KA3O\npMuo7ZHMfveBngv8yP+ZD/1sS6l+dfExvdaJdOdgFCnp4p3gPbw5+Lv70HrMjA==\n=BfZ/\n-----END PGP PUBLIC KEY BLOCK-----\n","checksum":"sha256:9f596e18f585bee30ac41c11fb11a83ed6b11d5b341c1cb56ca4015d7717cb97"}],"packages":["policycoreutils","selinux-policy-targeted","kernel","firewalld","chrony","langpacks-en"],"exclude_packages":["dracut-config-rescue"],"releasever":"30","basearch":"x86_64"}},{"name":"org.osbuild.fix-bls","options":{}},{"name":"org.osbuild.locale","options":{"language":"en_US"}},{"name":"org.osbuild.grub2","options":{"root_fs_uuid":"76a22bf4-f153-4541-b6c7-0332c0dfaeac","boot_fs_uuid":"00000000-0000-0000-0000-000000000000","kernel_opts":"ro biosdevname=0 net.ifnames=0"}},{"name":"org.osbuild.selinux","options":{"file_contexts":"etc/selinux/targeted/contexts/files/file_contexts"}}],"assembler":{"name":"org.osbuild.tar","options":{"filename":"root.tar.xz"}}},"targets":[{"image_name":"","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) {
diff --git a/internal/pipeline/stage_test.go b/internal/pipeline/stage_test.go
new file mode 100644
index 000000000..3093f69a9
--- /dev/null
+++ b/internal/pipeline/stage_test.go
@@ -0,0 +1,240 @@
+package pipeline
+
+import (
+	"bytes"
+	"encoding/json"
+	"reflect"
+	"testing"
+)
+
+func TestStage_UnmarshalJSON(t *testing.T) {
+	type fields struct {
+		Name    string
+		Options StageOptions
+	}
+	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 stage",
+			args: args{
+				data: []byte(`{"name":"org.osbuild.foo","options":{"bar":null}}`),
+			},
+			wantErr: true,
+		},
+		{
+			name: "missing options",
+			args: args{
+				data: []byte(`{"name":"org.osbuild.locale"}`),
+			},
+			wantErr: true,
+		},
+		{
+			name: "missing name",
+			args: args{
+				data: []byte(`{"foo":null,"options":{"bar":null}}`),
+			},
+			wantErr: true,
+		},
+		{
+			name: "chrony",
+			fields: fields{
+				Name:    "org.osbuild.chrony",
+				Options: &ChronyStageOptions{},
+			},
+			args: args{
+				data: []byte(`{"name":"org.osbuild.chrony","options":{"timeservers":null}}`),
+			},
+		},
+		{
+			name: "dnf",
+			fields: fields{
+				Name:    "org.osbuild.dnf",
+				Options: &DNFStageOptions{},
+			},
+			args: args{
+				data: []byte(`{"name":"org.osbuild.dnf","options":{"repos":null,"packages":null,"releasever":"","basearch":""}}`),
+			},
+		},
+		{
+			name: "firewall",
+			fields: fields{
+				Name:    "org.osbuild.firewall",
+				Options: &FirewallStageOptions{},
+			},
+			args: args{
+				data: []byte(`{"name":"org.osbuild.firewall","options":{}}`),
+			},
+		},
+		{
+			name: "fix-bls",
+			fields: fields{
+				Name:    "org.osbuild.fix-bls",
+				Options: &FixBLSStageOptions{},
+			},
+			args: args{
+				data: []byte(`{"name":"org.osbuild.fix-bls","options":{}}`),
+			},
+		},
+		{
+			name: "fstab",
+			fields: fields{
+				Name:    "org.osbuild.fstab",
+				Options: &FSTabStageOptions{},
+			},
+			args: args{
+				data: []byte(`{"name":"org.osbuild.fstab","options":{"filesystems":null}}`),
+			},
+		},
+		{
+			name: "groups",
+			fields: fields{
+				Name:    "org.osbuild.groups",
+				Options: &GroupsStageOptions{},
+			},
+			args: args{
+				data: []byte(`{"name":"org.osbuild.groups","options":{"groups":null}}`),
+			},
+		},
+		{
+			name: "grub2",
+			fields: fields{
+				Name:    "org.osbuild.grub2",
+				Options: &GRUB2StageOptions{},
+			},
+			args: args{
+				data: []byte(`{"name":"org.osbuild.grub2","options":{"root_fs_uuid":"00000000-0000-0000-0000-000000000000","boot_fs_uuid":"00000000-0000-0000-0000-000000000000"}}`),
+			},
+		},
+		{
+			name: "hostname",
+			fields: fields{
+				Name:    "org.osbuild.hostname",
+				Options: &HostnameStageOptions{},
+			},
+			args: args{
+				data: []byte(`{"name":"org.osbuild.hostname","options":{"hostname":""}}`),
+			},
+		},
+		{
+			name: "keymap",
+			fields: fields{
+				Name:    "org.osbuild.keymap",
+				Options: &KeymapStageOptions{},
+			},
+			args: args{
+				data: []byte(`{"name":"org.osbuild.keymap","options":{"keymap":""}}`),
+			},
+		},
+		{
+			name: "locale",
+			fields: fields{
+				Name:    "org.osbuild.locale",
+				Options: &LocaleStageOptions{},
+			},
+			args: args{
+				data: []byte(`{"name":"org.osbuild.locale","options":{"language":""}}`),
+			},
+		},
+		{
+			name: "script",
+			fields: fields{
+				Name:    "org.osbuild.script",
+				Options: &ScriptStageOptions{},
+			},
+			args: args{
+				data: []byte(`{"name":"org.osbuild.script","options":{"script":""}}`),
+			},
+		},
+		{
+			name: "selinux",
+			fields: fields{
+				Name:    "org.osbuild.selinux",
+				Options: &SELinuxStageOptions{},
+			},
+			args: args{
+				data: []byte(`{"name":"org.osbuild.selinux","options":{"file_contexts":""}}`),
+			},
+		},
+		{
+			name: "systemd",
+			fields: fields{
+				Name:    "org.osbuild.systemd",
+				Options: &SystemdStageOptions{},
+			},
+			args: args{
+				data: []byte(`{"name":"org.osbuild.systemd","options":{}}`),
+			},
+		},
+		{
+			name: "systemd-enabled",
+			fields: fields{
+				Name: "org.osbuild.systemd",
+				Options: &SystemdStageOptions{
+					EnabledServices: []string{"foo.service"},
+				},
+			},
+			args: args{
+				data: []byte(`{"name":"org.osbuild.systemd","options":{"enabled_services":["foo.service"]}}`),
+			},
+		},
+		{
+			name: "timezone",
+			fields: fields{
+				Name:    "org.osbuild.timezone",
+				Options: &TimezoneStageOptions{},
+			},
+			args: args{
+				data: []byte(`{"name":"org.osbuild.timezone","options":{"zone":""}}`),
+			},
+		},
+		{
+			name: "users",
+			fields: fields{
+				Name:    "org.osbuild.users",
+				Options: &UsersStageOptions{},
+			},
+			args: args{
+				data: []byte(`{"name":"org.osbuild.users","options":{"users":null}}`),
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			stage := &Stage{
+				Name:    tt.fields.Name,
+				Options: tt.fields.Options,
+			}
+			var gotStage Stage
+			if err := gotStage.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
+				t.Errorf("Stage.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
+			}
+			if tt.wantErr {
+				return
+			}
+			gotBytes, err := json.Marshal(stage)
+			if err != nil {
+				t.Errorf("Could not marshal stage: %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(&gotStage, stage) {
+				t.Errorf("got {%v, %v}, expected {%v, %v}", gotStage.Name, gotStage.Options, stage.Name, stage.Options)
+			}
+		})
+	}
+}