debian-forge-composer/internal/cloudapi/v2/errors.go
Brian C. Lane 7438e29375 cloudapi: Create a compose using the blueprint data
If the request includes a blueprint (and not customizations) it uses
that blueprint for the compose.
2024-01-15 11:48:35 +01:00

325 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 {
se := find(code)
he := echo.NewHTTPError(se.httpStatus, detailsError{code, ""})
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(code ServiceErrorCode, serviceError *serviceError, c echo.Context, details *interface{}) *Error {
se := serviceError
if se == nil {
se = find(code)
}
operationID, ok := c.Get("operationID").(string)
if !ok || operationID == "" {
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,
Details: details,
}
}
// 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.code, &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) {
// don't anticipate serviceerrorcode, instead check what type it is
if !c.Response().Committed {
var err error
sec := find(code)
apiErr := APIError(code, sec, c, details)
if sec.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(sec.httpStatus)
} else {
err = c.JSON(sec.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
}
var det *interface{}
if err.details != nil {
det = &err.details
}
doResponse(det, err.errorCode, c, he.Internal)
}