debian-forge-composer/internal/worker/api/errors.go
sanne 2f328b0e97 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
2021-09-27 13:10:05 +01:00

186 lines
6.7 KiB
Go

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