Repository: osbuild/osbuild-composer
Pull request #152 (2019-12-09 15:12:41)

Compose/delete
see invididual commits


Patch:
diff --git a/cmd/osbuild-composer/main.go b/cmd/osbuild-composer/main.go
index 37aae7909..bf895b10e 100644
--- a/cmd/osbuild-composer/main.go
+++ b/cmd/osbuild-composer/main.go
@@ -19,7 +19,7 @@ func main() {
 	flag.BoolVar(&verbose, "v", false, "Print access log")
 	flag.Parse()
 
-	stateFile := "/var/lib/osbuild-composer/state.json"
+	stateDir := "/var/lib/osbuild-composer"
 
 	listeners, err := activation.Listeners()
 	if err != nil {
@@ -45,7 +45,7 @@ func main() {
 		logger = log.New(os.Stdout, "", 0)
 	}
 
-	store := store.New(&stateFile, distribution)
+	store := store.New(&stateDir, distribution)
 
 	jobAPI := jobqueue.New(logger, store)
 	weldrAPI := weldr.New(rpm, distribution, logger, store)
diff --git a/internal/jobqueue/job.go b/internal/jobqueue/job.go
index fd948adb4..db04aa019 100644
--- a/internal/jobqueue/job.go
+++ b/internal/jobqueue/job.go
@@ -99,16 +99,13 @@ func (job *Job) Run(d distro.Distro) (*store.Image, error, []error) {
 	for _, t := range job.Targets {
 		switch options := t.Options.(type) {
 		case *target.LocalTargetOptions:
-			err = os.MkdirAll(options.Location, 0755)
+			err = runCommand("cp", "-a", "-L", "/var/cache/osbuild-composer/store/refs/"+result.OutputID+"/.", options.Location)
 			if err != nil {
 				r = append(r, err)
 				continue
 			}
 
-			cp := exec.Command("cp", "-a", "-L", "/var/cache/osbuild-composer/store/refs/"+result.OutputID+"/.", options.Location)
-			cp.Stderr = os.Stderr
-			cp.Stdout = os.Stdout
-			err = cp.Run()
+			err = runCommand("chown", "-R", "_osbuild-composer:_osbuild-composer", options.Location)
 			if err != nil {
 				r = append(r, err)
 				continue
@@ -164,3 +161,10 @@ func (job *Job) Run(d distro.Distro) (*store.Image, error, []error) {
 
 	return &image, nil, r
 }
+
+func runCommand(command string, params ...string) error {
+	cp := exec.Command(command, params...)
+	cp.Stderr = os.Stderr
+	cp.Stdout = os.Stdout
+	return cp.Run()
+}
diff --git a/internal/store/store.go b/internal/store/store.go
index 14b792859..6d1c86c86 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -39,6 +39,7 @@ type Store struct {
 	pendingJobs  chan Job
 	stateChannel chan []byte
 	distro       distro.Distro
+	stateDir     *string
 }
 
 // A Compose represent the task of building one image. It contains all the information
@@ -110,12 +111,19 @@ func (e *InvalidRequestError) Error() string {
 	return e.message
 }
 
-func New(stateFile *string, distro distro.Distro) *Store {
+func New(stateDir *string, distro distro.Distro) *Store {
 	var s Store
 
-	if stateFile != nil {
-		state, err := ioutil.ReadFile(*stateFile)
-		if state != nil {
+	if stateDir != nil {
+		err := os.Mkdir(*stateDir+"/"+"outputs", 0755)
+		if err != nil && !os.IsExist(err) {
+			log.Fatalf("cannot create output directory")
+		}
+
+		stateFile := *stateDir + "/state.json"
+		state, err := ioutil.ReadFile(stateFile)
+
+		if err == nil {
 			err := json.Unmarshal(state, &s)
 			if err != nil {
 				log.Fatalf("invalid initial state: %v", err)
@@ -128,7 +136,7 @@ func New(stateFile *string, distro distro.Distro) *Store {
 
 		go func() {
 			for {
-				err := writeFileAtomically(*stateFile, <-s.stateChannel, 0755)
+				err := writeFileAtomically(stateFile, <-s.stateChannel, 0755)
 				if err != nil {
 					log.Fatalf("cannot write state: %v", err)
 				}
@@ -155,6 +163,7 @@ func New(stateFile *string, distro distro.Distro) *Store {
 	s.pendingJobs = make(chan Job, 200)
 
 	s.distro = distro
+	s.stateDir = stateDir
 
 	return &s
 }
@@ -407,12 +416,14 @@ func (s *Store) DeleteBlueprintFromWorkspace(name string) {
 }
 
 func (s *Store) PushCompose(composeID uuid.UUID, bp *blueprint.Blueprint, composeType string, uploadTarget *target.Target) error {
-	targets := []*target.Target{
-		target.NewLocalTarget(
+	targets := []*target.Target{}
+
+	if s.stateDir != nil {
+		targets = append(targets, target.NewLocalTarget(
 			&target.LocalTargetOptions{
-				Location: "/var/lib/osbuild-composer/outputs/" + composeID.String(),
+				Location: *s.stateDir + "/outputs/" + composeID.String(),
 			},
-		),
+		))
 	}
 
 	if uploadTarget != nil {
@@ -443,6 +454,29 @@ func (s *Store) PushCompose(composeID uuid.UUID, bp *blueprint.Blueprint, compos
 	return nil
 }
 
+func (s *Store) DeleteCompose(id uuid.UUID) error {
+	return s.change(func() error {
+		compose, exists := s.Composes[id]
+
+		if !exists {
+			return &NotFoundError{}
+		}
+
+		if compose.QueueStatus != "FINISHED" && compose.QueueStatus != "FAILED" {
+			return &InvalidRequestError{fmt.Sprintf("Compose %s is not in FINISHED or FAILED.", id)}
+		}
+
+		delete(s.Composes, id)
+
+		var err error
+		if s.stateDir != nil {
+			err = os.RemoveAll(*s.stateDir + "/outputs/" + id.String())
+		}
+
+		return err
+	})
+}
+
 func (s *Store) PopCompose() Job {
 	job := <-s.pendingJobs
 	s.change(func() error {
diff --git a/internal/weldr/api.go b/internal/weldr/api.go
index f9c266abc..6838f9c7a 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -86,6 +86,7 @@ func New(rpmmd rpmmd.RPMMD, distro distro.Distro, logger *log.Logger, store *sto
 	api.router.DELETE("/api/v:version/blueprints/workspace/:blueprint", api.blueprintDeleteWorkspaceHandler)
 
 	api.router.POST("/api/v:version/compose", api.composeHandler)
+	api.router.DELETE("/api/v:version/compose/delete/:uuids", api.composeDeleteHandler)
 	api.router.GET("/api/v:version/compose/types", api.composeTypesHandler)
 	api.router.GET("/api/v:version/compose/queue", api.composeQueueHandler)
 	api.router.GET("/api/v:version/compose/status/:uuids", api.composeStatusHandler)
@@ -1267,6 +1268,68 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
 	json.NewEncoder(writer).Encode(reply)
 }
 
+func (api *API) composeDeleteHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
+	if !verifyRequestVersion(writer, params, 0) {
+		return
+	}
+
+	type composeDeleteStatus struct {
+		UUID   uuid.UUID `json:"uuid"`
+		Status bool      `json:"status"`
+	}
+
+	type composeDeleteError struct {
+		ID  string `json:"id"`
+		Msg string `json:"msg"`
+	}
+
+	uuidsParam := params.ByName("uuids")
+
+	results := []composeDeleteStatus{}
+	errors := []composeDeleteError{}
+	uuidStrings := strings.Split(uuidsParam, ",")
+	for _, uuidString := range uuidStrings {
+		id, err := uuid.Parse(uuidString)
+		if err != nil {
+			errors = append(errors, composeDeleteError{
+				"UnknownUUID",
+				fmt.Sprintf("%s is not a valid uuid", uuidString),
+			})
+		}
+
+		err = api.store.DeleteCompose(id)
+
+		if err != nil {
+			switch err.(type) {
+			case *store.NotFoundError:
+				errors = append(errors, composeDeleteError{
+					"UnknownUUID",
+					fmt.Sprintf("compose %s doesn't exist", id),
+				})
+			case *store.InvalidRequestError:
+				errors = append(errors, composeDeleteError{
+					"BuildInWrongState",
+					err.Error(),
+				})
+			default:
+				errors = append(errors, composeDeleteError{
+					"ComposeError",
+					fmt.Sprintf("%s: %s", id, err.Error()),
+				})
+			}
+		}
+
+		results = append(results, composeDeleteStatus{id, true})
+	}
+
+	reply := struct {
+		UUIDs  []composeDeleteStatus `json:"uuids"`
+		Errors []composeDeleteError  `json:"errors"`
+	}{results, errors}
+
+	json.NewEncoder(writer).Encode(reply)
+}
+
 func (api *API) composeTypesHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
 	if !verifyRequestVersion(writer, params, 0) {
 		return
diff --git a/tools/image-info b/tools/image-info
index 4cc84effb..49c33043a 100755
--- a/tools/image-info
+++ b/tools/image-info
@@ -29,6 +29,16 @@ def nbd_connect(image):
         raise RuntimeError("no free network block device")
 
 
+@contextlib.contextmanager
+def mount_at(device, mountpoint, options=[]):
+    opts = ",".join(["ro"] + options)
+    subprocess.run(["mount", "-o", opts, device, mountpoint], check=True)
+    try:
+        yield mountpoint
+    finally:
+        subprocess.run(["umount", "--lazy", mountpoint], check=True)
+
+
 @contextlib.contextmanager
 def mount(device):
     with tempfile.TemporaryDirectory() as mountpoint:
@@ -221,6 +231,19 @@ def append_filesystem(report, tree):
         with open(f"{tree}/grub2/grubenv") as f:
             report["boot-environment"] = parse_environment_vars(f.read())
         report["bootmenu"] = read_boot_entries(tree)
+    elif len(glob.glob(f"{tree}/EFI")):
+        print("EFI partition", file=sys.stderr)
+
+
+def partition_is_esp(partition):
+    return partition["type"] == "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
+
+
+def find_esp(partitions):
+    for i, p in enumerate(partitions):
+        if partition_is_esp(p):
+            return p, i
+    return None, 0
 
 
 report = {}
@@ -230,11 +253,16 @@ with nbd_connect(image) as device:
     report["partition-table"], report["partition-table-id"], report["partitions"] = read_partition_table(device)
 
     if report["partition-table"]:
+        esp, esp_id = find_esp(report["partitions"])
         n_partitions = len(report["partitions"])
         for n in range(n_partitions):
             if report["partitions"][n]["fstype"]:
                 with mount(device + f"p{n + 1}") as tree:
-                    append_filesystem(report, tree)
+                    if esp and os.path.exists(f"{tree}/boot/efi"):
+                        with mount_at(device + f"p{esp_id + 1}", f"{tree}/boot/efi", options=['umask=077']):
+                            append_filesystem(report, tree)
+                    else:
+                        append_filesystem(report, tree)
     else:
         with mount(device) as tree:
             append_filesystem(report, tree)


Test patch:
diff --git a/internal/jobqueue/api_test.go b/internal/jobqueue/api_test.go
index 86217ff92..9fbe16a32 100644
--- a/internal/jobqueue/api_test.go
+++ b/internal/jobqueue/api_test.go
@@ -49,7 +49,7 @@ func TestCreate(t *testing.T) {
 	}
 
 	test.TestRoute(t, api, false, "POST", "/job-queue/v1/jobs", `{}`, http.StatusCreated,
-		`{"id":"ffffffff-ffff-ffff-ffff-ffffffffffff","output_type":"tar","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"},"status":"RUNNING"}]}`, "created", "uuid")
+		`{"id":"ffffffff-ffff-ffff-ffff-ffffffffffff","output_type":"tar","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":[]}`, "created", "uuid")
 }
 
 func testUpdateTransition(t *testing.T, from, to string, expectedStatus int) {
diff --git a/internal/weldr/api_test.go b/internal/weldr/api_test.go
index 36bb1da88..398b9c669 100644
--- a/internal/weldr/api_test.go
+++ b/internal/weldr/api_test.go
@@ -23,6 +23,7 @@ import (
 
 	"github.com/BurntSushi/toml"
 	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
 )
 
 func createWeldrAPI(fixtureGenerator rpmmd_mock.FixtureGenerator) (*weldr.API, *store.Store) {
@@ -259,14 +260,7 @@ func TestCompose(t *testing.T) {
 			Customizations: nil,
 		},
 		OutputType: "tar",
-		Targets: []*target.Target{
-			{
-				Name:    "org.osbuild.local",
-				Created: time.Time{},
-				Status:  "WAITING",
-				Options: &target.LocalTargetOptions{},
-			},
-		},
+		Targets:    []*target.Target{},
 	}
 
 	expectedComposeLocalAndAws := &store.Compose{
@@ -281,11 +275,6 @@ func TestCompose(t *testing.T) {
 		},
 		OutputType: "tar",
 		Targets: []*target.Target{
-			{
-				Name:    "org.osbuild.local",
-				Status:  "WAITING",
-				Options: &target.LocalTargetOptions{},
-			},
 			{
 				Name:      "org.osbuild.aws",
 				Status:    "WAITING",
@@ -342,6 +331,41 @@ func TestCompose(t *testing.T) {
 	}
 }
 
+func TestComposeDelete(t *testing.T) {
+	if len(os.Getenv("OSBUILD_COMPOSER_TEST_EXTERNAL")) > 0 {
+		t.Skip("This test is for internal testing only")
+	}
+
+	var cases = []struct {
+		Path               string
+		ExpectedJSON       string
+		ExpectedIDsInStore []string
+	}{
+		{"/api/v0/compose/delete/30000000-0000-0000-0000-000000000002", `{"uuids":[{"uuid":"30000000-0000-0000-0000-000000000002","status":true}],"errors":[]}`, []string{"30000000-0000-0000-0000-000000000000", "30000000-0000-0000-0000-000000000001", "30000000-0000-0000-0000-000000000003"}},
+		{"/api/v0/compose/delete/30000000-0000-0000-0000-000000000002,30000000-0000-0000-0000-000000000003", `{"uuids":[{"uuid":"30000000-0000-0000-0000-000000000002","status":true},{"uuid":"30000000-0000-0000-0000-000000000003","status":true}],"errors":[]}`, []string{"30000000-0000-0000-0000-000000000000", "30000000-0000-0000-0000-000000000001"}},
+		{"/api/v0/compose/delete/30000000-0000-0000-0000-000000000003,30000000-0000-0000-0000-000000000000", `{"uuids":[{"uuid":"30000000-0000-0000-0000-000000000003","status":true},{"uuid":"30000000-0000-0000-0000-000000000000","status":true}],"errors":[{"id":"BuildInWrongState","msg":"Compose 30000000-0000-0000-0000-000000000000 is not in FINISHED or FAILED."}]}`, []string{"30000000-0000-0000-0000-000000000000", "30000000-0000-0000-0000-000000000001", "30000000-0000-0000-0000-000000000002"}},
+		{"/api/v0/compose/delete/30000000-0000-0000-0000-000000000003,30000000-0000-0000-0000", `{"uuids":[{"uuid":"30000000-0000-0000-0000-000000000003","status":true},{"uuid":"00000000-0000-0000-0000-000000000000","status":true}],"errors":[{"id":"UnknownUUID","msg":"30000000-0000-0000-0000 is not a valid uuid"},{"id":"UnknownUUID","msg":"compose 00000000-0000-0000-0000-000000000000 doesn't exist"}]}`, []string{"30000000-0000-0000-0000-000000000000", "30000000-0000-0000-0000-000000000001", "30000000-0000-0000-0000-000000000002"}},
+		{"/api/v0/compose/delete/30000000-0000-0000-0000-000000000003,42000000-0000-0000-0000-000000000000", `{"uuids":[{"uuid":"30000000-0000-0000-0000-000000000003","status":true},{"uuid":"42000000-0000-0000-0000-000000000000","status":true}],"errors":[{"id":"UnknownUUID","msg":"compose 42000000-0000-0000-0000-000000000000 doesn't exist"}]}`, []string{"30000000-0000-0000-0000-000000000000", "30000000-0000-0000-0000-000000000001", "30000000-0000-0000-0000-000000000002"}},
+	}
+
+	for _, c := range cases {
+		api, s := createWeldrAPI(rpmmd_mock.BaseFixture)
+		test.TestRoute(t, api, false, "DELETE", c.Path, "", http.StatusOK, c.ExpectedJSON)
+
+		idsInStore := []string{}
+
+		for id, _ := range s.Composes {
+			idsInStore = append(idsInStore, id.String())
+		}
+
+		diff := cmp.Diff(idsInStore, c.ExpectedIDsInStore, cmpopts.SortSlices(func(a, b string) bool { return a < b }))
+
+		if diff != "" {
+			t.Errorf("%s: composes in store are different, expected: %v, got: %v, diff:\n%s", c.Path, c.ExpectedIDsInStore, idsInStore, diff)
+		}
+	}
+}
+
 func TestComposeStatus(t *testing.T) {
 	var cases = []struct {
 		Fixture        rpmmd_mock.FixtureGenerator