api/errors/errors.go
package errors import ( "errors" "fmt" "net/http" "reflect" "strings" "code.cloudfoundry.org/korifi/controllers/webhooks/validation" "github.com/go-logr/logr" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1") type ApiError interface { Detail() string Title() string Code() int HttpStatus() int Unwrap() error Error() string} // LogAndReturn logs api errors at the info level and other errors at the// error level since api errors are expected recoverable conditions.// It returns the error for convenience.func LogAndReturn(logger logr.Logger, err error, msg string, keysAndValues ...interface{}) error { var apiError ApiError if errors.As(err, &apiError) { keysAndValues = append(keysAndValues, "reason", err) logger.Info(msg, keysAndValues...) } else { logger.Error(err, msg, keysAndValues...) } return err} type apiError struct { cause error detail string title string code int httpStatus int additionalDetails map[string]string} func (e apiError) Error() string { if e.cause == nil { return "unknown" } return e.cause.Error()} func (e apiError) Unwrap() error { return e.cause} func (e apiError) Detail() string { detail := e.detail for k, v := range e.additionalDetails { detail += fmt.Sprintf(" %s=%q", k, v) } return detail} func (e apiError) Title() string { return e.title} func (e apiError) Code() int { return e.code} func (e apiError) HttpStatus() int { return e.httpStatus} func toKeyValues(s ...string) map[string]string { result := map[string]string{} for i := 0; i < len(s); i += 2 { key := s[i] val := "" if i+1 < len(s) { val = s[i+1] } result[key] = val } return result} type UnprocessableEntityError struct { apiError} func NewUnprocessableEntityError(cause error, detail string) UnprocessableEntityError { return UnprocessableEntityError{ apiError{ cause: cause, title: "CF-UnprocessableEntity", detail: detail, code: 10008, httpStatus: http.StatusUnprocessableEntity, }, }} type MessageParseError struct { apiError} func NewMessageParseError(cause error) MessageParseError { return MessageParseError{ apiError{ cause: cause, title: "CF-MessageParseError", detail: "Request invalid due to parse error: invalid request body", code: 1001, httpStatus: http.StatusBadRequest, }, }} // UnknownError is a generic wrapper over an error Korifi cannot recover from.// Unknown errors should be only used by the presentation layer to present such// an error to the user. Other components (handlers, repositories, etc.) should// simply return the incoming error, it would be mapped to `UnknownError` by// the presentation layertype UnknownError struct { apiError} // NewUnknownError creates an UnknownError. One should generally not create// unknown errors as generic errors are automatically presented as unknown// errors to the userfunc NewUnknownError(cause error) UnknownError { return UnknownError{ apiError{ cause: cause, title: "UnknownError", detail: "An unknown error occurred.", code: 10001, httpStatus: http.StatusInternalServerError, }, }} type NotFoundError struct { apiError} func NewNotFoundError(cause error, resourceType string, additionalDetails ...string) NotFoundError { return NotFoundError{ apiError{ cause: cause, title: "CF-ResourceNotFound", detail: fmt.Sprintf("%s not found. Ensure it exists and you have access to it.", resourceType), additionalDetails: toKeyValues(additionalDetails...), code: 10010, httpStatus: http.StatusNotFound, }, }} type EndpointNotFoundError struct { apiError} func NewEndpointNotFoundError() EndpointNotFoundError { return EndpointNotFoundError{ apiError{ title: "CF-NotFound", detail: "Unknown request", code: 10000, httpStatus: http.StatusNotFound, }, }} type InvalidAuthError struct { apiError} func NewInvalidAuthError(cause error) InvalidAuthError { return InvalidAuthError{ apiError{ cause: cause, title: "CF-InvalidAuthToken", detail: "Invalid Auth Token", code: 1000, httpStatus: http.StatusUnauthorized, }, }} type NotAuthenticatedError struct { apiError} func NewNotAuthenticatedError(cause error) NotAuthenticatedError { return NotAuthenticatedError{ apiError{ cause: cause, title: "CF-NotAuthenticated", detail: "Authentication error", code: 10002, httpStatus: http.StatusUnauthorized, }, }} type ForbiddenError struct { apiError resourceType string} func (e ForbiddenError) ResourceType() string { return e.resourceType} func NewForbiddenError(cause error, resourceType string) ForbiddenError { return ForbiddenError{ apiError: apiError{ cause: cause, title: "CF-NotAuthorized", detail: "You are not authorized to perform the requested action", code: 10003, httpStatus: http.StatusForbidden, }, resourceType: resourceType, }} type BadQueryParamValueError struct { apiError} func NewBadQueryParamValueError(key string, validValues ...string) BadQueryParamValueError { return BadQueryParamValueError{ apiError: apiError{ title: "CF-BadQueryParameter", detail: fmt.Sprintf("The query parameter is invalid: %s can only be: %s", key, quotedCommaSeparatedList(validValues)), code: 10005, httpStatus: http.StatusBadRequest, }, }} type UnknownKeyError struct { apiError} func NewUnknownKeyError(cause error, validKeys []string) UnknownKeyError { return UnknownKeyError{ apiError: apiError{ cause: cause, title: "CF-BadQueryParameter", detail: fmt.Sprintf("The query parameter is invalid: Valid parameters are: %s", quotedCommaSeparatedList(validKeys)), code: 10005, httpStatus: http.StatusBadRequest, }, }} func quotedCommaSeparatedList(in []string) string { var out []string for _, i := range in { out = append(out, fmt.Sprintf("'%s'", i)) } return strings.Join(out, ", ")} type UniquenessError struct { apiError} func NewUniquenessError(cause error, detail string) UniquenessError { return UniquenessError{ apiError: apiError{ cause: cause, title: "CF-UniquenessError", detail: detail, code: 10016, httpStatus: http.StatusUnprocessableEntity, }, }} type InvalidRequestError struct { apiError} func NewInvalidRequestError(cause error, detail string) InvalidRequestError { return InvalidRequestError{ apiError: apiError{ cause: cause, title: "CF-InvalidRequest", detail: detail, code: 10004, httpStatus: http.StatusBadRequest, }, }} type PackageBitsAlreadyUploadedError struct { apiError} func NewPackageBitsAlreadyUploadedError(cause error) PackageBitsAlreadyUploadedError { return PackageBitsAlreadyUploadedError{ apiError: apiError{ cause: cause, title: "CF-PackageBitsAlreadyUploaded", detail: "Bits may be uploaded only once. Create a new package to upload different bits.", code: 150004, httpStatus: http.StatusBadRequest, }, }} type BlobstoreUnavailableError struct { apiError} func NewBlobstoreUnavailableError(cause error) BlobstoreUnavailableError { return BlobstoreUnavailableError{ apiError: apiError{ cause: cause, title: "CF-BlobstoreUnavailable", detail: "Error uploading source package to the container registry", code: 150006, httpStatus: http.StatusBadGateway, }, }} type ResourceNotReadyError struct { apiError} func NewResourceNotReadyError(cause error) ResourceNotReadyError { return ResourceNotReadyError{ apiError: apiError{ cause: cause, title: "CF-ResourceNotReady", detail: cause.Error(), code: 420000, httpStatus: http.StatusInternalServerError, }, }} type RollingDeployNotSupportedError struct { apiError} func NewRollingDeployNotSupportedError(runnerName string) RollingDeployNotSupportedError { detail := fmt.Sprintf("The configured runner '%s' does not support rolling deploys", runnerName) return RollingDeployNotSupportedError{ apiError: apiError{ title: "CF-RollingDeployNotSupported", detail: detail, code: 42000, httpStatus: http.StatusBadRequest, }, }} func FromK8sError(err error, resourceType string) error { if webhookValidationError, ok := validation.WebhookErrorToValidationError(err); ok { return NewUnprocessableEntityError(err, webhookValidationError.GetMessage()) } switch { case k8serrors.IsUnauthorized(err): return NewInvalidAuthError(err) case k8serrors.IsNotFound(err): return NewNotFoundError(err, resourceType) case k8serrors.IsForbidden(err): return NewForbiddenError(err, resourceType) case k8serrors.IsInvalid(err): cause, ok := k8serrors.StatusCause(err, metav1.CauseTypeFieldValueInvalid) if ok { return NewUnprocessableEntityError(err, fmt.Sprintf("%s is invalid: %s", cause.Field, cause.Message)) } return NewUnprocessableEntityError(err, resourceType) default: return err }} func AsUnprocessableEntity(err error, detail string, errTypes ...ApiError) error { if err == nil { return nil } for i := range errTypes { // At this point in time the errors in the errType array are downgraded // to `ApiError`. This means that pointers to api errors that only // embed `apiError` are assignable to each other. Therefore `errors.As` // would return `true` and would change the initial value type of the // array entry. That is why we need to get the "desiredType" first and // compare it to the type that has been set by `errors.As` desiredErrType := reflect.ValueOf(errTypes[i]).Type() if !errors.As(err, &errTypes[i]) { continue } asErrType := reflect.ValueOf(errTypes[i]).Type() if asErrType != desiredErrType { continue } return NewUnprocessableEntityError(errTypes[i].Unwrap(), detail) } return err} func ForbiddenAsNotFound(err error) error { var forbiddenErr ForbiddenError if errors.As(err, &forbiddenErr) { return NewNotFoundError(forbiddenErr.Unwrap(), forbiddenErr.ResourceType()) } return err} // DropletForbiddenAsNotFound is a special case due to the CF CLI expecting the error message "Droplet not found" exactly instead of the generic case// https://github.com/cloudfoundry/korifi/issues/965func DropletForbiddenAsNotFound(err error) error { var forbiddenErr ForbiddenError if errors.As(err, &forbiddenErr) { return NotFoundError{ apiError{ cause: forbiddenErr.Unwrap(), title: "CF-ResourceNotFound", detail: "Droplet not found", code: 10010, httpStatus: http.StatusNotFound, }, } } var notFoundErr NotFoundError if errors.As(err, ¬FoundErr) { return NotFoundError{ apiError{ cause: notFoundErr.Unwrap(), title: "CF-ResourceNotFound", detail: "Droplet not found", code: 10010, httpStatus: http.StatusNotFound, }, } } return err}