cloudfoundry/korifi

View on GitHub
api/errors/errors.go

Summary

Maintainability
A
0 mins
Test Coverage
A
92%
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 layer
type 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 user
func 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/965
func 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, &notFoundErr) {
return NotFoundError{
apiError{
cause: notFoundErr.Unwrap(),
title: "CF-ResourceNotFound",
detail: "Droplet not found",
code: 10010,
httpStatus: http.StatusNotFound,
},
}
}
return err
}