github.com/LukasHeimann/cloudfoundrycli@v7.1.0+incompatible/api/cloudcontroller/ccv3/errors.go (about)

     1  package ccv3
     2  
     3  import (
     4  	"encoding/json"
     5  	"net/http"
     6  	"regexp"
     7  	"strings"
     8  
     9  	"code.cloudfoundry.org/cli/api/cloudcontroller"
    10  	"code.cloudfoundry.org/cli/api/cloudcontroller/ccerror"
    11  )
    12  
    13  const taskWorkersUnavailable = "CF-TaskWorkersUnavailable"
    14  
    15  // errorWrapper is the wrapper that converts responses with 4xx and 5xx status
    16  // codes to an error.
    17  type errorWrapper struct {
    18  	connection cloudcontroller.Connection
    19  }
    20  
    21  func newErrorWrapper() *errorWrapper {
    22  	return new(errorWrapper)
    23  }
    24  
    25  // Make creates a connection in the wrapped connection and handles errors
    26  // that it returns.
    27  func (e *errorWrapper) Make(request *cloudcontroller.Request, passedResponse *cloudcontroller.Response) error {
    28  	err := e.connection.Make(request, passedResponse)
    29  
    30  	if rawHTTPStatusErr, ok := err.(ccerror.RawHTTPStatusError); ok {
    31  		if rawHTTPStatusErr.StatusCode >= http.StatusInternalServerError {
    32  			return convert500(rawHTTPStatusErr)
    33  		}
    34  		return convert400(rawHTTPStatusErr, request)
    35  	}
    36  	return err
    37  }
    38  
    39  // Wrap wraps a Cloud Controller connection in this error handling wrapper.
    40  func (e *errorWrapper) Wrap(innerconnection cloudcontroller.Connection) cloudcontroller.Connection {
    41  	e.connection = innerconnection
    42  	return e
    43  }
    44  
    45  func convert400(rawHTTPStatusErr ccerror.RawHTTPStatusError, request *cloudcontroller.Request) error {
    46  	firstErr, errorResponse, err := unmarshalFirstV3Error(rawHTTPStatusErr)
    47  	if err != nil {
    48  		return err
    49  	}
    50  
    51  	if len(errorResponse.Errors) > 1 {
    52  		return ccerror.MultiError{Errors: errorResponse.Errors, ResponseCode: rawHTTPStatusErr.StatusCode}
    53  	}
    54  
    55  	switch rawHTTPStatusErr.StatusCode {
    56  	case http.StatusBadRequest: // 400
    57  		return handleBadRequest(firstErr, request)
    58  	case http.StatusUnauthorized: // 401
    59  		if firstErr.Title == "CF-InvalidAuthToken" {
    60  			return ccerror.InvalidAuthTokenError{Message: firstErr.Detail}
    61  		}
    62  		return ccerror.UnauthorizedError{Message: firstErr.Detail}
    63  	case http.StatusForbidden: // 403
    64  		return ccerror.ForbiddenError{Message: firstErr.Detail}
    65  	case http.StatusNotFound: // 404
    66  		return handleNotFound(firstErr, request)
    67  	case http.StatusUnprocessableEntity: // 422
    68  		return handleUnprocessableEntity(firstErr)
    69  	case http.StatusServiceUnavailable: // 503
    70  		if firstErr.Title == taskWorkersUnavailable {
    71  			return ccerror.TaskWorkersUnavailableError{Message: firstErr.Detail}
    72  		}
    73  		return ccerror.ServiceUnavailableError{Message: firstErr.Detail}
    74  	default:
    75  		return ccerror.V3UnexpectedResponseError{
    76  			ResponseCode:    rawHTTPStatusErr.StatusCode,
    77  			RequestIDs:      rawHTTPStatusErr.RequestIDs,
    78  			V3ErrorResponse: errorResponse,
    79  		}
    80  	}
    81  }
    82  
    83  func convert500(rawHTTPStatusErr ccerror.RawHTTPStatusError) error {
    84  	switch rawHTTPStatusErr.StatusCode {
    85  	case http.StatusServiceUnavailable: // 503
    86  		firstErr, _, err := unmarshalFirstV3Error(rawHTTPStatusErr)
    87  		if err != nil {
    88  			return err
    89  		}
    90  		if firstErr.Title == taskWorkersUnavailable {
    91  			return ccerror.TaskWorkersUnavailableError{Message: firstErr.Detail}
    92  		}
    93  		return ccerror.ServiceUnavailableError{Message: firstErr.Detail}
    94  	default:
    95  		return ccerror.V3UnexpectedResponseError{
    96  			ResponseCode: rawHTTPStatusErr.StatusCode,
    97  			RequestIDs:   rawHTTPStatusErr.RequestIDs,
    98  			V3ErrorResponse: ccerror.V3ErrorResponse{
    99  				Errors: []ccerror.V3Error{{
   100  					Detail: string(rawHTTPStatusErr.RawResponse),
   101  				}},
   102  			},
   103  		}
   104  	}
   105  }
   106  
   107  func handleBadRequest(errorResponse ccerror.V3Error, _ *cloudcontroller.Request) error {
   108  	switch errorResponse.Detail {
   109  	case "Bad request: Cannot stage package whose state is not ready.":
   110  		return ccerror.InvalidStateError{}
   111  	default:
   112  		return ccerror.BadRequestError{Message: errorResponse.Detail}
   113  
   114  	}
   115  }
   116  
   117  func handleNotFound(errorResponse ccerror.V3Error, request *cloudcontroller.Request) error {
   118  	switch errorResponse.Detail {
   119  	case "App not found":
   120  		return ccerror.ApplicationNotFoundError{}
   121  	case "Droplet not found":
   122  		return ccerror.DropletNotFoundError{}
   123  	case "Deployment not found":
   124  		return ccerror.DeploymentNotFoundError{}
   125  	case "Feature flag not found":
   126  		return ccerror.FeatureFlagNotFoundError{}
   127  	case "Instance not found":
   128  		return ccerror.InstanceNotFoundError{}
   129  	case "Process not found":
   130  		return ccerror.ProcessNotFoundError{}
   131  	case "User not found":
   132  		return ccerror.UserNotFoundError{}
   133  	case "Unknown request":
   134  		return ccerror.APINotFoundError{URL: request.URL.String()}
   135  	default:
   136  		return ccerror.ResourceNotFoundError{Message: errorResponse.Detail}
   137  	}
   138  }
   139  
   140  func handleUnprocessableEntity(errorResponse ccerror.V3Error) error {
   141  	//idea to make route already exist error flexible for all relevant error cases
   142  	errorString := errorResponse.Detail
   143  	err := ccerror.UnprocessableEntityError{Message: errorResponse.Detail}
   144  	appNameTakenRegexp := regexp.MustCompile(`App with the name '.*' already exists\.`)
   145  	orgNameTakenRegexp := regexp.MustCompile(`Organization '.*' already exists\.`)
   146  	roleExistsRegexp := regexp.MustCompile(`User '.*' already has '.*' role.*`)
   147  	quotaExistsRegexp := regexp.MustCompile(`.* Quota '.*' already exists\.`)
   148  	securityGroupExistsRegexp := regexp.MustCompile(`Security group with name '.*' already exists\.`)
   149  
   150  	// boolean switch case with partial/regex string matchers
   151  	switch {
   152  	case appNameTakenRegexp.MatchString(errorString) || strings.Contains(errorString, "name must be unique in space"):
   153  		return ccerror.NameNotUniqueInSpaceError{UnprocessableEntityError: err}
   154  	case strings.Contains(errorString,
   155  		"Name must be unique per organization"):
   156  		return ccerror.NameNotUniqueInOrgError{}
   157  	case strings.Contains(errorString,
   158  		"Route already exists"):
   159  		return ccerror.RouteNotUniqueError{UnprocessableEntityError: err}
   160  	case strings.Contains(errorString,
   161  		"Buildpack must be an existing admin buildpack or a valid git URI"):
   162  		return ccerror.InvalidBuildpackError{}
   163  	case strings.Contains(errorString,
   164  		"Assign a droplet before starting this app."):
   165  		return ccerror.InvalidStartError{}
   166  	case orgNameTakenRegexp.MatchString(errorString):
   167  		return ccerror.OrganizationNameTakenError{UnprocessableEntityError: err}
   168  	case roleExistsRegexp.MatchString(errorString):
   169  		return ccerror.RoleAlreadyExistsError{UnprocessableEntityError: err}
   170  	case quotaExistsRegexp.MatchString(errorString):
   171  		return ccerror.QuotaAlreadyExists{Message: err.Message}
   172  	case securityGroupExistsRegexp.MatchString(errorString):
   173  		return ccerror.SecurityGroupAlreadyExists{Message: err.Message}
   174  	case strings.Contains(errorString,
   175  		"Ensure the space is bound to this security group."):
   176  		return ccerror.SecurityGroupNotBound{Message: err.Message}
   177  	default:
   178  		return err
   179  	}
   180  }
   181  
   182  func unmarshalFirstV3Error(rawHTTPStatusErr ccerror.RawHTTPStatusError) (ccerror.V3Error, ccerror.V3ErrorResponse, error) {
   183  	// Try to unmarshal the raw error into a CC error. If unmarshaling fails,
   184  	// return the raw error.
   185  	var errorResponse ccerror.V3ErrorResponse
   186  	err := json.Unmarshal(rawHTTPStatusErr.RawResponse, &errorResponse)
   187  	if err != nil {
   188  		return ccerror.V3Error{}, errorResponse, ccerror.UnknownHTTPSourceError{
   189  			StatusCode:  rawHTTPStatusErr.StatusCode,
   190  			RawResponse: rawHTTPStatusErr.RawResponse,
   191  		}
   192  	}
   193  
   194  	errors := errorResponse.Errors
   195  	if len(errors) == 0 {
   196  		return ccerror.V3Error{}, errorResponse, ccerror.V3UnexpectedResponseError{
   197  			ResponseCode:    rawHTTPStatusErr.StatusCode,
   198  			V3ErrorResponse: errorResponse,
   199  		}
   200  	}
   201  
   202  	return errors[0], errorResponse, nil
   203  }