store: add image struct into compose struct

As a part of f4991cb1 ComposeEntry struct was removed from store package.
This change made sense because this struct is connected more with API than
with store - store uses its own Compose struct. In addition, converters
between Compose and ComposeEntry were added. Unfortunately, ComposeEntry
contains ImageSize which was not stored in Compose but retrieved from store
using GetImage method. This made those converters dependent on the store,
which was messy.

To solve this issue this commit adds image struct into Compose struct.
The content of image struct is generated on the worker side - when the worker
sets the compose status to FINISHED, it also sends Image struct with detailed
information about the result.
This commit is contained in:
Ondřej Budai 2019-12-03 15:05:46 +01:00 committed by Lars Karlitski
parent b2880cacc6
commit f89a9671be
7 changed files with 127 additions and 88 deletions

View file

@ -11,6 +11,7 @@ import (
"github.com/osbuild/osbuild-composer/internal/distro" "github.com/osbuild/osbuild-composer/internal/distro"
"github.com/osbuild/osbuild-composer/internal/jobqueue" "github.com/osbuild/osbuild-composer/internal/jobqueue"
"github.com/osbuild/osbuild-composer/internal/store"
) )
type ComposerClient struct { type ComposerClient struct {
@ -53,9 +54,9 @@ func (c *ComposerClient) AddJob() (*jobqueue.Job, error) {
return job, nil return job, nil
} }
func (c *ComposerClient) UpdateJob(job *jobqueue.Job, status string) error { func (c *ComposerClient) UpdateJob(job *jobqueue.Job, status string, image *store.Image) error {
var b bytes.Buffer var b bytes.Buffer
json.NewEncoder(&b).Encode(&jobqueue.JobStatus{status}) json.NewEncoder(&b).Encode(&jobqueue.JobStatus{status, image})
req, err := http.NewRequest("PATCH", "http://localhost/job-queue/v1/jobs/"+job.ID.String(), &b) req, err := http.NewRequest("PATCH", "http://localhost/job-queue/v1/jobs/"+job.ID.String(), &b)
if err != nil { if err != nil {
return err return err
@ -82,23 +83,23 @@ func handleJob(client *ComposerClient, distro distro.Distro) {
panic(err) panic(err)
} }
client.UpdateJob(job, "RUNNING") client.UpdateJob(job, "RUNNING", nil)
fmt.Printf("Running job %s\n", job.ID.String()) fmt.Printf("Running job %s\n", job.ID.String())
err, errs := job.Run(distro) image, err, errs := job.Run(distro)
if err != nil { if err != nil {
client.UpdateJob(job, "FAILED") client.UpdateJob(job, "FAILED", nil)
return return
} }
for _, err := range errs { for _, err := range errs {
if err != nil { if err != nil {
client.UpdateJob(job, "FAILED") client.UpdateJob(job, "FAILED", nil)
return return
} }
} }
client.UpdateJob(job, "FINISHED") client.UpdateJob(job, "FINISHED", image)
} }
func main() { func main() {

View file

@ -93,12 +93,10 @@ func (api *API) addJobHandler(writer http.ResponseWriter, request *http.Request,
nextJob := api.store.PopCompose() nextJob := api.store.PopCompose()
writer.WriteHeader(http.StatusCreated) writer.WriteHeader(http.StatusCreated)
json.NewEncoder(writer).Encode(replyBody{nextJob.ComposeID, nextJob.Pipeline, nextJob.Targets}) json.NewEncoder(writer).Encode(replyBody{nextJob.ComposeID, nextJob.Pipeline, nextJob.Targets, nextJob.OutputType})
} }
func (api *API) updateJobHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { func (api *API) updateJobHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
type requestBody JobStatus
contentType := request.Header["Content-Type"] contentType := request.Header["Content-Type"]
if len(contentType) != 1 || contentType[0] != "application/json" { if len(contentType) != 1 || contentType[0] != "application/json" {
statusResponseError(writer, http.StatusUnsupportedMediaType) statusResponseError(writer, http.StatusUnsupportedMediaType)
@ -111,14 +109,14 @@ func (api *API) updateJobHandler(writer http.ResponseWriter, request *http.Reque
return return
} }
var body requestBody var body JobStatus
err = json.NewDecoder(request.Body).Decode(&body) err = json.NewDecoder(request.Body).Decode(&body)
if err != nil { if err != nil {
statusResponseError(writer, http.StatusBadRequest, "invalid status: "+err.Error()) statusResponseError(writer, http.StatusBadRequest, "invalid status: "+err.Error())
return return
} }
err = api.store.UpdateCompose(id, body.Status) err = api.store.UpdateCompose(id, body.Status, body.Image)
if err != nil { if err != nil {
switch err.(type) { switch err.(type) {
case *store.NotFoundError: case *store.NotFoundError:

View file

@ -49,7 +49,7 @@ func TestCreate(t *testing.T) {
} }
test.TestRoute(t, api, false, "POST", "/job-queue/v1/jobs", `{}`, http.StatusCreated, 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":[{"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":[{"image_name":"","name":"org.osbuild.local","options":{"location":"/var/lib/osbuild-composer/outputs/ffffffff-ffff-ffff-ffff-ffffffffffff"},"status":"RUNNING"}]}`, "created", "uuid")
} }
func testUpdateTransition(t *testing.T, from, to string, expectedStatus int) { func testUpdateTransition(t *testing.T, from, to string, expectedStatus int) {

View file

@ -8,36 +8,40 @@ import (
"os/exec" "os/exec"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/osbuild/osbuild-composer/internal/distro" "github.com/osbuild/osbuild-composer/internal/distro"
"github.com/osbuild/osbuild-composer/internal/pipeline" "github.com/osbuild/osbuild-composer/internal/pipeline"
"github.com/osbuild/osbuild-composer/internal/store"
"github.com/osbuild/osbuild-composer/internal/target" "github.com/osbuild/osbuild-composer/internal/target"
"github.com/osbuild/osbuild-composer/internal/upload/awsupload" "github.com/osbuild/osbuild-composer/internal/upload/awsupload"
) )
type Job struct { type Job struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
Pipeline *pipeline.Pipeline `json:"pipeline"` Pipeline *pipeline.Pipeline `json:"pipeline"`
Targets []*target.Target `json:"targets"` Targets []*target.Target `json:"targets"`
OutputType string `json:"output_type"`
} }
type JobStatus struct { type JobStatus struct {
Status string `json:"status"` Status string `json:"status"`
Image *store.Image `json:"image"`
} }
func (job *Job) Run(d distro.Distro) (error, []error) { func (job *Job) Run(d distro.Distro) (*store.Image, error, []error) {
build := pipeline.Build{ build := pipeline.Build{
Runner: d.Runner(), Runner: d.Runner(),
} }
buildFile, err := ioutil.TempFile("", "osbuild-worker-build-env-*") buildFile, err := ioutil.TempFile("", "osbuild-worker-build-env-*")
if err != nil { if err != nil {
return err, nil return nil, err, nil
} }
defer os.Remove(buildFile.Name()) defer os.Remove(buildFile.Name())
err = json.NewEncoder(buildFile).Encode(build) err = json.NewEncoder(buildFile).Encode(build)
if err != nil { if err != nil {
return err, nil return nil, err, nil
} }
cmd := exec.Command( cmd := exec.Command(
@ -50,22 +54,22 @@ func (job *Job) Run(d distro.Distro) (error, []error) {
stdin, err := cmd.StdinPipe() stdin, err := cmd.StdinPipe()
if err != nil { if err != nil {
return err, nil return nil, err, nil
} }
stdout, err := cmd.StdoutPipe() stdout, err := cmd.StdoutPipe()
if err != nil { if err != nil {
return err, nil return nil, err, nil
} }
err = cmd.Start() err = cmd.Start()
if err != nil { if err != nil {
return err, nil return nil, err, nil
} }
err = json.NewEncoder(stdin).Encode(job.Pipeline) err = json.NewEncoder(stdin).Encode(job.Pipeline)
if err != nil { if err != nil {
return err, nil return nil, err, nil
} }
stdin.Close() stdin.Close()
@ -75,14 +79,21 @@ func (job *Job) Run(d distro.Distro) (error, []error) {
} }
err = json.NewDecoder(stdout).Decode(&result) err = json.NewDecoder(stdout).Decode(&result)
if err != nil { if err != nil {
return err, nil return nil, err, nil
} }
err = cmd.Wait() err = cmd.Wait()
if err != nil { if err != nil {
return err, nil return nil, err, nil
} }
filename, mimeType, err := d.FilenameFromType(job.OutputType)
if err != nil {
return nil, err, nil
}
var image store.Image
var r []error var r []error
for _, t := range job.Targets { for _, t := range job.Targets {
@ -102,6 +113,25 @@ func (job *Job) Run(d distro.Distro) (error, []error) {
r = append(r, err) r = append(r, err)
continue continue
} }
imagePath := options.Location + "/" + filename
file, err := os.Open(imagePath)
if err != nil {
r = append(r, err)
continue
}
fileStat, err := file.Stat()
if err != nil {
return nil, err, nil
}
image = store.Image{
Path: imagePath,
Mime: mimeType,
Size: fileStat.Size(),
}
case *target.AWSTargetOptions: case *target.AWSTargetOptions:
a, err := awsupload.New(options.Region, options.AccessKeyID, options.SecretAccessKey) a, err := awsupload.New(options.Region, options.AccessKeyID, options.SecretAccessKey)
if err != nil { if err != nil {
@ -132,5 +162,5 @@ func (job *Job) Run(d distro.Distro) (error, []error) {
r = append(r, nil) r = append(r, nil)
} }
return nil, r return &image, nil, r
} }

View file

@ -51,19 +51,20 @@ type Compose struct {
JobCreated time.Time `json:"job_created"` JobCreated time.Time `json:"job_created"`
JobStarted time.Time `json:"job_started"` JobStarted time.Time `json:"job_started"`
JobFinished time.Time `json:"job_finished"` JobFinished time.Time `json:"job_finished"`
Image *Image `json:"image"`
} }
// A Job contains the information about a compose a worker needs to process it. // A Job contains the information about a compose a worker needs to process it.
type Job struct { type Job struct {
ComposeID uuid.UUID ComposeID uuid.UUID
Pipeline *pipeline.Pipeline Pipeline *pipeline.Pipeline
Targets []*target.Target Targets []*target.Target
OutputType string
} }
// An Image represents the image resulting from a compose. // An Image represents the image resulting from a compose.
type Image struct { type Image struct {
File *os.File Path string
Name string
Mime string Mime string
Size int64 Size int64
} }
@ -223,6 +224,14 @@ func (s *Store) ListBlueprints() []string {
return names return names
} }
func (s *Store) GetCompose(id uuid.UUID) (Compose, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
compose, exists := s.Composes[id]
return compose, exists
}
func (s *Store) GetAllComposes() map[uuid.UUID]Compose { func (s *Store) GetAllComposes() map[uuid.UUID]Compose {
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()
@ -431,9 +440,10 @@ func (s *Store) PushCompose(composeID uuid.UUID, bp *blueprint.Blueprint, compos
return nil return nil
}) })
s.pendingJobs <- Job{ s.pendingJobs <- Job{
ComposeID: composeID, ComposeID: composeID,
Pipeline: pipeline, Pipeline: pipeline,
Targets: targets, Targets: targets,
OutputType: composeType,
} }
return nil return nil
@ -457,7 +467,7 @@ func (s *Store) PopCompose() Job {
return job return job
} }
func (s *Store) UpdateCompose(composeID uuid.UUID, status string) error { func (s *Store) UpdateCompose(composeID uuid.UUID, status string, image *Image) error {
return s.change(func() error { return s.change(func() error {
compose, exists := s.Composes[composeID] compose, exists := s.Composes[composeID]
if !exists { if !exists {
@ -484,6 +494,11 @@ func (s *Store) UpdateCompose(composeID uuid.UUID, status string) error {
for _, t := range compose.Targets { for _, t := range compose.Targets {
t.Status = status t.Status = status
} }
if status == "FINISHED" {
compose.Image = image
}
s.Composes[composeID] = compose s.Composes[composeID] = compose
default: default:
return &InvalidRequestError{"invalid state transition"} return &InvalidRequestError{"invalid state transition"}
@ -492,44 +507,6 @@ func (s *Store) UpdateCompose(composeID uuid.UUID, status string) error {
}) })
} }
func (s *Store) GetImage(composeID uuid.UUID) (*Image, error) {
s.mu.RLock()
defer s.mu.RUnlock()
if compose, exists := s.Composes[composeID]; exists {
if compose.QueueStatus != "FINISHED" {
return nil, &InvalidRequestError{"compose was not finished"}
}
name, mime, err := s.distro.FilenameFromType(compose.OutputType)
if err != nil {
panic("invalid output type")
}
for _, t := range compose.Targets {
switch options := t.Options.(type) {
case *target.LocalTargetOptions:
file, err := os.Open(options.Location + "/" + name)
if err == nil {
fileStat, err := file.Stat()
if err != nil {
return nil, &NotFoundError{"image info could not be found"}
}
size := fileStat.Size()
return &Image{
File: file,
Name: name,
Mime: mime,
Size: size,
}, nil
}
}
}
return nil, &NotFoundError{"image could not be found"}
}
return nil, &NotFoundError{"compose could not be found"}
}
func (s *Store) PushSource(source SourceConfig) { func (s *Store) PushSource(source SourceConfig) {
s.change(func() error { s.change(func() error {
s.Sources[source.Name] = source s.Sources[source.Name] = source

View file

@ -9,6 +9,8 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"os"
"path/filepath"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@ -1370,21 +1372,51 @@ func (api *API) composeImageHandler(writer http.ResponseWriter, request *http.Re
return return
} }
image, err := api.store.GetImage(uuid) compose, exists := api.store.GetCompose(uuid)
if err != nil { if !exists {
errors := responseError{ errors := responseError{
ID: "BuildMissingFile", ID: "BuildMissingFile",
Msg: fmt.Sprintf("Build %s is missing image file %s", uuidString, image.Name), Msg: fmt.Sprintf("Compose %s doesn't exist", uuidString),
} }
statusResponseError(writer, http.StatusBadRequest, errors) statusResponseError(writer, http.StatusBadRequest, errors)
return return
} }
writer.Header().Set("Content-Disposition", "attachment; filename="+uuid.String()+"-"+image.Name) if compose.QueueStatus != "FINISHED" {
writer.Header().Set("Content-Type", image.Mime) errors := responseError{
writer.Header().Set("Content-Length", fmt.Sprintf("%d", image.Size)) ID: "BuildInWrongState",
Msg: fmt.Sprintf("Build %s is in wrong state: %s", uuidString, compose.QueueStatus),
}
statusResponseError(writer, http.StatusBadRequest, errors)
return
}
io.Copy(writer, image.File) if compose.Image == nil {
errors := responseError{
ID: "BuildMissingFile",
Msg: fmt.Sprintf("Compose %s doesn't have an image assigned", uuidString),
}
statusResponseError(writer, http.StatusBadRequest, errors)
return
}
imageName := filepath.Base(compose.Image.Path)
file, err := os.Open(compose.Image.Path)
if err != nil {
errors := responseError{
ID: "BuildMissingFile",
Msg: fmt.Sprintf("Build %s is missing file %s!", uuidString, imageName),
}
statusResponseError(writer, http.StatusBadRequest, errors)
return
}
writer.Header().Set("Content-Disposition", "attachment; filename="+uuid.String()+"-"+imageName)
writer.Header().Set("Content-Type", compose.Image.Mime)
writer.Header().Set("Content-Length", fmt.Sprintf("%d", compose.Image.Size))
io.Copy(writer, file)
} }
func (api *API) composeFinishedHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { func (api *API) composeFinishedHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {

View file

@ -3,6 +3,7 @@ package weldr
import ( import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/osbuild/osbuild-composer/internal/store" "github.com/osbuild/osbuild-composer/internal/store"
"log"
"sort" "sort"
) )
@ -41,13 +42,13 @@ func composeToComposeEntry(id uuid.UUID, compose store.Compose, includeUploads b
composeEntry.JobStarted = float64(compose.JobStarted.UnixNano()) / 1000000000 composeEntry.JobStarted = float64(compose.JobStarted.UnixNano()) / 1000000000
case "FINISHED": case "FINISHED":
//image, err := s.GetImage(id) if compose.Image != nil {
//imageSize := int64(0) composeEntry.ImageSize = compose.Image.Size
//if err == nil { } else {
// imageSize = image.Size log.Printf("finished compose with id %s has nil image\n", id.String())
//} composeEntry.ImageSize = 0
// TODO: this is currently broken! }
composeEntry.ImageSize = int64(0)
composeEntry.JobCreated = float64(compose.JobCreated.UnixNano()) / 1000000000 composeEntry.JobCreated = float64(compose.JobCreated.UnixNano()) / 1000000000
composeEntry.JobStarted = float64(compose.JobStarted.UnixNano()) / 1000000000 composeEntry.JobStarted = float64(compose.JobStarted.UnixNano()) / 1000000000
composeEntry.JobFinished = float64(compose.JobFinished.UnixNano()) / 1000000000 composeEntry.JobFinished = float64(compose.JobFinished.UnixNano()) / 1000000000