workers: Backwards compatible api.openshift.com spec compliance
The main changes are: - Kind, Href, Id fields for every object returned - Attach operationIds to each request, return it for errors - Errors are predefined and queryable
This commit is contained in:
parent
5e206322a2
commit
2f328b0e97
14 changed files with 816 additions and 209 deletions
|
|
@ -13,6 +13,7 @@ RUN mkdir -p "/etc/osbuild-composer/"
|
|||
RUN mkdir -p "/run/osbuild-composer/"
|
||||
RUN mkdir -p "/var/cache/osbuild-composer/"
|
||||
RUN mkdir -p "/var/lib/osbuild-composer/"
|
||||
RUN mkdir -p "/usr/share/osbuild-composer/"
|
||||
RUN mkdir -p "/opt/migrate/"
|
||||
COPY --from=builder /opt/app-root/src/go/bin/osbuild-composer /usr/libexec/osbuild-composer/
|
||||
COPY ./containers/osbuild-composer/entrypoint.py /opt/entrypoint.py
|
||||
|
|
|
|||
|
|
@ -249,7 +249,8 @@ func TestCompose(t *testing.T) {
|
|||
|
||||
initJobResult, err := json.Marshal(&jobResult{Result: result})
|
||||
require.NoError(t, err)
|
||||
test.TestRoute(t, workerHandler, false, "PATCH", fmt.Sprintf("/api/worker/v1/jobs/%v", token), string(initJobResult), http.StatusOK, `{}`)
|
||||
test.TestRoute(t, workerHandler, false, "PATCH", fmt.Sprintf("/api/worker/v1/jobs/%v", token), string(initJobResult), http.StatusOK,
|
||||
fmt.Sprintf(`{"href":"/api/worker/v1/jobs/%v","id":"%v","kind":"UpdateJobResponse"}`, token, token))
|
||||
|
||||
wg.Done()
|
||||
}(t, c.initResult)
|
||||
|
|
@ -300,7 +301,8 @@ func TestCompose(t *testing.T) {
|
|||
|
||||
buildJobResult, err := json.Marshal(&jobResult{Result: c.buildResult})
|
||||
require.NoError(t, err)
|
||||
test.TestRoute(t, workerHandler, false, "PATCH", fmt.Sprintf("/api/worker/v1/jobs/%v", token), string(buildJobResult), http.StatusOK, `{}`)
|
||||
test.TestRoute(t, workerHandler, false, "PATCH", fmt.Sprintf("/api/worker/v1/jobs/%v", token), string(buildJobResult), http.StatusOK,
|
||||
fmt.Sprintf(`{"href":"/api/worker/v1/jobs/%v","id":"%v","kind":"UpdateJobResponse"}`, token, token))
|
||||
|
||||
_, token, jobType, rawJob, _, err = workerServer.RequestJob(context.Background(), test_distro.TestArchName, []string{"osbuild-koji"})
|
||||
require.NoError(t, err)
|
||||
|
|
@ -322,7 +324,8 @@ func TestCompose(t *testing.T) {
|
|||
"success": true
|
||||
}
|
||||
}
|
||||
}`, test_distro.TestArchName, test_distro.TestDistroName), http.StatusOK, `{}`)
|
||||
}`, test_distro.TestArchName, test_distro.TestDistroName), http.StatusOK,
|
||||
fmt.Sprintf(`{"href":"/api/worker/v1/jobs/%v","id":"%v","kind":"UpdateJobResponse"}`, token, token))
|
||||
|
||||
finalizeID, token, jobType, rawJob, _, err := workerServer.RequestJob(context.Background(), test_distro.TestArchName, []string{"koji-finalize"})
|
||||
require.NoError(t, err)
|
||||
|
|
@ -342,7 +345,8 @@ func TestCompose(t *testing.T) {
|
|||
|
||||
finalizeResult, err := json.Marshal(&jobResult{Result: c.finalizeResult})
|
||||
require.NoError(t, err)
|
||||
test.TestRoute(t, workerHandler, false, "PATCH", fmt.Sprintf("/api/worker/v1/jobs/%v", token), string(finalizeResult), http.StatusOK, `{}`)
|
||||
test.TestRoute(t, workerHandler, false, "PATCH", fmt.Sprintf("/api/worker/v1/jobs/%v", token), string(finalizeResult), http.StatusOK,
|
||||
fmt.Sprintf(`{"href":"/api/worker/v1/jobs/%v","id":"%v","kind":"UpdateJobResponse"}`, token, token))
|
||||
|
||||
test.TestRoute(t, handler, false, "GET", fmt.Sprintf("/api/composer-koji/v1/compose/%v", finalizeID), ``, http.StatusOK, c.composeStatus)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,29 +4,86 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/deepmap/oapi-codegen/pkg/runtime"
|
||||
"github.com/getkin/kin-openapi/openapi3"
|
||||
"github.com/labstack/echo/v4"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Error defines model for Error.
|
||||
type Error struct {
|
||||
Message string `json:"message"`
|
||||
// Embedded struct due to allOf(#/components/schemas/ObjectReference)
|
||||
ObjectReference
|
||||
// Embedded fields due to inline allOf schema
|
||||
Code string `json:"code"`
|
||||
|
||||
// Backward compatibility with workers <= v33, equals reason
|
||||
Message string `json:"message"`
|
||||
OperationId string `json:"operation_id"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// RequestJobJSONBody defines parameters for RequestJob.
|
||||
type RequestJobJSONBody struct {
|
||||
// GetJobResponse defines model for GetJobResponse.
|
||||
type GetJobResponse struct {
|
||||
// Embedded struct due to allOf(#/components/schemas/ObjectReference)
|
||||
ObjectReference
|
||||
// Embedded fields due to inline allOf schema
|
||||
Canceled bool `json:"canceled"`
|
||||
}
|
||||
|
||||
// ObjectReference defines model for ObjectReference.
|
||||
type ObjectReference struct {
|
||||
Href string `json:"href"`
|
||||
Id string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
}
|
||||
|
||||
// RequestJobRequest defines model for RequestJobRequest.
|
||||
type RequestJobRequest struct {
|
||||
Arch string `json:"arch"`
|
||||
Types []string `json:"types"`
|
||||
}
|
||||
|
||||
// UpdateJobJSONBody defines parameters for UpdateJob.
|
||||
type UpdateJobJSONBody struct {
|
||||
Result interface{} `json:"result"`
|
||||
Status string `json:"status"`
|
||||
// RequestJobResponse defines model for RequestJobResponse.
|
||||
type RequestJobResponse struct {
|
||||
// Embedded struct due to allOf(#/components/schemas/ObjectReference)
|
||||
ObjectReference
|
||||
// Embedded fields due to inline allOf schema
|
||||
Args *json.RawMessage `json:"args,omitempty"`
|
||||
ArtifactLocation string `json:"artifact_location"`
|
||||
DynamicArgs *[]json.RawMessage `json:"dynamic_args,omitempty"`
|
||||
Location string `json:"location"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// StatusResponse defines model for StatusResponse.
|
||||
type StatusResponse struct {
|
||||
// Embedded struct due to allOf(#/components/schemas/ObjectReference)
|
||||
ObjectReference
|
||||
// Embedded fields due to inline allOf schema
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// UpdateJobRequest defines model for UpdateJobRequest.
|
||||
type UpdateJobRequest struct {
|
||||
Result json.RawMessage `json:"result"`
|
||||
}
|
||||
|
||||
// UpdateJobResponse defines model for UpdateJobResponse.
|
||||
type UpdateJobResponse ObjectReference
|
||||
|
||||
// RequestJobJSONBody defines parameters for RequestJob.
|
||||
type RequestJobJSONBody RequestJobRequest
|
||||
|
||||
// UpdateJobJSONBody defines parameters for UpdateJob.
|
||||
type UpdateJobJSONBody UpdateJobRequest
|
||||
|
||||
// RequestJobRequestBody defines body for RequestJob for application/json ContentType.
|
||||
type RequestJobJSONRequestBody RequestJobJSONBody
|
||||
|
||||
|
|
@ -35,6 +92,9 @@ type UpdateJobJSONRequestBody UpdateJobJSONBody
|
|||
|
||||
// ServerInterface represents all server handlers.
|
||||
type ServerInterface interface {
|
||||
// Get error description
|
||||
// (GET /errors/{id})
|
||||
GetError(ctx echo.Context, id string) error
|
||||
// Request a job
|
||||
// (POST /jobs)
|
||||
RequestJob(ctx echo.Context) error
|
||||
|
|
@ -47,6 +107,9 @@ type ServerInterface interface {
|
|||
// Upload an artifact
|
||||
// (PUT /jobs/{token}/artifacts/{name})
|
||||
UploadJobArtifact(ctx echo.Context, token string, name string) error
|
||||
// Get the openapi spec in json format
|
||||
// (GET /openapi)
|
||||
GetOpenapi(ctx echo.Context) error
|
||||
// status
|
||||
// (GET /status)
|
||||
GetStatus(ctx echo.Context) error
|
||||
|
|
@ -57,6 +120,24 @@ type ServerInterfaceWrapper struct {
|
|||
Handler ServerInterface
|
||||
}
|
||||
|
||||
// GetError converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) GetError(ctx echo.Context) error {
|
||||
var err error
|
||||
// ------------- Path parameter "id" -------------
|
||||
var id string
|
||||
|
||||
err = runtime.BindStyledParameter("simple", false, "id", ctx.Param("id"), &id)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err))
|
||||
}
|
||||
|
||||
ctx.Set("Bearer.Scopes", []string{""})
|
||||
|
||||
// Invoke the callback with all the unmarshalled arguments
|
||||
err = w.Handler.GetError(ctx, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// RequestJob converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) RequestJob(ctx echo.Context) error {
|
||||
var err error
|
||||
|
|
@ -122,6 +203,15 @@ func (w *ServerInterfaceWrapper) UploadJobArtifact(ctx echo.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// GetOpenapi converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) GetOpenapi(ctx echo.Context) error {
|
||||
var err error
|
||||
|
||||
// Invoke the callback with all the unmarshalled arguments
|
||||
err = w.Handler.GetOpenapi(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetStatus converts echo context to params.
|
||||
func (w *ServerInterfaceWrapper) GetStatus(ctx echo.Context) error {
|
||||
var err error
|
||||
|
|
@ -153,10 +243,60 @@ func RegisterHandlers(router EchoRouter, si ServerInterface) {
|
|||
Handler: si,
|
||||
}
|
||||
|
||||
router.GET("/errors/:id", wrapper.GetError)
|
||||
router.POST("/jobs", wrapper.RequestJob)
|
||||
router.GET("/jobs/:token", wrapper.GetJob)
|
||||
router.PATCH("/jobs/:token", wrapper.UpdateJob)
|
||||
router.PUT("/jobs/:token/artifacts/:name", wrapper.UploadJobArtifact)
|
||||
router.GET("/openapi", wrapper.GetOpenapi)
|
||||
router.GET("/status", wrapper.GetStatus)
|
||||
|
||||
}
|
||||
|
||||
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||
var swaggerSpec = []string{
|
||||
|
||||
"H4sIAAAAAAAC/9xXTW/jNhD9KwTbo2I5TfcioIfNtlhkiyJF0kUXSINgTI0tJhKpDCknhqH/XvDDX5Ji",
|
||||
"Z4H4sDlZCcmZN2/eDIdLLnRVa4XKGp4tuREFVuA//yDS5D6gLC+nPLtZ8p8JpzzjP6WbQ2k8kV5O7lHY",
|
||||
"K5wioRLI22TJa9I1kpXoDQqdo/u1ixp5xo0lqWa8TXiFxsDMr+VoBMnaSq14xs9BPDwB5cz5AysnspR2",
|
||||
"wZ6kLdiTpgckw/5rxuMz8Rubn50lDB8bKA0jBKMVT/quHB5w1u9kPoglHu0v+bXHRhLmPLsJway3dwxv",
|
||||
"QrpdY9CeH97etgn/jPaLnlyhqbUy+KYcgxJY4nZsE61LBNWPYLV1GGPXV9Z1VXigAxS+wOyDVPlhXj17",
|
||||
"fmsSPPTRJfwKHxs0gUP/1UcHJIpBGO4ffoe0WJkXt/CMAxEsegDD+SQ4OATu7RMMNPO/zyczfRJ93xut",
|
||||
"Rlfw9FcUXevQWTkFYe9KLSBU00Cg+UJBJcXdyuiakgPWdwlK+F4n4R+H8u5XtywNhTAs1GsLtjHH4Np4",
|
||||
"y4exx33D8L7WOVjcJ1VC05T2IO0dp/HUkAK3XG5I+S4qnDOpprrfkv8ppGHSMFDs498XbKpp3YmtZhRi",
|
||||
"ZKByVoDKS2T3emJGrhVLWzqYl9fnjSxz9snBMEjshP3rDfCEz5FMcHMam7WCWvKMn43GozFPeA228Jyl",
|
||||
"6G4nky5l3rq/Z2j7WD+jQ8KkMtb1OqanzBbI/FFmahRyKjFnkwXzXWfdwi/ycDjcgM4rQYUWyXhR7Tq5",
|
||||
"+H3HLnfE8cwj5QlXULmgvf1N9iw1mMS71sHGZ6hqz87pWf/Wam/d2ZBJH/wv43G4T5VF5eOGui5lqJL0",
|
||||
"Pt5fG/P7Uh9ibH3Gf/327Sh2PxzFbptwg6IhaRc+LecIhMSzm1tHmGmqCmgRVRBSvp04dzx12vT1qM2A",
|
||||
"fGLBGgZOxCPmpb8WCZuUWjwY1igry7DF18UcZAmTEkc9RW0uhigGNPZc54s346Z/LQaaOuI5PYrD2Gm8",
|
||||
"w10ePxGCxZz/aArrxuE1s9HV1arXudRv9JQurX5Atd2Veo1lJYEj1XRnvBwI5fJP/kPW+05RU6OUVLNA",
|
||||
"f69LD3Rhn5i9jXig89ZgwyS5m8X1HXukWu6NDYOlPD6Gv3csmxAlg13tdEs3XY2eJl066fharhs7pIJS",
|
||||
"Q/5FTz7GE/w1OvQ/3yPD5O3k/DqtamHRnhhLCNUu6V2TL4ny3QnHJdpNkyttBNmsR9SXm/1l3PIanqI5",
|
||||
"P5wyqZjD7mbsCvxg/+EYg1+3yL8qfK5RWMzj2KSFaMjpq9+C3di7F7PjaPOMGpzSr6WbfVnYFV8NxJ4K",
|
||||
"KQpGaBtShhmkuRSrTUOz+vVq5WgdsvPOfI/tMdIbZ2uar3pYQyXPeAq1TMNjL52f+vfy1oKI77mTrR23",
|
||||
"7f8BAAD//yi1+mtgFAAA",
|
||||
}
|
||||
|
||||
// GetSwagger returns the Swagger specification corresponding to the generated code
|
||||
// in this file.
|
||||
func GetSwagger() (*openapi3.Swagger, error) {
|
||||
zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, ""))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error base64 decoding spec: %s", err)
|
||||
}
|
||||
zr, err := gzip.NewReader(bytes.NewReader(zipped))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decompressing spec: %s", err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
_, err = buf.ReadFrom(zr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decompressing spec: %s", err)
|
||||
}
|
||||
|
||||
swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData(buf.Bytes())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error loading Swagger: %s", err)
|
||||
}
|
||||
return swagger, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen -package=api -generate types,server -o api.gen.go openapi.yml
|
||||
//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen -package=api -generate types,server,spec -o api.gen.go openapi.yml
|
||||
|
||||
package api
|
||||
|
||||
|
|
|
|||
186
internal/worker/api/errors.go
Normal file
186
internal/worker/api/errors.go
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrorCodePrefix = "COMPOSER-WORKER-"
|
||||
ErrorHREF = "/api/composer-worker/v1/errors"
|
||||
|
||||
ErrorUnsupportedMediaType ServiceErrorCode = 3
|
||||
ErrorJobNotFound ServiceErrorCode = 5
|
||||
ErrorJobNotRunning ServiceErrorCode = 6
|
||||
ErrorMalformedJobId ServiceErrorCode = 7
|
||||
ErrorMalformedJobToken ServiceErrorCode = 8
|
||||
ErrorInvalidErrorId ServiceErrorCode = 9
|
||||
ErrorBodyDecodingError ServiceErrorCode = 10
|
||||
ErrorResourceNotFound ServiceErrorCode = 11
|
||||
ErrorMethodNotAllowed ServiceErrorCode = 12
|
||||
ErrorNotAcceptable ServiceErrorCode = 13
|
||||
ErrorErrorNotFound ServiceErrorCode = 14
|
||||
// ErrorTokenNotFound ServiceErrorCode = 6
|
||||
|
||||
// internal errors
|
||||
ErrorDiscardingArtifact ServiceErrorCode = 1000
|
||||
ErrorCreatingArtifact ServiceErrorCode = 1001
|
||||
ErrorWritingArtifact ServiceErrorCode = 1002
|
||||
ErrorResolvingJobId ServiceErrorCode = 1003
|
||||
ErrorFinishingJob ServiceErrorCode = 1004
|
||||
ErrorRetrievingJobStatus ServiceErrorCode = 1005
|
||||
ErrorRequestingJob ServiceErrorCode = 1006
|
||||
ErrorFailedLoadingOpenAPISpec ServiceErrorCode = 1007
|
||||
|
||||
// Errors contained within this file
|
||||
ErrorUnspecified ServiceErrorCode = 10000
|
||||
ErrorNotHTTPError ServiceErrorCode = 10001
|
||||
ErrorServiceErrorNotFound ServiceErrorCode = 10002
|
||||
ErrorMalformedOperationID ServiceErrorCode = 10003
|
||||
)
|
||||
|
||||
type ServiceErrorCode int
|
||||
|
||||
type serviceError struct {
|
||||
code ServiceErrorCode
|
||||
httpStatus int
|
||||
reason string
|
||||
}
|
||||
|
||||
type serviceErrors []serviceError
|
||||
|
||||
func getServiceErrors() serviceErrors {
|
||||
return serviceErrors{
|
||||
serviceError{ErrorUnsupportedMediaType, http.StatusUnsupportedMediaType, "Only 'application/json' content is supported"},
|
||||
serviceError{ErrorBodyDecodingError, http.StatusBadRequest, "Malformed json, unable to decode body"},
|
||||
|
||||
serviceError{ErrorJobNotFound, http.StatusNotFound, "Token not found"},
|
||||
serviceError{ErrorJobNotRunning, http.StatusBadRequest, "Job is not running"},
|
||||
serviceError{ErrorMalformedJobId, http.StatusBadRequest, "Given job id is not a uuidv4"},
|
||||
serviceError{ErrorMalformedJobToken, http.StatusBadRequest, "Given job id is not a uuidv4"},
|
||||
|
||||
serviceError{ErrorDiscardingArtifact, http.StatusInternalServerError, "Error discarding artifact"},
|
||||
serviceError{ErrorCreatingArtifact, http.StatusInternalServerError, "Error creating artifact"},
|
||||
serviceError{ErrorWritingArtifact, http.StatusInternalServerError, "Error writing artifact"},
|
||||
serviceError{ErrorResolvingJobId, http.StatusInternalServerError, "Error resolving id from job token"},
|
||||
serviceError{ErrorFinishingJob, http.StatusInternalServerError, "Error finishing job"},
|
||||
serviceError{ErrorRetrievingJobStatus, http.StatusInternalServerError, "Error requesting job"},
|
||||
serviceError{ErrorRequestingJob, http.StatusInternalServerError, "Error requesting job"},
|
||||
serviceError{ErrorInvalidErrorId, http.StatusBadRequest, "Invalid format for error id, it should be an integer as a string"},
|
||||
serviceError{ErrorFailedLoadingOpenAPISpec, http.StatusInternalServerError, "Unable to load openapi spec"},
|
||||
serviceError{ErrorResourceNotFound, http.StatusNotFound, "Requested resource doesn't exist"},
|
||||
serviceError{ErrorMethodNotAllowed, http.StatusMethodNotAllowed, "Requested method isn't supported for resource"},
|
||||
serviceError{ErrorNotAcceptable, http.StatusNotAcceptable, "Only 'application/json' content is supported"},
|
||||
serviceError{ErrorErrorNotFound, http.StatusNotFound, "Error with given id not found"},
|
||||
|
||||
serviceError{ErrorUnspecified, http.StatusInternalServerError, "Unspecified internal error "},
|
||||
serviceError{ErrorNotHTTPError, http.StatusInternalServerError, "Error is not an instance of HTTPError"},
|
||||
serviceError{ErrorServiceErrorNotFound, http.StatusInternalServerError, "Error does not exist"},
|
||||
serviceError{ErrorMalformedOperationID, http.StatusInternalServerError, "OperationID is empty or is not a string"},
|
||||
}
|
||||
}
|
||||
|
||||
func find(code ServiceErrorCode) *serviceError {
|
||||
for _, e := range getServiceErrors() {
|
||||
if e.code == code {
|
||||
return &e
|
||||
}
|
||||
}
|
||||
return &serviceError{ErrorServiceErrorNotFound, http.StatusInternalServerError, "Error does not exist"}
|
||||
}
|
||||
|
||||
// Make an echo compatible error out of a service error
|
||||
func HTTPError(code ServiceErrorCode) error {
|
||||
return HTTPErrorWithInternal(code, nil)
|
||||
}
|
||||
|
||||
// echo.HTTPError has a message interface{} field, which can be used to include the ServiceErrorCode
|
||||
func HTTPErrorWithInternal(code ServiceErrorCode, internalErr error) error {
|
||||
se := find(code)
|
||||
he := echo.NewHTTPError(se.httpStatus, se.code)
|
||||
if internalErr != nil {
|
||||
he.Internal = internalErr
|
||||
}
|
||||
return he
|
||||
}
|
||||
|
||||
// Convert a ServiceErrorCode into an Error as defined in openapi.v2.yml
|
||||
// serviceError is optional, prevents multiple find() calls
|
||||
func APIError(code ServiceErrorCode, serviceError *serviceError, c echo.Context) *Error {
|
||||
se := serviceError
|
||||
if se == nil {
|
||||
se = find(code)
|
||||
}
|
||||
|
||||
operationID, ok := c.Get("operationID").(string)
|
||||
if !ok || operationID == "" {
|
||||
c.Logger().Errorf("Couldn't find operationID handling error %v", code)
|
||||
se = find(ErrorMalformedOperationID)
|
||||
}
|
||||
|
||||
return &Error{
|
||||
ObjectReference: ObjectReference{
|
||||
Href: fmt.Sprintf("%s/%d", ErrorHREF, se.code),
|
||||
Id: fmt.Sprintf("%d", se.code),
|
||||
Kind: "Error",
|
||||
},
|
||||
Code: fmt.Sprintf("%s%d", ErrorCodePrefix, se.code),
|
||||
OperationId: operationID, // set operation id from context
|
||||
Reason: se.reason,
|
||||
Message: se.reason, // backward compatibility
|
||||
}
|
||||
}
|
||||
|
||||
func apiErrorFromEchoError(echoError *echo.HTTPError) ServiceErrorCode {
|
||||
switch echoError.Code {
|
||||
case http.StatusNotFound:
|
||||
return ErrorResourceNotFound
|
||||
case http.StatusMethodNotAllowed:
|
||||
return ErrorMethodNotAllowed
|
||||
case http.StatusNotAcceptable:
|
||||
return ErrorNotAcceptable
|
||||
default:
|
||||
return ErrorUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
// Convert an echo error into an AOC compliant one so we send a correct json error response
|
||||
func HTTPErrorHandler(echoError error, c echo.Context) {
|
||||
doResponse := func(code ServiceErrorCode, c echo.Context) {
|
||||
if !c.Response().Committed {
|
||||
var err error
|
||||
sec := find(code)
|
||||
apiErr := APIError(code, sec, c)
|
||||
|
||||
if sec.httpStatus == http.StatusInternalServerError {
|
||||
c.Logger().Errorf("Internal server error. Code: %s, OperationId: %s",
|
||||
apiErr.Code, apiErr.OperationId)
|
||||
}
|
||||
|
||||
if c.Request().Method == http.MethodHead {
|
||||
err = c.NoContent(sec.httpStatus)
|
||||
} else {
|
||||
err = c.JSON(sec.httpStatus, apiErr)
|
||||
}
|
||||
if err != nil {
|
||||
c.Logger().Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
he, ok := echoError.(*echo.HTTPError)
|
||||
if !ok {
|
||||
doResponse(ErrorNotHTTPError, c)
|
||||
return
|
||||
}
|
||||
|
||||
sec, ok := he.Message.(ServiceErrorCode)
|
||||
if !ok {
|
||||
// No service code was set, so Echo threw this error
|
||||
doResponse(apiErrorFromEchoError(he), c)
|
||||
return
|
||||
}
|
||||
doResponse(sec, c)
|
||||
}
|
||||
|
|
@ -5,105 +5,76 @@ info:
|
|||
description: This is an API for workers to request and handle jobs.
|
||||
servers:
|
||||
- url: /api/worker/v1
|
||||
- url: /api/composer-worker/v1
|
||||
|
||||
paths:
|
||||
/openapi:
|
||||
get:
|
||||
operationId: getOpenapi
|
||||
summary: Get the openapi spec in json format
|
||||
responses:
|
||||
'200':
|
||||
description: openapi spec in json format
|
||||
'500':
|
||||
description: Unexpected error occurred
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
/status:
|
||||
get:
|
||||
operationId: GetStatus
|
||||
summary: status
|
||||
tags: []
|
||||
description: Simple status handler which returns service status
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
headers: {}
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
- OK
|
||||
required:
|
||||
- status
|
||||
4XX:
|
||||
$ref: '#/components/schemas/StatusResponse'
|
||||
'4XX':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
5XX:
|
||||
description: ''
|
||||
'5XX':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
operationId: GetStatus
|
||||
description: Simple status handler to check whether the service is up.
|
||||
|
||||
/jobs:
|
||||
post:
|
||||
operationId: RequestJob
|
||||
summary: Request a job
|
||||
tags: []
|
||||
description: Requests a job. This operation blocks until a job is available.
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RequestJobRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: Created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
location:
|
||||
type: string
|
||||
artifact_location:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- osbuild
|
||||
args: {}
|
||||
dynamic_args:
|
||||
type: array
|
||||
items: {}
|
||||
required:
|
||||
- type
|
||||
- location
|
||||
- id
|
||||
4XX:
|
||||
$ref: '#/components/schemas/RequestJobResponse'
|
||||
'4XX':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
5XX:
|
||||
'5XX':
|
||||
description: ''
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
operationId: RequestJob
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
types:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- osbuild
|
||||
arch:
|
||||
type: string
|
||||
required:
|
||||
- types
|
||||
- arch
|
||||
description: ''
|
||||
description: Requests a job. This operation blocks until a job is available.
|
||||
parameters: []
|
||||
'/jobs/{token}':
|
||||
|
||||
/jobs/{token}:
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
|
|
@ -111,98 +82,212 @@ paths:
|
|||
in: path
|
||||
required: true
|
||||
get:
|
||||
operationId: GetJob
|
||||
summary: Get running job
|
||||
tags: []
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
canceled:
|
||||
type: boolean
|
||||
required:
|
||||
- canceled
|
||||
4XX:
|
||||
description: ''
|
||||
$ref: '#/components/schemas/GetJobResponse'
|
||||
'4XX':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
5XX:
|
||||
description: ''
|
||||
'5XX':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
operationId: GetJob
|
||||
description: ''
|
||||
patch:
|
||||
summary: Update a running job
|
||||
tags: []
|
||||
responses: {}
|
||||
operationId: UpdateJob
|
||||
summary: Update a running job
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
- WAITING
|
||||
- RUNNING
|
||||
- FINISHED
|
||||
- FAILED
|
||||
result: {}
|
||||
required:
|
||||
- status
|
||||
- result
|
||||
'/jobs/{token}/artifacts/{name}':
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: name
|
||||
in: path
|
||||
required: true
|
||||
- schema:
|
||||
type: string
|
||||
name: token
|
||||
in: path
|
||||
required: true
|
||||
put:
|
||||
summary: Upload an artifact
|
||||
tags: []
|
||||
$ref: '#/components/schemas/UpdateJobRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
4XX:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateJobResponse'
|
||||
'4XX':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
5XX:
|
||||
description: ''
|
||||
'5XX':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
/jobs/{token}/artifacts/{name}:
|
||||
put:
|
||||
operationId: UploadJobArtifact
|
||||
summary: Upload an artifact
|
||||
requestBody:
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: name
|
||||
in: path
|
||||
required: true
|
||||
- schema:
|
||||
type: string
|
||||
name: token
|
||||
in: path
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
'4XX':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'5XX':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
/errors/{id}:
|
||||
get:
|
||||
operationId: getError
|
||||
summary: Get error description
|
||||
description: Get an instance of the error specified by id
|
||||
security:
|
||||
- Bearer: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
example: '13'
|
||||
required: true
|
||||
description: ID of the error
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'4XX':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'5XX':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Error:
|
||||
title: Error
|
||||
ObjectReference:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
required:
|
||||
- message
|
||||
- id
|
||||
- kind
|
||||
- href
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
kind:
|
||||
type: string
|
||||
href:
|
||||
type: string
|
||||
|
||||
Error:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ObjectReference'
|
||||
- type: object
|
||||
required:
|
||||
- code
|
||||
- reason
|
||||
- operation_id
|
||||
- message
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
reason:
|
||||
type: string
|
||||
operation_id:
|
||||
type: string
|
||||
message:
|
||||
description: Backward compatibility with workers <= v33, equals reason
|
||||
type: string
|
||||
|
||||
StatusResponse:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ObjectReference'
|
||||
- type: object
|
||||
required:
|
||||
- status
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
|
||||
RequestJobRequest:
|
||||
type: object
|
||||
required:
|
||||
- types
|
||||
- arch
|
||||
properties:
|
||||
types:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
arch:
|
||||
type: string
|
||||
RequestJobResponse:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ObjectReference'
|
||||
- type: object
|
||||
required:
|
||||
- type
|
||||
- location
|
||||
- artifact_location
|
||||
properties:
|
||||
location:
|
||||
type: string
|
||||
artifact_location:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
args:
|
||||
x-go-type: json.RawMessage
|
||||
dynamic_args:
|
||||
type: array
|
||||
items:
|
||||
x-go-type: json.RawMessage
|
||||
|
||||
GetJobResponse:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ObjectReference'
|
||||
- type: object
|
||||
required:
|
||||
- canceled
|
||||
properties:
|
||||
canceled:
|
||||
type: boolean
|
||||
UpdateJobRequest:
|
||||
type: object
|
||||
required:
|
||||
- result
|
||||
properties:
|
||||
result:
|
||||
x-go-type: json.RawMessage
|
||||
UpdateJobResponse:
|
||||
$ref: '#/components/schemas/ObjectReference'
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ func (c *Client) RequestJob(types []string, arch string) (Job, error) {
|
|||
return nil, errorFromResponse(response, "error requesting job")
|
||||
}
|
||||
|
||||
var jr requestJobResponse
|
||||
var jr api.RequestJobResponse
|
||||
err = json.NewDecoder(response.Body).Decode(&jr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing response: %v", err)
|
||||
|
|
@ -201,12 +201,26 @@ func (c *Client) RequestJob(types []string, arch string) (Job, error) {
|
|||
return nil, fmt.Errorf("error parsing artifact location url in response: %v", err)
|
||||
}
|
||||
|
||||
jobId, err := uuid.Parse(jr.Id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing job id in response: %v", err)
|
||||
}
|
||||
|
||||
args := json.RawMessage{}
|
||||
if jr.Args != nil {
|
||||
args = *jr.Args
|
||||
}
|
||||
dynamicArgs := []json.RawMessage{}
|
||||
if jr.DynamicArgs != nil {
|
||||
dynamicArgs = *jr.DynamicArgs
|
||||
}
|
||||
|
||||
return &job{
|
||||
client: c,
|
||||
id: jr.Id,
|
||||
id: jobId,
|
||||
jobType: jr.Type,
|
||||
args: jr.Args,
|
||||
dynamicArgs: jr.DynamicArgs,
|
||||
args: args,
|
||||
dynamicArgs: dynamicArgs,
|
||||
location: location.String(),
|
||||
artifactLocation: artifactLocation.String(),
|
||||
}, nil
|
||||
|
|
@ -242,7 +256,7 @@ func (j *job) DynamicArgs(i int, args interface{}) error {
|
|||
|
||||
func (j *job) Update(result interface{}) error {
|
||||
var buf bytes.Buffer
|
||||
err := json.NewEncoder(&buf).Encode(api.UpdateJobJSONRequestBody{
|
||||
err := json.NewEncoder(&buf).Encode(updateJobRequest{
|
||||
Result: result,
|
||||
})
|
||||
if err != nil {
|
||||
|
|
@ -285,7 +299,7 @@ func (j *job) Canceled() (bool, error) {
|
|||
return false, errorFromResponse(response, "error fetching job info")
|
||||
}
|
||||
|
||||
var jr getJobResponse
|
||||
var jr api.GetJobResponse
|
||||
err = json.NewDecoder(response.Body).Decode(&jr)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error parsing reponse: %v", err)
|
||||
|
|
@ -337,5 +351,5 @@ func errorFromResponse(response *http.Response, message string) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("failed to parse error response: %v", err)
|
||||
}
|
||||
return fmt.Errorf("%v: %v — %v", message, response.StatusCode, e.Message)
|
||||
return fmt.Errorf("%v: %v — %s (%v)", message, response.StatusCode, e.Reason, e.Code)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
package worker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/osbuild/osbuild-composer/internal/distro"
|
||||
osbuild "github.com/osbuild/osbuild-composer/internal/osbuild1"
|
||||
"github.com/osbuild/osbuild-composer/internal/target"
|
||||
|
|
@ -76,29 +73,9 @@ type KojiFinalizeJobResult struct {
|
|||
}
|
||||
|
||||
//
|
||||
// JSON-serializable types for the HTTP API
|
||||
// JSON-serializable types for the client
|
||||
//
|
||||
|
||||
type statusResponse struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type requestJobResponse struct {
|
||||
Id uuid.UUID `json:"id"`
|
||||
Location string `json:"location"`
|
||||
ArtifactLocation string `json:"artifact_location"`
|
||||
Type string `json:"type"`
|
||||
Args json.RawMessage `json:"args,omitempty"`
|
||||
DynamicArgs []json.RawMessage `json:"dynamic_args,omitempty"`
|
||||
}
|
||||
|
||||
type getJobResponse struct {
|
||||
Canceled bool `json:"canceled"`
|
||||
}
|
||||
|
||||
type updateJobRequest struct {
|
||||
Result json.RawMessage `json:"result"`
|
||||
}
|
||||
|
||||
type updateJobResponse struct {
|
||||
Result interface{} `json:"result"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,13 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
"github.com/osbuild/osbuild-composer/internal/common"
|
||||
"github.com/osbuild/osbuild-composer/internal/jobqueue"
|
||||
"github.com/osbuild/osbuild-composer/internal/prometheus"
|
||||
"github.com/osbuild/osbuild-composer/internal/worker/api"
|
||||
|
|
@ -53,11 +55,8 @@ func (s *Server) Handler() http.Handler {
|
|||
e.StdLogger = s.logger
|
||||
|
||||
// log errors returned from handlers
|
||||
e.HTTPErrorHandler = func(err error, c echo.Context) {
|
||||
log.Println(c.Path(), c.QueryParams().Encode(), err.Error())
|
||||
e.DefaultHTTPErrorHandler(err, c)
|
||||
}
|
||||
|
||||
e.HTTPErrorHandler = api.HTTPErrorHandler
|
||||
e.Pre(common.OperationIDMiddleware)
|
||||
handler := apiHandlers{
|
||||
server: s,
|
||||
}
|
||||
|
|
@ -135,8 +134,10 @@ func (s *Server) Job(id uuid.UUID, job interface{}) (string, json.RawMessage, []
|
|||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(rawArgs, job); err != nil {
|
||||
return "", nil, nil, fmt.Errorf("error unmarshaling arguments for job '%s': %v", id, err)
|
||||
if job != nil {
|
||||
if err := json.Unmarshal(rawArgs, job); err != nil {
|
||||
return "", nil, nil, fmt.Errorf("error unmarshaling arguments for job '%s': %v", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
return jobType, rawArgs, deps, nil
|
||||
|
|
@ -284,12 +285,39 @@ type apiHandlers struct {
|
|||
server *Server
|
||||
}
|
||||
|
||||
func (h *apiHandlers) GetOpenapi(ctx echo.Context) error {
|
||||
spec, err := api.GetSwagger()
|
||||
if err != nil {
|
||||
return api.HTTPError(api.ErrorFailedLoadingOpenAPISpec)
|
||||
}
|
||||
return ctx.JSON(http.StatusOK, spec)
|
||||
}
|
||||
|
||||
func (h *apiHandlers) GetStatus(ctx echo.Context) error {
|
||||
return ctx.JSON(http.StatusOK, &statusResponse{
|
||||
return ctx.JSON(http.StatusOK, &api.StatusResponse{
|
||||
ObjectReference: api.ObjectReference{
|
||||
Href: fmt.Sprintf("%s/status", api.BasePath),
|
||||
Id: "status",
|
||||
Kind: "Status",
|
||||
},
|
||||
Status: "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *apiHandlers) GetError(ctx echo.Context, id string) error {
|
||||
errorId, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
return api.HTTPError(api.ErrorInvalidErrorId)
|
||||
}
|
||||
|
||||
apiError := api.APIError(api.ServiceErrorCode(errorId), nil, ctx)
|
||||
// If the service error wasn't found, it's a 404 in this instance
|
||||
if apiError.Id == fmt.Sprintf("%d", api.ErrorServiceErrorNotFound) {
|
||||
return api.HTTPError(api.ErrorErrorNotFound)
|
||||
}
|
||||
return ctx.JSON(http.StatusOK, apiError)
|
||||
}
|
||||
|
||||
func (h *apiHandlers) RequestJob(ctx echo.Context) error {
|
||||
var body api.RequestJobJSONRequestBody
|
||||
err := ctx.Bind(&body)
|
||||
|
|
@ -299,47 +327,73 @@ func (h *apiHandlers) RequestJob(ctx echo.Context) error {
|
|||
|
||||
jobId, token, jobType, jobArgs, dynamicJobArgs, err := h.server.RequestJob(ctx.Request().Context(), body.Arch, body.Types)
|
||||
if err != nil {
|
||||
return err
|
||||
return api.HTTPErrorWithInternal(api.ErrorRequestingJob, err)
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusCreated, requestJobResponse{
|
||||
Id: jobId,
|
||||
var respArgs *json.RawMessage
|
||||
if len(jobArgs) != 0 {
|
||||
respArgs = &jobArgs
|
||||
}
|
||||
var respDynArgs *[]json.RawMessage
|
||||
if len(dynamicJobArgs) != 0 {
|
||||
respDynArgs = &dynamicJobArgs
|
||||
}
|
||||
|
||||
response := api.RequestJobResponse{
|
||||
ObjectReference: api.ObjectReference{
|
||||
Href: fmt.Sprintf("%s/jobs", api.BasePath),
|
||||
Id: jobId.String(),
|
||||
Kind: "RequestJob",
|
||||
},
|
||||
Location: fmt.Sprintf("%s/jobs/%v", api.BasePath, token),
|
||||
ArtifactLocation: fmt.Sprintf("%s/jobs/%v/artifacts/", api.BasePath, token),
|
||||
Type: jobType,
|
||||
Args: jobArgs,
|
||||
DynamicArgs: dynamicJobArgs,
|
||||
})
|
||||
Args: respArgs,
|
||||
DynamicArgs: respDynArgs,
|
||||
}
|
||||
return ctx.JSON(http.StatusCreated, response)
|
||||
}
|
||||
|
||||
func (h *apiHandlers) GetJob(ctx echo.Context, tokenstr string) error {
|
||||
token, err := uuid.Parse(tokenstr)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "cannot parse job token")
|
||||
return api.HTTPError(api.ErrorMalformedJobToken)
|
||||
}
|
||||
|
||||
jobId, err := h.server.jobs.IdFromToken(token)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case jobqueue.ErrNotExist:
|
||||
return ErrInvalidToken
|
||||
return api.HTTPError(api.ErrorJobNotFound)
|
||||
default:
|
||||
return err
|
||||
return api.HTTPError(api.ErrorResolvingJobId)
|
||||
}
|
||||
}
|
||||
|
||||
if jobId == uuid.Nil {
|
||||
return ctx.JSON(http.StatusOK, getJobResponse{})
|
||||
return ctx.JSON(http.StatusOK, api.GetJobResponse{
|
||||
ObjectReference: api.ObjectReference{
|
||||
Href: fmt.Sprintf("%s/jobs/%v", api.BasePath, token),
|
||||
Id: token.String(),
|
||||
Kind: "JobStatus",
|
||||
},
|
||||
Canceled: false,
|
||||
})
|
||||
}
|
||||
|
||||
h.server.jobs.RefreshHeartbeat(token)
|
||||
|
||||
status, _, err := h.server.JobStatus(jobId, &json.RawMessage{})
|
||||
if err != nil {
|
||||
return err
|
||||
return api.HTTPErrorWithInternal(api.ErrorRetrievingJobStatus, err)
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, getJobResponse{
|
||||
return ctx.JSON(http.StatusOK, api.GetJobResponse{
|
||||
ObjectReference: api.ObjectReference{
|
||||
Href: fmt.Sprintf("%s/jobs/%v", api.BasePath, token),
|
||||
Id: token.String(),
|
||||
Kind: "JobStatus",
|
||||
},
|
||||
Canceled: status.Canceled,
|
||||
})
|
||||
}
|
||||
|
|
@ -347,10 +401,10 @@ func (h *apiHandlers) GetJob(ctx echo.Context, tokenstr string) error {
|
|||
func (h *apiHandlers) UpdateJob(ctx echo.Context, idstr string) error {
|
||||
token, err := uuid.Parse(idstr)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "cannot parse job token")
|
||||
return api.HTTPError(api.ErrorMalformedJobId)
|
||||
}
|
||||
|
||||
var body updateJobRequest
|
||||
var body api.UpdateJobRequest
|
||||
err = ctx.Bind(&body)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -360,21 +414,25 @@ func (h *apiHandlers) UpdateJob(ctx echo.Context, idstr string) error {
|
|||
if err != nil {
|
||||
switch err {
|
||||
case ErrInvalidToken:
|
||||
fallthrough
|
||||
return api.HTTPError(api.ErrorJobNotFound)
|
||||
case ErrJobNotRunning:
|
||||
return echo.NewHTTPError(http.StatusNotFound, "not found")
|
||||
return api.HTTPError(api.ErrorJobNotRunning)
|
||||
default:
|
||||
return err
|
||||
return api.HTTPError(api.ErrorFinishingJob)
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.JSON(http.StatusOK, updateJobResponse{})
|
||||
return ctx.JSON(http.StatusOK, api.UpdateJobResponse{
|
||||
Href: fmt.Sprintf("%s/jobs/%v", api.BasePath, token),
|
||||
Id: token.String(),
|
||||
Kind: "UpdateJobResponse",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *apiHandlers) UploadJobArtifact(ctx echo.Context, tokenstr string, name string) error {
|
||||
token, err := uuid.Parse(tokenstr)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "cannot parse job token")
|
||||
return api.HTTPError(api.ErrorMalformedJobId)
|
||||
}
|
||||
|
||||
request := ctx.Request()
|
||||
|
|
@ -382,19 +440,19 @@ func (h *apiHandlers) UploadJobArtifact(ctx echo.Context, tokenstr string, name
|
|||
if h.server.artifactsDir == "" {
|
||||
_, err := io.Copy(ioutil.Discard, request.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error discarding artifact: %v", err)
|
||||
return api.HTTPError(api.ErrorDiscardingArtifact)
|
||||
}
|
||||
return ctx.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
f, err := os.Create(path.Join(h.server.artifactsDir, "tmp", token.String(), name))
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create artifact file: %v", err)
|
||||
return api.HTTPError(api.ErrorDiscardingArtifact)
|
||||
}
|
||||
|
||||
_, err = io.Copy(f, request.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error writing artifact file: %v", err)
|
||||
return api.HTTPError(api.ErrorWritingArtifact)
|
||||
}
|
||||
|
||||
return ctx.NoContent(http.StatusOK)
|
||||
|
|
@ -410,12 +468,12 @@ func (b binder) Bind(i interface{}, ctx echo.Context) error {
|
|||
|
||||
contentType := request.Header["Content-Type"]
|
||||
if len(contentType) != 1 || contentType[0] != "application/json" {
|
||||
return echo.NewHTTPError(http.StatusUnsupportedMediaType, "request must be json-encoded")
|
||||
return api.HTTPError(api.ErrorUnsupportedMediaType)
|
||||
}
|
||||
|
||||
err := json.NewDecoder(request.Body).Decode(i)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "cannot parse request body: "+err.Error())
|
||||
return api.HTTPError(api.ErrorBodyDecodingError)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ func TestStatus(t *testing.T) {
|
|||
|
||||
server := newTestServer(t, tempdir)
|
||||
handler := server.Handler()
|
||||
test.TestRoute(t, handler, false, "GET", "/api/worker/v1/status", ``, http.StatusOK, `{"status":"OK"}`, "message")
|
||||
test.TestRoute(t, handler, false, "GET", "/api/worker/v1/status", ``, http.StatusOK, `{"status":"OK", "href": "/api/worker/v1/status", "kind":"Status"}`, "message", "id")
|
||||
}
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
|
|
@ -68,7 +68,7 @@ func TestErrors(t *testing.T) {
|
|||
for _, c := range cases {
|
||||
server := newTestServer(t, tempdir)
|
||||
handler := server.Handler()
|
||||
test.TestRoute(t, handler, false, c.Method, c.Path, c.Body, c.ExpectedStatus, "{}", "message")
|
||||
test.TestRoute(t, handler, false, c.Method, c.Path, c.Body, c.ExpectedStatus, `{"kind":"Error"}`, "message", "href", "operation_id", "reason", "id", "code")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ func TestCreate(t *testing.T) {
|
|||
|
||||
test.TestRoute(t, handler, false, "POST", "/api/worker/v1/jobs",
|
||||
fmt.Sprintf(`{"types":["osbuild"],"arch":"%s"}`, test_distro.TestArchName), http.StatusCreated,
|
||||
`{"type":"osbuild","args":{"manifest":{"pipeline":{},"sources":{}}}}`, "id", "location", "artifact_location")
|
||||
`{"kind":"RequestJob","href":"/api/worker/v1/jobs","type":"osbuild","args":{"manifest":{"pipeline":{},"sources":{}}}}`, "id", "location", "artifact_location")
|
||||
}
|
||||
|
||||
func TestCancel(t *testing.T) {
|
||||
|
|
@ -133,13 +133,13 @@ func TestCancel(t *testing.T) {
|
|||
require.Nil(t, dynamicArgs)
|
||||
|
||||
test.TestRoute(t, handler, false, "GET", fmt.Sprintf("/api/worker/v1/jobs/%s", token), `{}`, http.StatusOK,
|
||||
`{"canceled":false}`)
|
||||
fmt.Sprintf(`{"canceled":false,"href":"/api/worker/v1/jobs/%s","id":"%s","kind":"JobStatus"}`, token, token))
|
||||
|
||||
err = server.Cancel(jobId)
|
||||
require.NoError(t, err)
|
||||
|
||||
test.TestRoute(t, handler, false, "GET", fmt.Sprintf("/api/worker/v1/jobs/%s", token), `{}`, http.StatusOK,
|
||||
`{"canceled":true}`)
|
||||
fmt.Sprintf(`{"canceled":true,"href":"/api/worker/v1/jobs/%s","id":"%s","kind":"JobStatus"}`, token, token))
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
|
|
@ -173,8 +173,11 @@ func TestUpdate(t *testing.T) {
|
|||
require.NotNil(t, args)
|
||||
require.Nil(t, dynamicArgs)
|
||||
|
||||
test.TestRoute(t, handler, false, "PATCH", fmt.Sprintf("/api/worker/v1/jobs/%s", token), `{}`, http.StatusOK, `{}`)
|
||||
test.TestRoute(t, handler, false, "PATCH", fmt.Sprintf("/api/worker/v1/jobs/%s", token), `{}`, http.StatusNotFound, `*`)
|
||||
test.TestRoute(t, handler, false, "PATCH", fmt.Sprintf("/api/worker/v1/jobs/%s", token), `{}`, http.StatusOK,
|
||||
fmt.Sprintf(`{"href":"/api/worker/v1/jobs/%s","id":"%s","kind":"UpdateJobResponse"}`, token, token))
|
||||
test.TestRoute(t, handler, false, "PATCH", fmt.Sprintf("/api/worker/v1/jobs/%s", token), `{}`, http.StatusNotFound,
|
||||
`{"href":"/api/composer-worker/v1/errors/5","code":"COMPOSER-WORKER-5","id":"5","kind":"Error","message":"Token not found","reason":"Token not found"}`,
|
||||
"operation_id")
|
||||
}
|
||||
|
||||
func TestArgs(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ Obsoletes: osbuild-composer-koji <= 23
|
|||
# Remove when F33 is EOL
|
||||
sed -i "s/openapi3.Swagger/openapi3.T/;s/openapi3.NewSwaggerLoader().LoadSwaggerFromData/openapi3.NewLoader().LoadFromData/" internal/cloudapi/v1/openapi.v1.gen.go
|
||||
sed -i "s/openapi3.Swagger/openapi3.T/;s/openapi3.NewSwaggerLoader().LoadSwaggerFromData/openapi3.NewLoader().LoadFromData/" internal/cloudapi/v2/openapi.v2.gen.go
|
||||
sed -i "s/openapi3.Swagger/openapi3.T/;s/openapi3.NewSwaggerLoader().LoadSwaggerFromData/openapi3.NewLoader().LoadFromData/" internal/worker/api/api.gen.go
|
||||
%endif
|
||||
|
||||
%build
|
||||
|
|
|
|||
|
|
@ -1,21 +1,26 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
|
||||
echo "Query host"
|
||||
|
||||
COMMIT=$(git rev-parse HEAD)
|
||||
|
||||
|
||||
echo "Prepare host system"
|
||||
|
||||
sudo dnf -y install podman
|
||||
|
||||
|
||||
echo "Build container"
|
||||
|
||||
IMAGE_NAME="quay.io/osbuild/osbuild-composer-ubi-pr"
|
||||
IMAGE_TAG="${CI_COMMIT_SHA:-$(git rev-parse HEAD)}"
|
||||
|
||||
podman \
|
||||
build \
|
||||
"--file=distribution/Dockerfile-ubi" \
|
||||
"--tag=osbuild-composer:${COMMIT}" \
|
||||
--file="distribution/Dockerfile-ubi" \
|
||||
--tag="${IMAGE_NAME}:${IMAGE_TAG}" \
|
||||
--label="quay.expires-after=1w" \
|
||||
.
|
||||
|
||||
# Push to reuse later in the pipeline (see regression tests)
|
||||
BRANCH_NAME="${BRANCH_NAME:-${CI_COMMIT_BRANCH}}"
|
||||
if [[ "$BRANCH_NAME" =~ ^PR-[0-9]+$ ]]; then
|
||||
podman push \
|
||||
--creds "${QUAY_USERNAME}":"${QUAY_PASSWORD}" \
|
||||
"${IMAGE_NAME}:${IMAGE_TAG}"
|
||||
fi
|
||||
|
|
|
|||
125
test/cases/regression-old-worker-new-composer.sh
Normal file
125
test/cases/regression-old-worker-new-composer.sh
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Verify that an older worker (v33) is still compatible with this composer
|
||||
# version.
|
||||
#
|
||||
# Any tweaks to the worker api need to be backwards compatible.
|
||||
|
||||
set -exuo pipefail
|
||||
|
||||
WORKER_VERSION=8f21f0b873420a38a261d78a7df130f28b8e2867
|
||||
WORKER_RPM=osbuild-composer-worker-33-1.20210830git8f21f0b.el8.x86_64
|
||||
|
||||
# grab the repos from the test rpms
|
||||
REPOS=$(mktemp -d)
|
||||
sudo dnf -y install osbuild-composer-tests
|
||||
sudo cp -a /usr/share/tests/osbuild-composer/repositories "$REPOS/repositories"
|
||||
sudo cp -fv "$REPOS/repositories/rhel-8.json" "$REPOS/repositories/rhel-84.json"
|
||||
|
||||
# Remove the "new" worker
|
||||
sudo dnf remove -y osbuild-composer osbuild-composer-worker osbuild-composer-tests
|
||||
|
||||
function setup_repo {
|
||||
local project=$1
|
||||
local commit=$2
|
||||
local priority=${3:-10}
|
||||
echo "Setting up dnf repository for ${project} ${commit}"
|
||||
sudo tee "/etc/yum.repos.d/${project}.repo" << EOF
|
||||
[${project}]
|
||||
name=${project} ${commit}
|
||||
baseurl=http://osbuild-composer-repos.s3-website.us-east-2.amazonaws.com/${project}/rhel-8-cdn/x86_64/${commit}
|
||||
enabled=1
|
||||
gpgcheck=0
|
||||
priority=${priority}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Composer v33
|
||||
setup_repo osbuild-composer "$WORKER_VERSION" 20
|
||||
sudo dnf install -y osbuild-composer-worker podman composer-cli
|
||||
|
||||
# verify the right worker is installed just to be sure
|
||||
rpm -q "$WORKER_RPM"
|
||||
|
||||
# run container
|
||||
WELDR_DIR="$(mktemp -d)"
|
||||
WELDR_SOCK="$WELDR_DIR/api.socket"
|
||||
|
||||
sudo podman pull --creds "${QUAY_USERNAME}":"${QUAY_PASSWORD}" \
|
||||
"quay.io/osbuild/osbuild-composer-ubi-pr:${CI_COMMIT_SHA}"
|
||||
sudo podman run \
|
||||
--name=composer \
|
||||
-d \
|
||||
-v /etc/osbuild-composer:/etc/osbuild-composer:Z \
|
||||
-v "$REPOS/repositories":/usr/share/osbuild-composer/repositories:Z \
|
||||
-v "$WELDR_DIR:/run/weldr/":Z \
|
||||
-p 8700:8700 \
|
||||
"quay.io/osbuild/osbuild-composer-ubi-pr:${CI_COMMIT_SHA}" \
|
||||
--weldr-api --remote-worker-api \
|
||||
--no-local-worker-api --no-composer-api
|
||||
|
||||
# try starting a worker
|
||||
set +e
|
||||
sudo systemctl start osbuild-remote-worker@localhost:8700.service
|
||||
while ! sudo systemctl --quiet is-active osbuild-remote-worker@localhost:8700.service; do
|
||||
sudo systemctl status osbuild-remote-worker@localhost:8700.service
|
||||
sleep 1
|
||||
sudo systemctl start osbuild-remote-worker@localhost:8700.service
|
||||
done
|
||||
set -e
|
||||
|
||||
function log_on_exit() {
|
||||
sudo podman logs composer
|
||||
}
|
||||
|
||||
trap log_on_exit EXIT
|
||||
|
||||
BLUEPRINT_FILE=$(mktemp)
|
||||
COMPOSE_START=$(mktemp)
|
||||
COMPOSE_INFO=$(mktemp)
|
||||
tee "$BLUEPRINT_FILE" > /dev/null << EOF2
|
||||
name = "simple"
|
||||
version = "0.0.1"
|
||||
|
||||
[customizations]
|
||||
hostname = "simple"
|
||||
EOF2
|
||||
|
||||
sudo composer-cli -s "$WELDR_SOCK" blueprints push "$BLUEPRINT_FILE"
|
||||
sudo composer-cli -s "$WELDR_SOCK" blueprints depsolve simple
|
||||
sudo composer-cli -s "$WELDR_SOCK" --json compose start simple qcow2 | tee "${COMPOSE_START}"
|
||||
if rpm -q --quiet weldr-client; then
|
||||
COMPOSE_ID=$(jq -r '.body.build_id' "$COMPOSE_START")
|
||||
else
|
||||
COMPOSE_ID=$(jq -r '.build_id' "$COMPOSE_START")
|
||||
fi
|
||||
|
||||
# Wait for the compose to finish.
|
||||
echo "⏱ Waiting for compose to finish: ${COMPOSE_ID}"
|
||||
while true; do
|
||||
sudo composer-cli -s "$WELDR_SOCK" --json compose info "${COMPOSE_ID}" | tee "$COMPOSE_INFO" > /dev/null
|
||||
if rpm -q --quiet weldr-client; then
|
||||
COMPOSE_STATUS=$(jq -r '.body.queue_status' "$COMPOSE_INFO")
|
||||
else
|
||||
COMPOSE_STATUS=$(jq -r '.queue_status' "$COMPOSE_INFO")
|
||||
fi
|
||||
|
||||
# Is the compose finished?
|
||||
if [[ $COMPOSE_STATUS != RUNNING ]] && [[ $COMPOSE_STATUS != WAITING ]]; then
|
||||
break
|
||||
fi
|
||||
|
||||
# Wait 30 seconds and try again.
|
||||
sleep 30
|
||||
done
|
||||
|
||||
sudo journalctl -u osbuild-remote-worker@localhost:8700.service
|
||||
# Verify that the remote worker finished a job
|
||||
sudo journalctl -u osbuild-remote-worker@localhost:8700.service |
|
||||
grep -qE "Job [0-9a-fA-F-]+ finished"
|
||||
|
||||
# Did the compose finish with success?
|
||||
if [[ $COMPOSE_STATUS != FINISHED ]]; then
|
||||
echo "Something went wrong with the compose. 😢"
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -30,7 +30,7 @@ run_test_case () {
|
|||
echo "🏃🏻 Running test: ${TEST_NAME}"
|
||||
test_divider
|
||||
|
||||
if sudo "${1}" -test.v | tee "${LOGS_DIRECTORY}"/"${TEST_NAME}".log; then
|
||||
if sudo -E "${1}" -test.v | tee "${LOGS_DIRECTORY}"/"${TEST_NAME}".log; then
|
||||
PASSED_TESTS+=("$TEST_NAME")
|
||||
else
|
||||
FAILED_TESTS+=("$TEST_NAME")
|
||||
|
|
@ -44,6 +44,14 @@ run_test_case () {
|
|||
# Provision the software under test.
|
||||
/usr/libexec/osbuild-composer-test/provision.sh
|
||||
|
||||
ARCH=$(uname -m)
|
||||
# Only run this on x86 and rhel8 GA; since the container is based on the ubi
|
||||
# container, and we use the weldr api
|
||||
if [ "$ARCH" = "x86_64" ] && [ "$ID" = rhel ] && sudo subscription-manager status; then
|
||||
# Always run this one last as it force-installs an older worker
|
||||
TEST_CASES+=("regression-old-worker-new-composer.sh")
|
||||
fi
|
||||
|
||||
# Run test cases common for all distros.
|
||||
for TEST_CASE in "${TEST_CASES[@]}"; do
|
||||
run_test_case ${TESTS_PATH}/"$TEST_CASE"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue