Repository: osbuild/osbuild-composer
Pull request #147 (2019-12-06 11:41:28)

api: implement /compose/logs route
The implementation is just a stub returning always the same tar archive.
The ability to return actual logs will be implemented in the future - osbuild
isn't currently returning any logs.


Patch:
diff --git a/internal/weldr/api.go b/internal/weldr/api.go
index 742c44a55..f9c266abc 100644
--- a/internal/weldr/api.go
+++ b/internal/weldr/api.go
@@ -1,6 +1,7 @@
 package weldr
 
 import (
+	"archive/tar"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -92,6 +93,7 @@ func New(rpmmd rpmmd.RPMMD, distro distro.Distro, logger *log.Logger, store *sto
 	api.router.GET("/api/v:version/compose/finished", api.composeFinishedHandler)
 	api.router.GET("/api/v:version/compose/failed", api.composeFailedHandler)
 	api.router.GET("/api/v:version/compose/image/:uuid", api.composeImageHandler)
+	api.router.GET("/api/v:version/compose/logs/:uuid", api.composeLogsHandler)
 	api.router.POST("/api/v:version/compose/uploads/schedule/:uuid", api.uploadsScheduleHandler)
 
 	api.router.DELETE("/api/v:version/upload/delete/:uuid", api.uploadsDeleteHandler)
@@ -1463,6 +1465,59 @@ func (api *API) composeImageHandler(writer http.ResponseWriter, request *http.Re
 	io.Copy(writer, file)
 }
 
+func (api *API) composeLogsHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
+	if !verifyRequestVersion(writer, params, 0) {
+		return
+	}
+
+	uuidString := params.ByName("uuid")
+	id, err := uuid.Parse(uuidString)
+	if err != nil {
+		errors := responseError{
+			ID:  "UnknownUUID",
+			Msg: fmt.Sprintf("%s is not a valid build uuid", uuidString),
+		}
+		statusResponseError(writer, http.StatusBadRequest, errors)
+		return
+	}
+
+	compose, exists := api.store.GetCompose(id)
+	if !exists {
+		errors := responseError{
+			ID:  "UnknownUUID",
+			Msg: fmt.Sprintf("Compose %s doesn't exist", uuidString),
+		}
+		statusResponseError(writer, http.StatusBadRequest, errors)
+		return
+	}
+
+	if compose.QueueStatus != "FINISHED" && compose.QueueStatus != "FAILED" {
+		errors := responseError{
+			ID:  "BuildInWrongState",
+			Msg: fmt.Sprintf("Build %s not in FINISHED or FAILED state.", uuidString),
+		}
+		statusResponseError(writer, http.StatusBadRequest, errors)
+		return
+	}
+
+	writer.Header().Set("Content-Disposition", "attachment; filename="+id.String()+"-logs.tar")
+	writer.Header().Set("Content-Type", "application/x-tar")
+
+	tw := tar.NewWriter(writer)
+
+	// TODO: return real log from osbuild
+	fileContents := []byte("SUCCESS\n")
+
+	header := &tar.Header{
+		Name: "logs/osbuild.log",
+		Mode: 0644,
+		Size: int64(len(fileContents)),
+	}
+
+	tw.WriteHeader(header)
+	tw.Write(fileContents)
+}
+
 func (api *API) composeFinishedHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
 	if !verifyRequestVersion(writer, params, 0) {
 		return


Test patch:
diff --git a/internal/weldr/api_test.go b/internal/weldr/api_test.go
index 893be40e6..36bb1da88 100644
--- a/internal/weldr/api_test.go
+++ b/internal/weldr/api_test.go
@@ -1,7 +1,9 @@
 package weldr_test
 
 import (
+	"archive/tar"
 	"bytes"
+	"io"
 	"math/rand"
 	"net/http"
 	"net/http/httptest"
@@ -389,6 +391,73 @@ func TestComposeInfo(t *testing.T) {
 	}
 }
 
+func TestComposeLogs(t *testing.T) {
+	if len(os.Getenv("OSBUILD_COMPOSER_TEST_EXTERNAL")) > 0 {
+		t.Skip("This test is for internal testing only")
+	}
+
+	var successCases = []struct {
+		Path                       string
+		ExpectedContentDisposition string
+		ExpectedContentType        string
+		ExpectedFileName           string
+		ExpectedFileContent        string
+	}{
+		{"/api/v0/compose/logs/30000000-0000-0000-0000-000000000002", "attachment; filename=30000000-0000-0000-0000-000000000002-logs.tar", "application/x-tar", "logs/osbuild.log", "SUCCESS\n"},
+		{"/api/v1/compose/logs/30000000-0000-0000-0000-000000000002", "attachment; filename=30000000-0000-0000-0000-000000000002-logs.tar", "application/x-tar", "logs/osbuild.log", "SUCCESS\n"},
+	}
+
+	for _, c := range successCases {
+		api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
+
+		response := test.SendHTTP(api, false, "GET", c.Path, "")
+		if response.Header.Get("content-disposition") != c.ExpectedContentDisposition {
+			t.Errorf("%s: expected content-disposition: %s, but got: %s", c.Path, c.ExpectedContentDisposition, response.Header.Get("content-disposition"))
+		}
+
+		if response.Header.Get("content-type") != c.ExpectedContentType {
+			t.Errorf("%s: expected content-type: %s, but got: %s", c.Path, c.ExpectedContentType, response.Header.Get("content-type"))
+		}
+
+		tr := tar.NewReader(response.Body)
+		h, err := tr.Next()
+
+		if err != nil {
+			t.Errorf("untarring failed with error: %s", err.Error())
+		}
+
+		if h.Name != c.ExpectedFileName {
+			t.Errorf("%s: expected log content: %s, but got: %s", c.Path, c.ExpectedFileName, h.Name)
+		}
+
+		var buffer bytes.Buffer
+
+		io.Copy(&buffer, tr)
+
+		if buffer.String() != c.ExpectedFileContent {
+			t.Errorf("%s: expected log content: %s, but got: %s", c.Path, c.ExpectedFileContent, buffer.String())
+		}
+	}
+
+	var failureCases = []struct {
+		Path         string
+		ExpectedJSON string
+	}{
+		{"/api/v1/compose/logs/30000000-0000-0000-0000", `{"status":false,"errors":[{"id":"UnknownUUID","msg":"30000000-0000-0000-0000 is not a valid build uuid"}]}`},
+		{"/api/v1/compose/logs/42000000-0000-0000-0000-000000000000", `{"status":false,"errors":[{"id":"UnknownUUID","msg":"Compose 42000000-0000-0000-0000-000000000000 doesn't exist"}]}`},
+		{"/api/v1/compose/logs/30000000-0000-0000-0000-000000000000", `{"status":false,"errors":[{"id":"BuildInWrongState","msg":"Build 30000000-0000-0000-0000-000000000000 not in FINISHED or FAILED state."}]}`},
+	}
+
+	if len(os.Getenv("OSBUILD_COMPOSER_TEST_EXTERNAL")) > 0 {
+		t.Skip("This test is for internal testing only")
+	}
+
+	for _, c := range failureCases {
+		api, _ := createWeldrAPI(rpmmd_mock.BaseFixture)
+		test.TestRoute(t, api, false, "GET", c.Path, "", http.StatusBadRequest, c.ExpectedJSON)
+	}
+}
+
 func TestComposeQueue(t *testing.T) {
 	var cases = []struct {
 		Fixture        rpmmd_mock.FixtureGenerator