api: Add support for upload API

This commit introduces basic support for upload API. Currently, all the routes
required by cockpit-composer are supported (except for /compose/log).

Also, ComposeEntry struct is moved outside of the store package. I decided
to do it because it isn't connected in any way to store, it's more connected
to API. Due to this move there's currently a known bug that image size is
not returned. This should be solved by moving Image struct inside Compose
struct by follow-up PR.
This commit is contained in:
Ondřej Budai 2019-11-28 15:15:27 +01:00 committed by Tom Gundersen
parent be1cf79d6a
commit f4991cb1ca
7 changed files with 570 additions and 171 deletions

View file

@ -43,13 +43,13 @@ func TestCreate(t *testing.T) {
store := store.New(nil, distro.New("fedora-30"))
api := jobqueue.New(nil, store)
err := store.PushCompose(id, &blueprint.Blueprint{}, "tar")
err := store.PushCompose(id, &blueprint.Blueprint{}, "tar", nil)
if err != nil {
t.Fatalf("error pushing compose: %v", err)
}
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"}}]}`)
`{"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")
}
func testUpdateTransition(t *testing.T, from, to string, expectedStatus int) {
@ -58,7 +58,7 @@ func testUpdateTransition(t *testing.T, from, to string, expectedStatus int) {
api := jobqueue.New(nil, store)
if from != "VOID" {
err := store.PushCompose(id, &blueprint.Blueprint{}, "tar")
err := store.PushCompose(id, &blueprint.Blueprint{}, "tar", nil)
if err != nil {
t.Fatalf("error pushing compose: %v", err)
}

View file

@ -10,6 +10,7 @@ import (
"github.com/osbuild/osbuild-composer/internal/distro"
"github.com/osbuild/osbuild-composer/internal/rpmmd"
"github.com/osbuild/osbuild-composer/internal/store"
"github.com/osbuild/osbuild-composer/internal/target"
)
type FixtureGenerator func() Fixture
@ -54,44 +55,78 @@ func generatePackageList() rpmmd.PackageList {
func createBaseStoreFixture() *store.Store {
var bName = "test"
var b = blueprint.Blueprint{Name: bName, Version: "0.0.0"}
var b = blueprint.Blueprint{
Name: bName,
Version: "0.0.0",
Packages: []blueprint.Package{},
Modules: []blueprint.Package{},
Groups: []blueprint.Group{},
Customizations: nil,
}
var date = time.Date(2019, 11, 27, 13, 19, 0, 0, time.FixedZone("UTC+1", 60*60))
var localTarget = &target.Target{
Uuid: uuid.MustParse("20000000-0000-0000-0000-000000000000"),
Name: "org.osbuild.local",
ImageName: "localimage",
Created: date,
Status: "WAITING",
Options: &target.LocalTargetOptions{
Location: "/tmp/localimage",
},
}
var awsTarget = &target.Target{
Uuid: uuid.MustParse("10000000-0000-0000-0000-000000000000"),
Name: "org.osbuild.aws",
ImageName: "awsimage",
Created: date,
Status: "WAITING",
Options: &target.AWSTargetOptions{
Region: "frankfurt",
AccessKeyID: "accesskey",
SecretAccessKey: "secretkey",
Bucket: "clay",
Key: "imagekey",
},
}
d := distro.New("fedora-30")
s := store.New(nil, d)
s.Blueprints[bName] = b
s.Composes = map[uuid.UUID]store.Compose{
uuid.MustParse("e65f76f8-b0d9-4974-9dd7-745ae80b4721"): store.Compose{
uuid.MustParse("30000000-0000-0000-0000-000000000000"): store.Compose{
QueueStatus: "WAITING",
Blueprint: &b,
OutputType: "tar",
Targets: nil,
Targets: []*target.Target{localTarget, awsTarget},
JobCreated: date,
},
uuid.MustParse("e65f76f8-b0d9-4974-9dd7-745ae80b4722"): store.Compose{
uuid.MustParse("30000000-0000-0000-0000-000000000001"): store.Compose{
QueueStatus: "RUNNING",
Blueprint: &b,
OutputType: "tar",
Targets: nil,
Targets: []*target.Target{localTarget},
JobCreated: date,
JobStarted: date,
},
uuid.MustParse("e65f76f8-b0d9-4974-9dd7-745ae80b4723"): store.Compose{
uuid.MustParse("30000000-0000-0000-0000-000000000002"): store.Compose{
QueueStatus: "FINISHED",
Blueprint: &b,
OutputType: "tar",
Targets: nil,
Targets: []*target.Target{localTarget, awsTarget},
JobCreated: date,
JobStarted: date,
JobFinished: date,
},
uuid.MustParse("e65f76f8-b0d9-4974-9dd7-745ae80b4724"): store.Compose{
uuid.MustParse("30000000-0000-0000-0000-000000000003"): store.Compose{
QueueStatus: "FAILED",
Blueprint: &b,
OutputType: "tar",
Targets: nil,
Targets: []*target.Target{localTarget, awsTarget},
JobCreated: date,
JobStarted: date,
JobFinished: date,
@ -101,6 +136,44 @@ func createBaseStoreFixture() *store.Store {
return s
}
func createBaseDepsolveFixture() []rpmmd.PackageSpec {
return []rpmmd.PackageSpec{
{
Name: "dep-package1",
Epoch: 0,
Version: "1.33",
Release: "2.fc30",
Arch: "x86_64",
},
{
Name: "dep-package2",
Epoch: 0,
Version: "2.9",
Release: "1.fc30",
Arch: "x86_64",
},
}
}
func createStoreWithoutComposesFixture() *store.Store {
var bName = "test"
var b = blueprint.Blueprint{
Name: bName,
Version: "0.0.0",
Packages: []blueprint.Package{},
Modules: []blueprint.Package{},
Groups: []blueprint.Group{},
Customizations: nil,
}
d := distro.New("fedora-30")
s := store.New(nil, d)
s.Blueprints[bName] = b
return s
}
func BaseFixture() Fixture {
return Fixture{
fetchPackageList{
@ -108,28 +181,27 @@ func BaseFixture() Fixture {
nil,
},
depsolve{
[]rpmmd.PackageSpec{
{
Name: "dep-package1",
Epoch: 0,
Version: "1.33",
Release: "2.fc30",
Arch: "x86_64",
},
{
Name: "dep-package2",
Epoch: 0,
Version: "2.9",
Release: "1.fc30",
Arch: "x86_64",
},
},
createBaseDepsolveFixture(),
nil,
},
createBaseStoreFixture(),
}
}
func NoComposesFixture() Fixture {
return Fixture{
fetchPackageList{
generatePackageList(),
nil,
},
depsolve{
createBaseDepsolveFixture(),
nil,
},
createStoreWithoutComposesFixture(),
}
}
func NonExistingPackage() Fixture {
return Fixture{
fetchPackageList{

View file

@ -220,95 +220,26 @@ func (s *Store) ListBlueprints() []string {
return names
}
type ComposeEntry struct {
ID uuid.UUID `json:"id"`
Blueprint string `json:"blueprint"`
Version string `json:"version"`
ComposeType string `json:"compose_type"`
ImageSize int64 `json:"image_size"`
QueueStatus string `json:"queue_status"`
JobCreated float64 `json:"job_created"`
JobStarted float64 `json:"job_started,omitempty"`
JobFinished float64 `json:"job_finished,omitempty"`
}
func (s *Store) ListQueue(uuids []uuid.UUID) []*ComposeEntry {
func (s *Store) GetAllComposes() map[uuid.UUID]Compose {
s.mu.RLock()
defer s.mu.RUnlock()
newCompose := func(id uuid.UUID, compose Compose) *ComposeEntry {
switch compose.QueueStatus {
case "WAITING":
return &ComposeEntry{
ID: id,
Blueprint: compose.Blueprint.Name,
Version: compose.Blueprint.Version,
ComposeType: compose.OutputType,
QueueStatus: compose.QueueStatus,
JobCreated: float64(compose.JobCreated.UnixNano()) / 1000000000,
}
case "RUNNING":
return &ComposeEntry{
ID: id,
Blueprint: compose.Blueprint.Name,
Version: compose.Blueprint.Version,
ComposeType: compose.OutputType,
QueueStatus: compose.QueueStatus,
JobCreated: float64(compose.JobCreated.UnixNano()) / 1000000000,
JobStarted: float64(compose.JobStarted.UnixNano()) / 1000000000,
}
case "FINISHED":
image, err := s.GetImage(id)
imageSize := int64(0)
if err == nil {
imageSize = image.Size
}
return &ComposeEntry{
ID: id,
Blueprint: compose.Blueprint.Name,
Version: compose.Blueprint.Version,
ComposeType: compose.OutputType,
ImageSize: imageSize,
QueueStatus: compose.QueueStatus,
JobCreated: float64(compose.JobCreated.UnixNano()) / 1000000000,
JobStarted: float64(compose.JobStarted.UnixNano()) / 1000000000,
JobFinished: float64(compose.JobFinished.UnixNano()) / 1000000000,
}
case "FAILED":
return &ComposeEntry{
ID: id,
Blueprint: compose.Blueprint.Name,
Version: compose.Blueprint.Version,
ComposeType: compose.OutputType,
QueueStatus: compose.QueueStatus,
JobCreated: float64(compose.JobCreated.UnixNano()) / 1000000000,
JobStarted: float64(compose.JobStarted.UnixNano()) / 1000000000,
JobFinished: float64(compose.JobFinished.UnixNano()) / 1000000000,
}
default:
panic("invalid compose state")
}
}
composes := make(map[uuid.UUID]Compose)
var composes []*ComposeEntry
if uuids == nil {
composes = make([]*ComposeEntry, 0, len(s.Composes))
for id, compose := range s.Composes {
composes = append(composes, newCompose(id, compose))
}
} else {
composes = make([]*ComposeEntry, 0, len(uuids))
for _, id := range uuids {
if compose, exists := s.Composes[id]; exists {
composes = append(composes, newCompose(id, compose))
}
}
}
for id, compose := range s.Composes {
newCompose := compose
newCompose.Targets = []*target.Target{}
// make this function output more predictable
sort.Slice(composes, func(i, j int) bool {
return composes[i].ID.String() < composes[j].ID.String()
})
for _, t := range compose.Targets {
newTarget := *t
newCompose.Targets = append(newCompose.Targets, &newTarget)
}
newBlueprint := *compose.Blueprint
newCompose.Blueprint = &newBlueprint
composes[id] = newCompose
}
return composes
}
@ -448,7 +379,7 @@ func (s *Store) DeleteBlueprintFromWorkspace(name string) {
})
}
func (s *Store) PushCompose(composeID uuid.UUID, bp *blueprint.Blueprint, composeType string) error {
func (s *Store) PushCompose(composeID uuid.UUID, bp *blueprint.Blueprint, composeType string, uploadTarget *target.Target) error {
targets := []*target.Target{
target.NewLocalTarget(
&target.LocalTargetOptions{
@ -456,6 +387,11 @@ func (s *Store) PushCompose(composeID uuid.UUID, bp *blueprint.Blueprint, compos
},
),
}
if uploadTarget != nil {
targets = append(targets, uploadTarget)
}
pipeline, err := s.distro.Pipeline(bp, composeType)
if err != nil {
return err
@ -488,6 +424,9 @@ func (s *Store) PopCompose() Job {
}
compose.JobStarted = time.Now()
compose.QueueStatus = "RUNNING"
for _, t := range compose.Targets {
t.Status = "RUNNING"
}
s.Composes[job.ComposeID] = compose
return nil
})
@ -518,6 +457,9 @@ func (s *Store) UpdateCompose(composeID uuid.UUID, status string) error {
return &NotRunningError{"compose was not running"}
}
compose.QueueStatus = status
for _, t := range compose.Targets {
t.Status = status
}
s.Composes[composeID] = compose
default:
return &InvalidRequestError{"invalid state transition"}

View file

@ -15,10 +15,10 @@ import (
"github.com/julienschmidt/httprouter"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/distro"
"github.com/osbuild/osbuild-composer/internal/rpmmd"
"github.com/osbuild/osbuild-composer/internal/store"
"github.com/osbuild/osbuild-composer/internal/distro"
"github.com/osbuild/osbuild-composer/internal/target"
)
type API struct {
@ -137,6 +137,18 @@ func verifyRequestVersion(writer http.ResponseWriter, params httprouter.Params,
return true
}
func isRequestVersionAtLeast(params httprouter.Params, minVersion uint) bool {
versionString := params.ByName("version")
version, err := strconv.ParseUint(versionString, 10, 0)
if err != nil {
panic(err)
}
return uint(version) >= minVersion
}
func methodNotAllowedHandler(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusMethodNotAllowed)
}
@ -1110,15 +1122,16 @@ func (api *API) blueprintDeleteWorkspaceHandler(writer http.ResponseWriter, requ
// Schedule new compose by first translating the appropriate blueprint into a pipeline and then
// pushing it into the channel for waiting builds.
func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
if !verifyRequestVersion(writer, params, 0) { // TODO: version 1 API
if !verifyRequestVersion(writer, params, 0) {
return
}
// https://weldr.io/lorax/pylorax.api.html#pylorax.api.v0.v0_compose_start
type ComposeRequest struct {
BlueprintName string `json:"blueprint_name"`
ComposeType string `json:"compose_type"`
Branch string `json:"branch"`
BlueprintName string `json:"blueprint_name"`
ComposeType string `json:"compose_type"`
Branch string `json:"branch"`
Upload *UploadRequest `json:"upload"`
}
type ComposeReply struct {
BuildID uuid.UUID `json:"build_id"`
@ -1152,12 +1165,27 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
Status: true,
}
var uploadTarget *target.Target
if isRequestVersionAtLeast(params, 1) && cr.Upload != nil {
uploadTarget, err = UploadRequestToTarget(*cr.Upload)
if err != nil {
errors := responseError{
ID: "UploadError",
Msg: fmt.Sprintf("bad input format: %s", err.Error()),
}
statusResponseError(writer, http.StatusBadRequest, errors)
return
}
}
bp := blueprint.Blueprint{}
changed := false
found := api.store.GetBlueprint(cr.BlueprintName, &bp, &changed) // TODO: what to do with changed?
if found {
err := api.store.PushCompose(reply.BuildID, &bp, cr.ComposeType)
err := api.store.PushCompose(reply.BuildID, &bp, cr.ComposeType, uploadTarget)
// TODO: we should probably do some kind of blueprint validation in future
// for now, let's just 500 and bail out
@ -1183,7 +1211,7 @@ func (api *API) composeHandler(writer http.ResponseWriter, request *http.Request
}
func (api *API) composeTypesHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
if !verifyRequestVersion(writer, params, 0) { // TODO: version 1 API
if !verifyRequestVersion(writer, params, 0) {
return
}
type composeType struct {
@ -1203,24 +1231,22 @@ func (api *API) composeTypesHandler(writer http.ResponseWriter, request *http.Re
}
func (api *API) composeQueueHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
if !verifyRequestVersion(writer, params, 0) { // TODO: version 1 API
if !verifyRequestVersion(writer, params, 0) {
return
}
var reply struct {
New []*store.ComposeEntry `json:"new"`
Run []*store.ComposeEntry `json:"run"`
New []*ComposeEntry `json:"new"`
Run []*ComposeEntry `json:"run"`
}
reply.New = make([]*store.ComposeEntry, 0)
reply.Run = make([]*store.ComposeEntry, 0)
for _, entry := range api.store.ListQueue(nil) {
switch entry.QueueStatus {
composes := api.store.GetAllComposes()
for id, compose := range composes {
switch compose.QueueStatus {
case "WAITING":
reply.New = append(reply.New, entry)
reply.New = append(reply.New, composeToComposeEntry(id, compose, isRequestVersionAtLeast(params, 1)))
case "RUNNING":
reply.Run = append(reply.Run, entry)
reply.Run = append(reply.Run, composeToComposeEntry(id, compose, isRequestVersionAtLeast(params, 1)))
}
}
@ -1229,12 +1255,12 @@ func (api *API) composeQueueHandler(writer http.ResponseWriter, request *http.Re
func (api *API) composeStatusHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
// TODO: lorax has some params: /api/v0/compose/status/<uuids>[?blueprint=<blueprint_name>&status=<compose_status>&type=<compose_type>]
if !verifyRequestVersion(writer, params, 0) { // TODO: version 1 API
if !verifyRequestVersion(writer, params, 0) {
return
}
var reply struct {
UUIDs []*store.ComposeEntry `json:"uuids"`
UUIDs []*ComposeEntry `json:"uuids"`
}
uuidsParam := params.ByName("uuids")
@ -1257,7 +1283,9 @@ func (api *API) composeStatusHandler(writer http.ResponseWriter, request *http.R
uuids = append(uuids, id)
}
}
reply.UUIDs = api.store.ListQueue(uuids)
composes := api.store.GetAllComposes()
reply.UUIDs = composesToComposeEntries(composes, uuids, isRequestVersionAtLeast(params, 1))
json.NewEncoder(writer).Encode(reply)
}
@ -1305,29 +1333,41 @@ func (api *API) composeImageHandler(writer http.ResponseWriter, request *http.Re
}
func (api *API) composeFinishedHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
if !verifyRequestVersion(writer, params, 0) { // TODO: version 1 API
if !verifyRequestVersion(writer, params, 0) {
return
}
var reply struct {
Finished []interface{} `json:"finished"`
Finished []*ComposeEntry `json:"finished"`
}
reply.Finished = make([]interface{}, 0)
composes := api.store.GetAllComposes()
for _, entry := range composesToComposeEntries(composes, nil, isRequestVersionAtLeast(params, 1)) {
switch entry.QueueStatus {
case "FINISHED":
reply.Finished = append(reply.Finished, entry)
}
}
json.NewEncoder(writer).Encode(reply)
}
func (api *API) composeFailedHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
if !verifyRequestVersion(writer, params, 0) { // TODO: version 1 API
if !verifyRequestVersion(writer, params, 0) {
return
}
var reply struct {
Failed []interface{} `json:"failed"`
Failed []*ComposeEntry `json:"failed"`
}
reply.Failed = make([]interface{}, 0)
composes := api.store.GetAllComposes()
for _, entry := range composesToComposeEntries(composes, nil, isRequestVersionAtLeast(params, 1)) {
switch entry.QueueStatus {
case "FAILED":
reply.Failed = append(reply.Failed, entry)
}
}
json.NewEncoder(writer).Encode(reply)
}

View file

@ -8,20 +8,24 @@ import (
"testing"
"time"
"github.com/osbuild/osbuild-composer/internal/blueprint"
"github.com/osbuild/osbuild-composer/internal/distro"
_ "github.com/osbuild/osbuild-composer/internal/distro/test"
rpmmd_mock "github.com/osbuild/osbuild-composer/internal/mocks/rpmmd"
"github.com/osbuild/osbuild-composer/internal/store"
"github.com/osbuild/osbuild-composer/internal/target"
"github.com/osbuild/osbuild-composer/internal/test"
"github.com/osbuild/osbuild-composer/internal/weldr"
"github.com/osbuild/osbuild-composer/internal/distro"
_ "github.com/osbuild/osbuild-composer/internal/distro/test"
"github.com/google/go-cmp/cmp"
)
func createWeldrAPI(fixtureGenerator rpmmd_mock.FixtureGenerator) *weldr.API {
func createWeldrAPI(fixtureGenerator rpmmd_mock.FixtureGenerator) (*weldr.API, *store.Store) {
fixture := fixtureGenerator()
rpm := rpmmd_mock.NewRPMMDMock(fixture)
d := distro.New("test")
return weldr.New(rpm, d, nil, fixture.Store)
return weldr.New(rpm, d, nil, fixture.Store), fixture.Store
}
func TestBasic(t *testing.T) {
@ -46,7 +50,7 @@ func TestBasic(t *testing.T) {
}
for _, c := range cases {
api := createWeldrAPI(rpmmd_mock.BaseFixture)
api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
test.TestRoute(t, api, true, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
}
}
@ -63,7 +67,7 @@ func TestBlueprintsNew(t *testing.T) {
}
for _, c := range cases {
api := createWeldrAPI(rpmmd_mock.BaseFixture)
api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
test.TestRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
}
}
@ -80,7 +84,7 @@ func TestBlueprintsWorkspace(t *testing.T) {
}
for _, c := range cases {
api := createWeldrAPI(rpmmd_mock.BaseFixture)
api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
test.SendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"test","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
test.TestRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
}
@ -101,7 +105,7 @@ func TestBlueprintsInfo(t *testing.T) {
}
for _, c := range cases {
api := createWeldrAPI(rpmmd_mock.BaseFixture)
api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
test.SendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"test1","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
test.SendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"test2","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
test.SendHTTP(api, true, "POST", "/api/v0/blueprints/workspace", `{"name":"test2","description":"Test","packages":[{"name":"systemd","version":"123"}],"version":"0.0.0"}`)
@ -122,7 +126,7 @@ func TestBlueprintsFreeze(t *testing.T) {
}
for _, c := range cases {
api := createWeldrAPI(c.Fixture)
api, _ := createWeldrAPI(c.Fixture)
test.SendHTTP(api, false, "POST", "/api/v0/blueprints/new", `{"name":"test","description":"Test","packages":[{"name":"dep-package1","version":"*"}],"version":"0.0.0"}`)
test.TestRoute(t, api, false, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
test.SendHTTP(api, false, "DELETE", "/api/v0/blueprints/delete/test", ``)
@ -141,7 +145,7 @@ func TestBlueprintsDiff(t *testing.T) {
}
for _, c := range cases {
api := createWeldrAPI(rpmmd_mock.BaseFixture)
api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
test.SendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"test","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
test.SendHTTP(api, true, "POST", "/api/v0/blueprints/workspace", `{"name":"test","description":"Test","packages":[{"name":"systemd","version":"123"}],"version":"0.0.0"}`)
test.TestRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
@ -161,7 +165,7 @@ func TestBlueprintsDelete(t *testing.T) {
}
for _, c := range cases {
api := createWeldrAPI(rpmmd_mock.BaseFixture)
api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
test.SendHTTP(api, true, "POST", "/api/v0/blueprints/new", `{"name":"test","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
test.TestRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
test.SendHTTP(api, true, "DELETE", "/api/v0/blueprints/delete/test", ``)
@ -169,7 +173,7 @@ func TestBlueprintsDelete(t *testing.T) {
}
func TestBlueprintsChanges(t *testing.T) {
api := createWeldrAPI(rpmmd_mock.BaseFixture)
api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
rand.Seed(time.Now().UnixNano())
id := strconv.Itoa(rand.Int())
ignoreFields := []string{"commit", "timestamp"}
@ -184,24 +188,121 @@ func TestBlueprintsChanges(t *testing.T) {
}
func TestCompose(t *testing.T) {
expectedComposeLocal := &store.Compose{
QueueStatus: "WAITING",
Blueprint: &blueprint.Blueprint{
Name: "test",
Version: "0.0.0",
Packages: []blueprint.Package{},
Modules: []blueprint.Package{},
Groups: []blueprint.Group{},
Customizations: nil,
},
OutputType: "tar",
Targets: []*target.Target{
{
Name: "org.osbuild.local",
Created: time.Time{},
Status: "WAITING",
Options: &target.LocalTargetOptions{},
},
},
}
expectedComposeLocalAndAws := &store.Compose{
QueueStatus: "WAITING",
Blueprint: &blueprint.Blueprint{
Name: "test",
Version: "0.0.0",
Packages: []blueprint.Package{},
Modules: []blueprint.Package{},
Groups: []blueprint.Group{},
Customizations: nil,
},
OutputType: "tar",
Targets: []*target.Target{
{
Name: "org.osbuild.local",
Status: "WAITING",
Options: &target.LocalTargetOptions{},
},
{
Name: "org.osbuild.aws",
Status: "WAITING",
ImageName: "test_upload",
Options: &target.AWSTargetOptions{
Region: "frankfurt",
AccessKeyID: "accesskey",
SecretAccessKey: "secretkey",
Bucket: "clay",
Key: "imagekey",
},
},
},
}
var cases = []struct {
External bool
External bool
Method string
Path string
Body string
ExpectedStatus int
ExpectedJSON string
ExpectedCompose *store.Compose
IgnoreFields []string
}{
{true, "POST", "/api/v0/compose", `{"blueprint_name": "http-server","compose_type": "tar","branch": "master"}`, http.StatusBadRequest, `{"status":false,"errors":[{"id":"UnknownBlueprint","msg":"Unknown blueprint name: http-server"}]}`, nil, []string{"build_id"}},
{false, "POST", "/api/v0/compose", `{"blueprint_name": "test","compose_type": "tar","branch": "master"}`, http.StatusOK, `{"status": true}`, expectedComposeLocal, []string{"build_id"}},
{false, "POST", "/api/v1/compose", `{"blueprint_name": "test","compose_type":"tar","branch":"master","upload":{"image_name":"test_upload","provider":"aws","settings":{"region":"frankfurt","accessKeyID":"accesskey","secretAccessKey":"secretkey","bucket":"clay","key":"imagekey"}}}`, http.StatusOK, `{"status": true}`, expectedComposeLocalAndAws, []string{"build_id"}},
}
for _, c := range cases {
api, s := createWeldrAPI(rpmmd_mock.NoComposesFixture)
test.TestRoute(t, api, c.External, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON, c.IgnoreFields...)
if c.ExpectedStatus != http.StatusOK {
continue
}
if len(s.Composes) != 1 {
t.Fatalf("%s: bad compose count in store: %d", c.Path, len(s.Composes))
}
// I have no idea how to get the compose in better way
var compose store.Compose
for _, c := range s.Composes {
compose = c
break
}
if diff := cmp.Diff(compose, *c.ExpectedCompose, test.IgnoreDates(), test.IgnoreUuids(), test.Ignore("Targets.Options.Location")); diff != "" {
t.Errorf("%s: compose in store isn't the same as expected, diff:\n%s", c.Path, diff)
}
}
}
func TestComposeStatus(t *testing.T) {
var cases = []struct {
Fixture rpmmd_mock.FixtureGenerator
Method string
Path string
Body string
ExpectedStatus int
ExpectedJSON string
IgnoreFields []string
}{
{true, "POST", "/api/v0/compose", `{"blueprint_name": "http-server","compose_type": "tar","branch": "master"}`, http.StatusBadRequest, `{"status":false,"errors":[{"id":"UnknownBlueprint","msg":"Unknown blueprint name: http-server"}]}`, []string{"build_id"}},
{false, "POST", "/api/v0/compose", `{"blueprint_name": "test","compose_type": "tar","branch": "master"}`, http.StatusOK, `{"status": true}`, []string{"build_id"}},
{rpmmd_mock.BaseFixture, "GET", "/api/v0/compose/status/30000000-0000-0000-0000-000000000000,30000000-0000-0000-0000-000000000002", ``, http.StatusOK, `{"uuids":[{"id":"30000000-0000-0000-0000-000000000000","blueprint":"test","version":"0.0.0","compose_type":"tar","image_size":0,"queue_status":"WAITING","job_created":1574857140},{"id":"30000000-0000-0000-0000-000000000002","blueprint":"test","version":"0.0.0","compose_type":"tar","image_size":0,"queue_status":"FINISHED","job_created":1574857140,"job_started":1574857140,"job_finished":1574857140}]}`},
{rpmmd_mock.BaseFixture, "GET", "/api/v0/compose/status/*", ``, http.StatusOK, `{"uuids":[{"id":"30000000-0000-0000-0000-000000000000","blueprint":"test","version":"0.0.0","compose_type":"tar","image_size":0,"queue_status":"WAITING","job_created":1574857140},{"id":"30000000-0000-0000-0000-000000000001","blueprint":"test","version":"0.0.0","compose_type":"tar","image_size":0,"queue_status":"RUNNING","job_created":1574857140,"job_started":1574857140},{"id":"30000000-0000-0000-0000-000000000002","blueprint":"test","version":"0.0.0","compose_type":"tar","image_size":0,"queue_status":"FINISHED","job_created":1574857140,"job_started":1574857140,"job_finished":1574857140},{"id":"30000000-0000-0000-0000-000000000003","blueprint":"test","version":"0.0.0","compose_type":"tar","image_size":0,"queue_status":"FAILED","job_created":1574857140,"job_started":1574857140,"job_finished":1574857140}]}`},
{rpmmd_mock.BaseFixture, "GET", "/api/v1/compose/status/30000000-0000-0000-0000-000000000000", ``, http.StatusOK, `{"uuids":[{"id":"30000000-0000-0000-0000-000000000000","blueprint":"test","version":"0.0.0","compose_type":"tar","image_size":0,"queue_status":"WAITING","job_created":1574857140,"uploads":[{"uuid":"10000000-0000-0000-0000-000000000000","status":"WAITING","provider_name":"aws","image_name":"awsimage","creation_time":1574857140,"settings":{"region":"frankfurt","accessKeyID":"accesskey","secretAccessKey":"secretkey","bucket":"clay","key":"imagekey"}}]}]}`},
}
if len(os.Getenv("OSBUILD_COMPOSER_TEST_EXTERNAL")) > 0 {
t.Skip("This test is for internal testing only")
}
for _, c := range cases {
api := createWeldrAPI(rpmmd_mock.BaseFixture)
test.SendHTTP(api, c.External, "POST", "/api/v0/blueprints/new", `{"name":"test","description":"Test","packages":[{"name":"httpd","version":"2.4.*"}],"version":"0.0.0"}`)
test.TestRoute(t, api, c.External, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON, c.IgnoreFields...)
test.SendHTTP(api, c.External, "DELETE", "/api/v0/blueprints/delete/test", ``)
api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
test.TestRoute(t, api, false, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON, "id", "job_created", "job_started")
}
}
@ -213,9 +314,9 @@ func TestComposeQueue(t *testing.T) {
Body string
ExpectedStatus int
ExpectedJSON string
IgnoreFields []string
}{
{rpmmd_mock.BaseFixture, "GET", "/api/v0/compose/queue", ``, http.StatusOK, `{"new":[{"blueprint":"test","version":"0.0.0","compose_type":"tar","image_size":0,"queue_status":"WAITING"}],"run":[{"blueprint":"test","version":"0.0.0","compose_type":"tar","image_size":0,"queue_status":"RUNNING"}]}`, []string{"id", "job_created", "job_started"}},
{rpmmd_mock.BaseFixture, "GET", "/api/v0/compose/queue", ``, http.StatusOK, `{"new":[{"blueprint":"test","version":"0.0.0","compose_type":"tar","image_size":0,"queue_status":"WAITING"}],"run":[{"blueprint":"test","version":"0.0.0","compose_type":"tar","image_size":0,"queue_status":"RUNNING"}]}`},
{rpmmd_mock.BaseFixture, "GET", "/api/v1/compose/queue", ``, http.StatusOK, `{"new":[{"blueprint":"test","version":"0.0.0","compose_type":"tar","image_size":0,"queue_status":"WAITING","uploads":[{"uuid":"10000000-0000-0000-0000-000000000000","status":"WAITING","provider_name":"aws","image_name":"awsimage","creation_time":1574857140,"settings":{"region":"frankfurt","accessKeyID":"accesskey","secretAccessKey":"secretkey","bucket":"clay","key":"imagekey"}}]}],"run":[{"blueprint":"test","version":"0.0.0","compose_type":"tar","image_size":0,"queue_status":"RUNNING"}]}`},
}
if len(os.Getenv("OSBUILD_COMPOSER_TEST_EXTERNAL")) > 0 {
@ -223,8 +324,54 @@ func TestComposeQueue(t *testing.T) {
}
for _, c := range cases {
api := createWeldrAPI(rpmmd_mock.BaseFixture)
test.TestRoute(t, api, false, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON, c.IgnoreFields...)
api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
test.TestRoute(t, api, false, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON, "id", "job_created", "job_started")
}
}
func TestComposeFinished(t *testing.T) {
var cases = []struct {
Fixture rpmmd_mock.FixtureGenerator
Method string
Path string
Body string
ExpectedStatus int
ExpectedJSON string
}{
{rpmmd_mock.BaseFixture, "GET", "/api/v0/compose/finished", ``, http.StatusOK, `{"finished":[{"id":"30000000-0000-0000-0000-000000000002","blueprint":"test","version":"0.0.0","compose_type":"tar","image_size":0,"queue_status":"FINISHED","job_created":1574857140,"job_started":1574857140,"job_finished":1574857140}]}`},
{rpmmd_mock.BaseFixture, "GET", "/api/v1/compose/finished", ``, http.StatusOK, `{"finished":[{"id":"30000000-0000-0000-0000-000000000002","blueprint":"test","version":"0.0.0","compose_type":"tar","image_size":0,"queue_status":"FINISHED","job_created":1574857140,"job_started":1574857140,"job_finished":1574857140,"uploads":[{"uuid":"10000000-0000-0000-0000-000000000000","status":"WAITING","provider_name":"aws","image_name":"awsimage","creation_time":1574857140,"settings":{"region":"frankfurt","accessKeyID":"accesskey","secretAccessKey":"secretkey","bucket":"clay","key":"imagekey"}}]}]}`},
}
if len(os.Getenv("OSBUILD_COMPOSER_TEST_EXTERNAL")) > 0 {
t.Skip("This test is for internal testing only")
}
for _, c := range cases {
api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
test.TestRoute(t, api, false, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON, "id", "job_created", "job_started")
}
}
func TestComposeFailed(t *testing.T) {
var cases = []struct {
Fixture rpmmd_mock.FixtureGenerator
Method string
Path string
Body string
ExpectedStatus int
ExpectedJSON string
}{
{rpmmd_mock.BaseFixture, "GET", "/api/v0/compose/failed", ``, http.StatusOK, `{"failed":[{"id":"30000000-0000-0000-0000-000000000003","blueprint":"test","version":"0.0.0","compose_type":"tar","image_size":0,"queue_status":"FAILED","job_created":1574857140,"job_started":1574857140,"job_finished":1574857140}]}`},
{rpmmd_mock.BaseFixture, "GET", "/api/v1/compose/failed", ``, http.StatusOK, `{"failed":[{"id":"30000000-0000-0000-0000-000000000003","blueprint":"test","version":"0.0.0","compose_type":"tar","image_size":0,"queue_status":"FAILED","job_created":1574857140,"job_started":1574857140,"job_finished":1574857140,"uploads":[{"uuid":"10000000-0000-0000-0000-000000000000","status":"WAITING","provider_name":"aws","image_name":"awsimage","creation_time":1574857140,"settings":{"region":"frankfurt","accessKeyID":"accesskey","secretAccessKey":"secretkey","bucket":"clay","key":"imagekey"}}]}]}`},
}
if len(os.Getenv("OSBUILD_COMPOSER_TEST_EXTERNAL")) > 0 {
t.Skip("This test is for internal testing only")
}
for _, c := range cases {
api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
test.TestRoute(t, api, false, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON, "id", "job_created", "job_started")
}
}
@ -241,7 +388,7 @@ func TestSourcesNew(t *testing.T) {
}
for _, c := range cases {
api := createWeldrAPI(rpmmd_mock.BaseFixture)
api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
test.TestRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
test.SendHTTP(api, true, "DELETE", "/api/v0/projects/source/delete/fish", ``)
}
@ -260,7 +407,7 @@ func TestSourcesDelete(t *testing.T) {
}
for _, c := range cases {
api := createWeldrAPI(rpmmd_mock.BaseFixture)
api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
test.SendHTTP(api, true, "POST", "/api/v0/projects/source/new", `{"name": "fish","url": "https://download.opensuse.org/repositories/shells:/fish:/release:/3/Fedora_29/","type": "yum-baseurl","check_ssl": false,"check_gpg": false}`)
test.TestRoute(t, api, true, c.Method, c.Path, c.Body, c.ExpectedStatus, c.ExpectedJSON)
test.SendHTTP(api, true, "DELETE", "/api/v0/projects/source/delete/fish", ``)
@ -280,7 +427,7 @@ func TestProjectsDepsolve(t *testing.T) {
}
for _, c := range cases {
api := createWeldrAPI(c.Fixture)
api, _ := createWeldrAPI(c.Fixture)
test.TestRoute(t, api, true, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
}
}
@ -301,7 +448,7 @@ func TestProjectsInfo(t *testing.T) {
}
for _, c := range cases {
api := createWeldrAPI(c.Fixture)
api, _ := createWeldrAPI(c.Fixture)
test.TestRoute(t, api, true, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
}
}
@ -323,7 +470,7 @@ func TestModulesInfo(t *testing.T) {
}
for _, c := range cases {
api := createWeldrAPI(c.Fixture)
api, _ := createWeldrAPI(c.Fixture)
test.TestRoute(t, api, true, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
}
}
@ -342,7 +489,7 @@ func TestProjectsList(t *testing.T) {
}
for _, c := range cases {
api := createWeldrAPI(c.Fixture)
api, _ := createWeldrAPI(c.Fixture)
test.TestRoute(t, api, true, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
}
}
@ -364,7 +511,7 @@ func TestModulesList(t *testing.T) {
}
for _, c := range cases {
api := createWeldrAPI(c.Fixture)
api, _ := createWeldrAPI(c.Fixture)
test.TestRoute(t, api, true, "GET", c.Path, ``, c.ExpectedStatus, c.ExpectedJSON)
}
}

88
internal/weldr/compose.go Normal file
View file

@ -0,0 +1,88 @@
package weldr
import (
"github.com/google/uuid"
"github.com/osbuild/osbuild-composer/internal/store"
"sort"
)
type ComposeEntry struct {
ID uuid.UUID `json:"id"`
Blueprint string `json:"blueprint"`
Version string `json:"version"`
ComposeType string `json:"compose_type"`
ImageSize int64 `json:"image_size"`
QueueStatus string `json:"queue_status"`
JobCreated float64 `json:"job_created"`
JobStarted float64 `json:"job_started,omitempty"`
JobFinished float64 `json:"job_finished,omitempty"`
Uploads []UploadResponse `json:"uploads,omitempty"`
}
func composeToComposeEntry(id uuid.UUID, compose store.Compose, includeUploads bool) *ComposeEntry {
var composeEntry ComposeEntry
composeEntry.ID = id
composeEntry.Blueprint = compose.Blueprint.Name
composeEntry.Version = compose.Blueprint.Version
composeEntry.ComposeType = compose.OutputType
composeEntry.QueueStatus = compose.QueueStatus
if includeUploads {
composeEntry.Uploads = TargetsToUploadResponses(compose.Targets)
}
switch compose.QueueStatus {
case "WAITING":
composeEntry.JobCreated = float64(compose.JobCreated.UnixNano()) / 1000000000
case "RUNNING":
composeEntry.JobCreated = float64(compose.JobCreated.UnixNano()) / 1000000000
composeEntry.JobStarted = float64(compose.JobStarted.UnixNano()) / 1000000000
case "FINISHED":
//image, err := s.GetImage(id)
//imageSize := int64(0)
//if err == nil {
// imageSize = image.Size
//}
// TODO: this is currently broken!
composeEntry.ImageSize = int64(0)
composeEntry.JobCreated = float64(compose.JobCreated.UnixNano()) / 1000000000
composeEntry.JobStarted = float64(compose.JobStarted.UnixNano()) / 1000000000
composeEntry.JobFinished = float64(compose.JobFinished.UnixNano()) / 1000000000
case "FAILED":
composeEntry.JobCreated = float64(compose.JobCreated.UnixNano()) / 1000000000
composeEntry.JobStarted = float64(compose.JobStarted.UnixNano()) / 1000000000
composeEntry.JobFinished = float64(compose.JobFinished.UnixNano()) / 1000000000
default:
panic("invalid compose state")
}
return &composeEntry
}
func composesToComposeEntries(composes map[uuid.UUID]store.Compose, uuids []uuid.UUID, includeUploads bool) []*ComposeEntry {
var composeEntries []*ComposeEntry
if uuids == nil {
composeEntries = make([]*ComposeEntry, 0, len(composes))
for id, compose := range composes {
composeEntries = append(composeEntries, composeToComposeEntry(id, compose, includeUploads))
}
} else {
composeEntries = make([]*ComposeEntry, 0, len(uuids))
for _, id := range uuids {
if compose, exists := composes[id]; exists {
composeEntries = append(composeEntries, composeToComposeEntry(id, compose, includeUploads))
}
}
}
// make this function output more predictable
sort.Slice(composeEntries, func(i, j int) bool {
return composeEntries[i].ID.String() < composeEntries[j].ID.String()
})
return composeEntries
}

110
internal/weldr/upload.go Normal file
View file

@ -0,0 +1,110 @@
package weldr
import (
"encoding/json"
"errors"
"time"
"github.com/google/uuid"
"github.com/osbuild/osbuild-composer/internal/target"
)
type UploadResponse struct {
Uuid uuid.UUID `json:"uuid"`
Status string `json:"status"`
ProviderName string `json:"provider_name"`
ImageName string `json:"image_name"`
CreationTime float64 `json:"creation_time"`
Settings target.TargetOptions `json:"settings"`
}
type UploadRequest struct {
Provider string `json:"provider"`
ImageName string `json:"image_name"`
Settings target.TargetOptions `json:"settings"`
}
type rawUploadRequest struct {
Provider string `json:"provider"`
ImageName string `json:"image_name"`
Settings json.RawMessage `json:"settings"`
}
func (u *UploadRequest) UnmarshalJSON(data []byte) error {
var rawUpload rawUploadRequest
err := json.Unmarshal(data, &rawUpload)
if err != nil {
return err
}
// we need to convert provider name to target name to use the unmarshaller
targetName := providerNameToTargetNameMap[rawUpload.Provider]
options, err := target.UnmarshalTargetOptions(targetName, rawUpload.Settings)
u.Provider = rawUpload.Provider
u.ImageName = rawUpload.ImageName
u.Settings = options
return err
}
var targetNameToProviderNameMap = map[string]string{
"org.osbuild.aws": "aws",
"org.osbuild.azure": "azure",
}
var providerNameToTargetNameMap = map[string]string{
"aws": "org.osbuild.aws",
"azure": "org.osbuild.azure",
}
func targetToUploadResponse(t *target.Target) UploadResponse {
var u UploadResponse
providerName, providerExist := targetNameToProviderNameMap[t.Name]
if !providerExist {
panic("target name " + t.Name + " is not defined in conversion map!")
}
u.CreationTime = float64(t.Created.UnixNano()) / 1000000000
u.ImageName = t.ImageName
u.ProviderName = providerName
u.Status = t.Status
u.Uuid = t.Uuid
u.Settings = t.Options
return u
}
func TargetsToUploadResponses(targets []*target.Target) []UploadResponse {
var uploads []UploadResponse
for _, t := range targets {
if t.Name == "org.osbuild.local" {
continue
}
upload := targetToUploadResponse(t)
uploads = append(uploads, upload)
}
return uploads
}
func UploadRequestToTarget(u UploadRequest) (*target.Target, error) {
var t target.Target
targetName, targetExist := providerNameToTargetNameMap[u.Provider]
if !targetExist {
return nil, errors.New("Unknown provider name " + u.Provider)
}
t.Uuid = uuid.New()
t.ImageName = u.ImageName
t.Options = u.Settings
t.Name = targetName
t.Status = "WAITING"
t.Created = time.Now()
return &t, nil
}