In case details aren't explicitly given, and the internal error is set, include the internal error message in the details.
322 lines
15 KiB
Go
322 lines
15 KiB
Go
package v2
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
const (
|
|
ErrorCodePrefix = "IMAGE-BUILDER-COMPOSER-"
|
|
ErrorHREF = "/api/image-builder-composer/v2/errors"
|
|
|
|
// ocm-sdk sends ErrorUnauthenticated with id 401 & code COMPOSER-401
|
|
ErrorUnauthenticated ServiceErrorCode = 401
|
|
|
|
ErrorUnauthorized ServiceErrorCode = 2
|
|
ErrorUnsupportedMediaType ServiceErrorCode = 3
|
|
ErrorUnsupportedDistribution ServiceErrorCode = 4
|
|
ErrorUnsupportedArchitecture ServiceErrorCode = 5
|
|
ErrorUnsupportedImageType ServiceErrorCode = 6
|
|
ErrorInvalidRepository ServiceErrorCode = 7
|
|
ErrorDNFError ServiceErrorCode = 8
|
|
ErrorInvalidOSTreeRef ServiceErrorCode = 9
|
|
ErrorInvalidOSTreeRepo ServiceErrorCode = 10
|
|
ErrorFailedToMakeManifest ServiceErrorCode = 11
|
|
ErrorInvalidComposeId ServiceErrorCode = 14
|
|
ErrorComposeNotFound ServiceErrorCode = 15
|
|
ErrorInvalidErrorId ServiceErrorCode = 16
|
|
ErrorErrorNotFound ServiceErrorCode = 17
|
|
ErrorInvalidPageParam ServiceErrorCode = 18
|
|
ErrorInvalidSizeParam ServiceErrorCode = 19
|
|
ErrorBodyDecodingError ServiceErrorCode = 20
|
|
ErrorResourceNotFound ServiceErrorCode = 21
|
|
ErrorMethodNotAllowed ServiceErrorCode = 22
|
|
ErrorNotAcceptable ServiceErrorCode = 23
|
|
ErrorNoBaseURLInPayloadRepository ServiceErrorCode = 24
|
|
ErrorInvalidNumberOfImageBuilds ServiceErrorCode = 25
|
|
ErrorInvalidJobType ServiceErrorCode = 26
|
|
ErrorInvalidOSTreeParams ServiceErrorCode = 27
|
|
ErrorTenantNotFound ServiceErrorCode = 28
|
|
ErrorNoGPGKey ServiceErrorCode = 29
|
|
ErrorValidationFailed ServiceErrorCode = 30
|
|
ErrorComposeBadState ServiceErrorCode = 31
|
|
ErrorUnsupportedImage ServiceErrorCode = 32
|
|
ErrorInvalidImageFromComposeId ServiceErrorCode = 33
|
|
ErrorImageNotFound ServiceErrorCode = 34
|
|
ErrorInvalidCustomization ServiceErrorCode = 35
|
|
ErrorLocalSaveNotEnabled ServiceErrorCode = 36
|
|
ErrorInvalidPartitioningMode ServiceErrorCode = 37
|
|
ErrorInvalidUploadTarget ServiceErrorCode = 38
|
|
ErrorBlueprintOrCustomNotBoth ServiceErrorCode = 39
|
|
|
|
// Internal errors, these are bugs
|
|
ErrorFailedToInitializeBlueprint ServiceErrorCode = 1000
|
|
ErrorFailedToGenerateManifestSeed ServiceErrorCode = 1001
|
|
ErrorFailedToDepsolve ServiceErrorCode = 1002
|
|
ErrorJSONMarshallingError ServiceErrorCode = 1003
|
|
ErrorJSONUnMarshallingError ServiceErrorCode = 1004
|
|
ErrorEnqueueingJob ServiceErrorCode = 1005
|
|
ErrorSeveralUploadTargets ServiceErrorCode = 1006
|
|
ErrorUnknownUploadTarget ServiceErrorCode = 1007
|
|
ErrorFailedToLoadOpenAPISpec ServiceErrorCode = 1008
|
|
ErrorFailedToParseManifestVersion ServiceErrorCode = 1009
|
|
ErrorUnknownManifestVersion ServiceErrorCode = 1010
|
|
ErrorUnableToConvertOSTreeCommitStageMetadata ServiceErrorCode = 1011
|
|
ErrorMalformedOSBuildJobResult ServiceErrorCode = 1012
|
|
ErrorGettingDepsolveJobStatus ServiceErrorCode = 1013
|
|
ErrorDepsolveJobCanceled ServiceErrorCode = 1014
|
|
ErrorUnexpectedNumberOfImageBuilds ServiceErrorCode = 1015
|
|
ErrorGettingBuildDependencyStatus ServiceErrorCode = 1016
|
|
ErrorGettingOSBuildJobStatus ServiceErrorCode = 1017
|
|
ErrorGettingAWSEC2JobStatus ServiceErrorCode = 1018
|
|
ErrorGettingJobType ServiceErrorCode = 1019
|
|
ErrorTenantNotInContext ServiceErrorCode = 1020
|
|
|
|
// 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
|
|
|
|
// Maps ServiceErrorcode to a reason and http code
|
|
func getServiceErrors() serviceErrors {
|
|
return serviceErrors{
|
|
serviceError{ErrorUnauthenticated, http.StatusUnauthorized, "Account authentication could not be verified"},
|
|
serviceError{ErrorUnauthorized, http.StatusForbidden, "Account is unauthorized to perform this action"},
|
|
serviceError{ErrorUnsupportedMediaType, http.StatusUnsupportedMediaType, "Only 'application/json' content is supported"},
|
|
serviceError{ErrorUnsupportedDistribution, http.StatusBadRequest, "Unsupported distribution"},
|
|
serviceError{ErrorUnsupportedArchitecture, http.StatusBadRequest, "Unsupported architecture"},
|
|
serviceError{ErrorUnsupportedImageType, http.StatusBadRequest, "Unsupported image type"},
|
|
serviceError{ErrorInvalidRepository, http.StatusBadRequest, "Must specify baseurl, mirrorlist, or metalink"},
|
|
serviceError{ErrorDNFError, http.StatusBadRequest, "Failed to depsolve packages"},
|
|
serviceError{ErrorInvalidOSTreeRef, http.StatusBadRequest, "Invalid OSTree ref"},
|
|
serviceError{ErrorInvalidOSTreeRepo, http.StatusBadRequest, "Error resolving OSTree repo"},
|
|
serviceError{ErrorFailedToMakeManifest, http.StatusBadRequest, "Failed to get manifest"},
|
|
serviceError{ErrorInvalidComposeId, http.StatusBadRequest, "Invalid format for compose id"},
|
|
serviceError{ErrorComposeNotFound, http.StatusNotFound, "Compose with given id not found"},
|
|
serviceError{ErrorInvalidErrorId, http.StatusBadRequest, "Invalid format for error id, it should be an integer as a string"},
|
|
serviceError{ErrorErrorNotFound, http.StatusNotFound, "Error with given id not found"},
|
|
serviceError{ErrorInvalidPageParam, http.StatusBadRequest, "Invalid format for page param, it should be an integer as a string"},
|
|
serviceError{ErrorInvalidSizeParam, http.StatusBadRequest, "Invalid format for size param, it should be an integer as a string"},
|
|
serviceError{ErrorBodyDecodingError, http.StatusBadRequest, "Malformed json, unable to decode body"},
|
|
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{ErrorNoBaseURLInPayloadRepository, http.StatusBadRequest, "BaseURL must be specified for payload repositories"},
|
|
serviceError{ErrorInvalidJobType, http.StatusNotFound, "Job with given id has an invalid type"},
|
|
serviceError{ErrorInvalidNumberOfImageBuilds, http.StatusBadRequest, "Compose request has unsupported number of image builds"},
|
|
serviceError{ErrorInvalidOSTreeParams, http.StatusBadRequest, "Invalid OSTree parameters or parameter combination"},
|
|
serviceError{ErrorTenantNotFound, http.StatusBadRequest, "Tenant not found in JWT claims"},
|
|
serviceError{ErrorNoGPGKey, http.StatusBadRequest, "Invalid repository, when check_gpg is set, gpgkey must be specified"},
|
|
serviceError{ErrorValidationFailed, http.StatusBadRequest, "Request could not be validated"},
|
|
serviceError{ErrorComposeBadState, http.StatusBadRequest, "Compose is running or has failed"},
|
|
serviceError{ErrorUnsupportedImage, http.StatusBadRequest, "This compose doesn't support the creation of multiple images"},
|
|
serviceError{ErrorInvalidImageFromComposeId, http.StatusBadRequest, "Invalid format for image id"},
|
|
serviceError{ErrorImageNotFound, http.StatusBadRequest, "Image with given id not found"},
|
|
serviceError{ErrorInvalidCustomization, http.StatusBadRequest, "Invalid image customization"},
|
|
serviceError{ErrorLocalSaveNotEnabled, http.StatusBadRequest, "local_save is not enabled"},
|
|
serviceError{ErrorInvalidPartitioningMode, http.StatusBadRequest, "Requested partitioning mode is invalid"},
|
|
serviceError{ErrorInvalidUploadTarget, http.StatusBadRequest, "Invalid upload target for image type"},
|
|
serviceError{ErrorBlueprintOrCustomNotBoth, http.StatusBadRequest, "Invalid request, include blueprint or customizations, not both"},
|
|
|
|
serviceError{ErrorFailedToInitializeBlueprint, http.StatusInternalServerError, "Failed to initialize blueprint"},
|
|
serviceError{ErrorFailedToGenerateManifestSeed, http.StatusInternalServerError, "Failed to generate manifest seed"},
|
|
serviceError{ErrorFailedToDepsolve, http.StatusInternalServerError, "Failed to depsolve packages"},
|
|
serviceError{ErrorJSONMarshallingError, http.StatusInternalServerError, "Failed to marshal struct"},
|
|
serviceError{ErrorJSONUnMarshallingError, http.StatusInternalServerError, "Failed to unmarshal struct"},
|
|
serviceError{ErrorEnqueueingJob, http.StatusInternalServerError, "Failed to enqueue job"},
|
|
serviceError{ErrorSeveralUploadTargets, http.StatusInternalServerError, "Compose has more than one upload target"},
|
|
serviceError{ErrorUnknownUploadTarget, http.StatusInternalServerError, "Compose has unknown upload target"},
|
|
serviceError{ErrorFailedToLoadOpenAPISpec, http.StatusInternalServerError, "Unable to load openapi spec"},
|
|
serviceError{ErrorFailedToParseManifestVersion, http.StatusInternalServerError, "Unable to parse manifest version"},
|
|
serviceError{ErrorUnknownManifestVersion, http.StatusInternalServerError, "Unknown manifest version"},
|
|
serviceError{ErrorUnableToConvertOSTreeCommitStageMetadata, http.StatusInternalServerError, "Unable to convert ostree commit stage metadata"},
|
|
serviceError{ErrorMalformedOSBuildJobResult, http.StatusInternalServerError, "OSBuildJobResult does not have expected fields set"},
|
|
serviceError{ErrorGettingDepsolveJobStatus, http.StatusInternalServerError, "Unable to get depsolve job status"},
|
|
serviceError{ErrorDepsolveJobCanceled, http.StatusInternalServerError, "Depsolve job was cancelled"},
|
|
serviceError{ErrorUnexpectedNumberOfImageBuilds, http.StatusInternalServerError, "Compose has unexpected number of image builds"},
|
|
serviceError{ErrorGettingBuildDependencyStatus, http.StatusInternalServerError, "Error checking status of build job dependencies"},
|
|
serviceError{ErrorGettingOSBuildJobStatus, http.StatusInternalServerError, "Unable to get osbuild job status"},
|
|
serviceError{ErrorGettingAWSEC2JobStatus, http.StatusInternalServerError, "Unable to get ec2 job status"},
|
|
serviceError{ErrorGettingJobType, http.StatusInternalServerError, "Unable to get job type of existing job"},
|
|
serviceError{ErrorTenantNotInContext, http.StatusInternalServerError, "Unable to retrieve tenant from request context"},
|
|
|
|
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 {
|
|
de := detailsError{code, ""}
|
|
if internalErr != nil {
|
|
de.details = internalErr.Error()
|
|
}
|
|
|
|
he := echo.NewHTTPError(find(code).httpStatus, de)
|
|
if internalErr != nil {
|
|
he.Internal = internalErr
|
|
}
|
|
return he
|
|
}
|
|
|
|
type detailsError struct {
|
|
errorCode ServiceErrorCode
|
|
details interface{}
|
|
}
|
|
|
|
// instead of sending a ServiceErrorCode as he.Message, send the validation error string (see above)
|
|
func HTTPErrorWithDetails(code ServiceErrorCode, internalErr error, details string) error {
|
|
se := find(code)
|
|
he := echo.NewHTTPError(se.httpStatus, detailsError{code, details})
|
|
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(serviceError *serviceError, c echo.Context, details interface{}) *Error {
|
|
operationID, ok := c.Get("operationID").(string)
|
|
if !ok || operationID == "" {
|
|
serviceError = find(ErrorMalformedOperationID)
|
|
}
|
|
|
|
apiErr := &Error{
|
|
ObjectReference: ObjectReference{
|
|
Href: fmt.Sprintf("%s/%d", ErrorHREF, serviceError.code),
|
|
Id: fmt.Sprintf("%d", serviceError.code),
|
|
Kind: "Error",
|
|
},
|
|
Code: fmt.Sprintf("%s%d", ErrorCodePrefix, serviceError.code),
|
|
OperationId: operationID, // set operation id from context
|
|
Reason: serviceError.reason,
|
|
}
|
|
if details != nil {
|
|
apiErr.Details = &details
|
|
}
|
|
return apiErr
|
|
}
|
|
|
|
// Helper to make the ErrorList as defined in openapi.v2.yml
|
|
func APIErrorList(page int, pageSize int, c echo.Context) *ErrorList {
|
|
list := &ErrorList{
|
|
List: List{
|
|
Kind: "ErrorList",
|
|
Page: page,
|
|
Size: 0,
|
|
Total: len(getServiceErrors()),
|
|
},
|
|
Items: []Error{},
|
|
}
|
|
|
|
if page < 0 || pageSize < 0 {
|
|
return list
|
|
}
|
|
|
|
min := func(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
errs := getServiceErrors()[min(page*pageSize, len(getServiceErrors())):min(((page+1)*pageSize), len(getServiceErrors()))]
|
|
for _, e := range errs {
|
|
// Implicit memory alasing doesn't couse any bug in this case
|
|
/* #nosec G601 */
|
|
list.Items = append(list.Items, *APIError(&e, c, nil))
|
|
}
|
|
list.Size = len(list.Items)
|
|
return list
|
|
}
|
|
|
|
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 (s *Server) HTTPErrorHandler(echoError error, c echo.Context) {
|
|
doResponse := func(details interface{}, code ServiceErrorCode, c echo.Context, internal error) {
|
|
if !c.Response().Committed {
|
|
var err error
|
|
se := find(code)
|
|
apiErr := APIError(se, c, details)
|
|
|
|
if se.httpStatus == http.StatusInternalServerError {
|
|
errMsg := fmt.Sprintf("Internal server error. Code: %s, OperationId: %s", apiErr.Code, apiErr.OperationId)
|
|
|
|
if internal != nil {
|
|
errMsg += fmt.Sprintf(", InternalError: %v", internal)
|
|
}
|
|
|
|
c.Logger().Error(errMsg)
|
|
}
|
|
|
|
if c.Request().Method == http.MethodHead {
|
|
err = c.NoContent(se.httpStatus)
|
|
} else {
|
|
err = c.JSON(se.httpStatus, apiErr)
|
|
}
|
|
if err != nil {
|
|
c.Logger().Errorf("Failed to return error response: %v", err)
|
|
}
|
|
} else {
|
|
c.Logger().Infof("Failed to return error response, response already committed: %d", code)
|
|
}
|
|
}
|
|
|
|
he, ok := echoError.(*echo.HTTPError)
|
|
if !ok {
|
|
c.Logger().Errorf("ErrorNotHTTPError %v", echoError)
|
|
doResponse(nil, ErrorNotHTTPError, c, echoError)
|
|
return
|
|
}
|
|
|
|
err, ok := he.Message.(detailsError)
|
|
if !ok {
|
|
// No service code was set, so Echo threw this error
|
|
doResponse(nil, apiErrorFromEchoError(he), c, he.Internal)
|
|
return
|
|
}
|
|
doResponse(err.details, err.errorCode, c, he.Internal)
|
|
}
|