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:
sanne 2021-08-31 11:47:27 +02:00 committed by Tom Gundersen
parent 5e206322a2
commit 2f328b0e97
14 changed files with 816 additions and 209 deletions

View file

@ -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

View file

@ -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)

View file

@ -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
}

View file

@ -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

View 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)
}

View file

@ -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'

View file

@ -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)
}

View file

@ -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"`
}

View file

@ -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

View file

@ -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) {

View file

@ -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

View file

@ -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

View 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

View file

@ -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"