The store is responsible for two things: user state and the compose queue. This is problematic, because the rcm API has slightly different semantics from weldr and only used the queue part of the store. Also, the store is simply too complex. This commit splits the queue part out, using the new jobqueue package in both the weldr and the rcm package. The queue is saved to a new directory `queue/`. The weldr package now also has access to a worker server to enqueue and list jobs. Its store continues to track composes, but the `QueueStatus` for each compose (and image build) is deprecated. The field in `ImageBuild` is kept for backwards compatibility for composes which finished before this change, but a lot of code dealing with it in package compose is dropped. store.PushCompose() is degraded to storing a new compose. It should probably be renamed in the future. store.PopJob() is removed. Job ids are now independent of compose ids. Because of that, the local target gains ComposeId and ImageBuildId fields, because a worker cannot infer those from a job anymore. This also necessitates a change in the worker API: the job routes are changed to expect that instead of a (compose id, image build id) pair. The route that accepts built images keeps that pair, because it reports the image back to weldr. worker.Server() now interacts with a job queue instead of the store. It gains public functions that allow enqueuing an osbuild job and getting its status, because only it knows about the specific argument and result types in the job queue (OSBuildJob and OSBuildJobResult). One oddity remains: it needs to report an uploaded image to weldr. Do this with a function that's passed in for now, so that the dependency to the store can be dropped completely. The rcm API drops its dependencies to package blueprint and store, because it too interacts only with the worker server now. Fixes #342
311 lines
7.8 KiB
Go
311 lines
7.8 KiB
Go
package rcm_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"regexp"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/osbuild/osbuild-composer/internal/jobqueue/testjobqueue"
|
|
distro_mock "github.com/osbuild/osbuild-composer/internal/mocks/distro"
|
|
rpmmd_mock "github.com/osbuild/osbuild-composer/internal/mocks/rpmmd"
|
|
"github.com/osbuild/osbuild-composer/internal/rcm"
|
|
"github.com/osbuild/osbuild-composer/internal/worker"
|
|
)
|
|
|
|
type API interface {
|
|
ServeHTTP(writer http.ResponseWriter, request *http.Request)
|
|
}
|
|
|
|
func internalRequest(api API, method, path, body, contentType string) *http.Response {
|
|
req := httptest.NewRequest(method, path, bytes.NewReader([]byte(body)))
|
|
req.Header.Set("Content-Type", contentType)
|
|
resp := httptest.NewRecorder()
|
|
api.ServeHTTP(resp, req)
|
|
|
|
return resp.Result()
|
|
}
|
|
|
|
func newTestWorkerServer(t *testing.T) (*worker.Server, string) {
|
|
dir, err := ioutil.TempDir("", "rcm-test-")
|
|
require.NoError(t, err)
|
|
|
|
w := worker.NewServer(nil, testjobqueue.New(), nil)
|
|
require.NotNil(t, w)
|
|
|
|
return w, dir
|
|
}
|
|
|
|
func cleanupTempDir(t *testing.T, dir string) {
|
|
err := os.RemoveAll(dir)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestBasicRcmAPI(t *testing.T) {
|
|
// Test the HTTP API responses
|
|
// This test mainly focuses on HTTP status codes and JSON structures, not necessarily on their content
|
|
|
|
var cases = []struct {
|
|
Method string
|
|
Path string
|
|
Body string
|
|
ContentType string
|
|
ExpectedStatus int
|
|
ExpectedBodyRegex string
|
|
}{
|
|
{"GET", "/v1/compose", ``, "", http.StatusMethodNotAllowed, ``},
|
|
{"POST", "/v1/compose", `{"status":"RUNNING"}`, "application/json", http.StatusBadRequest, ``},
|
|
{"POST", "/v1/compose", `{"status":"RUNNING"}`, "text/plain", http.StatusBadRequest, ``},
|
|
{"POST", "/v1/compose", `{"image_builds":[]}`, "application/json", http.StatusBadRequest, ""},
|
|
{"POST", "/v1/compose/111-222-333", `{"status":"RUNNING"}`, "application/json", http.StatusMethodNotAllowed, ``},
|
|
{"GET", "/v1/compose/7802c476-9cd1-41b7-ba81-43c1906bce73", `{"status":"RUNNING"}`, "application/json", http.StatusBadRequest, ``},
|
|
}
|
|
|
|
registry, err := distro_mock.NewDefaultRegistry()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
workers, dir := newTestWorkerServer(t)
|
|
defer cleanupTempDir(t, dir)
|
|
|
|
api := rcm.New(nil, workers, rpmmd_mock.NewRPMMDMock(rpmmd_mock.BaseFixture()), registry)
|
|
|
|
for _, c := range cases {
|
|
resp := internalRequest(api, c.Method, c.Path, c.Body, c.ContentType)
|
|
buf := new(bytes.Buffer)
|
|
_, _ = buf.ReadFrom(resp.Body)
|
|
response_body := buf.String()
|
|
if resp.StatusCode != c.ExpectedStatus {
|
|
t.Errorf("%s request to %s should return status code %d but returns %d, response: %s", c.Method, c.Path, c.ExpectedStatus, resp.StatusCode, response_body)
|
|
}
|
|
matched, err := regexp.Match(c.ExpectedBodyRegex, []byte(response_body))
|
|
if err != nil {
|
|
t.Fatalf("Failed to match regex, correct the test definition!")
|
|
}
|
|
if !matched {
|
|
t.Errorf("The response to %s request to %s should match this regex %s but returns %s", c.Method, c.Path, c.ExpectedBodyRegex, response_body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSubmit(t *testing.T) {
|
|
registry, err := distro_mock.NewDefaultRegistry()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
workers, dir := newTestWorkerServer(t)
|
|
defer cleanupTempDir(t, dir)
|
|
|
|
api := rcm.New(nil, workers, rpmmd_mock.NewRPMMDMock(rpmmd_mock.BaseFixture()), registry)
|
|
|
|
var submit_reply struct {
|
|
UUID uuid.UUID `json:"compose_id"`
|
|
}
|
|
|
|
var cases = []struct {
|
|
Method string
|
|
Path string
|
|
Body string
|
|
ContentType string
|
|
ExpectedStatus int
|
|
}{
|
|
{
|
|
"POST",
|
|
"/v1/compose",
|
|
`{
|
|
"image_builds":
|
|
[
|
|
{
|
|
"distribution": "fedora-30",
|
|
"architecture": "x86_64",
|
|
"image_type": "qcow2",
|
|
"repositories":
|
|
[
|
|
{
|
|
"baseurl": "http://download.fedoraproject.org/pub/fedora/linux/releases/30/Everything/x86_64/os/"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}`,
|
|
"application/json",
|
|
http.StatusOK,
|
|
},
|
|
{
|
|
"POST",
|
|
"/v1/compose",
|
|
`{
|
|
"image_builds":
|
|
[
|
|
{
|
|
"distribution": "invalid",
|
|
"architecture": "x86_64",
|
|
"image_type": "qcow2",
|
|
"repositories":
|
|
[
|
|
{
|
|
"baseurl": "http://download.fedoraproject.org/pub/fedora/linux/releases/30/Everything/x86_64/os/"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}`,
|
|
"application/json",
|
|
http.StatusBadRequest,
|
|
},
|
|
{
|
|
"POST",
|
|
"/v1/compose",
|
|
`{
|
|
"image_builds":
|
|
[
|
|
{
|
|
"distribution": "fedora-30",
|
|
"architecture": "invalid",
|
|
"image_type": "qcow2",
|
|
"repositories":
|
|
[
|
|
{
|
|
"baseurl": "http://download.fedoraproject.org/pub/fedora/linux/releases/30/Everything/x86_64/os/"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}`,
|
|
"application/json",
|
|
http.StatusBadRequest,
|
|
},
|
|
{
|
|
"POST",
|
|
"/v1/compose",
|
|
`{
|
|
"image_builds":
|
|
[
|
|
{
|
|
"distribution": "fedora-30",
|
|
"architecture": "x86_64",
|
|
"image_type": "invalid",
|
|
"repositories":
|
|
[
|
|
{
|
|
"baseurl": "http://download.fedoraproject.org/pub/fedora/linux/releases/30/Everything/x86_64/os/"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}`,
|
|
"application/json",
|
|
http.StatusBadRequest,
|
|
},
|
|
}
|
|
|
|
for n, c := range cases {
|
|
// Submit job
|
|
t.Logf("RCM API submit compose, case %d\n", n)
|
|
resp := internalRequest(api, c.Method, c.Path, c.Body, c.ContentType)
|
|
if resp.StatusCode != c.ExpectedStatus {
|
|
errReason, _ := ioutil.ReadAll(resp.Body)
|
|
t.Fatal("Failed to call /v1/compose, HTTP status code: ", resp.StatusCode, "Error message: ", string(errReason))
|
|
}
|
|
if resp.StatusCode == http.StatusOK {
|
|
decoder := json.NewDecoder(resp.Body)
|
|
decoder.DisallowUnknownFields()
|
|
err := decoder.Decode(&submit_reply)
|
|
if err != nil {
|
|
t.Fatal("Failed to decode response to /v1/compose:", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStatus(t *testing.T) {
|
|
// Test the most basic use case: Submit a new job and get its status.
|
|
registry, err := distro_mock.NewDefaultRegistry()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
workers, dir := newTestWorkerServer(t)
|
|
defer cleanupTempDir(t, dir)
|
|
|
|
api := rcm.New(nil, workers, rpmmd_mock.NewRPMMDMock(rpmmd_mock.BaseFixture()), registry)
|
|
|
|
var submit_reply struct {
|
|
UUID uuid.UUID `json:"compose_id"`
|
|
}
|
|
var status_reply struct {
|
|
Status string `json:"status,omitempty"`
|
|
ErrorReason string `json:"error_reason,omitempty"`
|
|
}
|
|
// Submit a job
|
|
resp := internalRequest(api,
|
|
"POST",
|
|
"/v1/compose",
|
|
`{
|
|
"image_builds":
|
|
[
|
|
{
|
|
"distribution": "fedora-30",
|
|
"architecture": "x86_64",
|
|
"image_type": "qcow2",
|
|
"repositories":
|
|
[
|
|
{
|
|
"baseurl": "http://download.fedoraproject.org/pub/fedora/linux/releases/30/Everything/x86_64/os/"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}`,
|
|
"application/json")
|
|
if resp.StatusCode != http.StatusOK {
|
|
errReason, _ := ioutil.ReadAll(resp.Body)
|
|
t.Fatal("Failed to call /v1/compose, HTTP status code: ", resp.StatusCode, "Error message: ", string(errReason))
|
|
}
|
|
decoder := json.NewDecoder(resp.Body)
|
|
decoder.DisallowUnknownFields()
|
|
err = decoder.Decode(&submit_reply)
|
|
if err != nil {
|
|
t.Fatal("Failed to decode response to /v1/compose:", err)
|
|
}
|
|
|
|
var cases = []struct {
|
|
Method string
|
|
Path string
|
|
Body string
|
|
ContentType string
|
|
ExpectedStatus string
|
|
}{
|
|
{
|
|
"GET",
|
|
"/v1/compose/" + submit_reply.UUID.String(),
|
|
``,
|
|
"application/json",
|
|
"WAITING",
|
|
},
|
|
}
|
|
|
|
for n, c := range cases {
|
|
// Get the status
|
|
t.Logf("RCM API get compose status, case %d\n", n)
|
|
resp = internalRequest(api, c.Method, c.Path, c.Body, c.ContentType)
|
|
decoder = json.NewDecoder(resp.Body)
|
|
decoder.DisallowUnknownFields()
|
|
err = decoder.Decode(&status_reply)
|
|
if err != nil {
|
|
t.Fatal("Failed to decode response to /v1/compose/UUID:", err)
|
|
}
|
|
if status_reply.Status != c.ExpectedStatus {
|
|
t.Error("Failed to get compose status:", status_reply.Status)
|
|
}
|
|
}
|
|
}
|