github.com/swisscom/cloudfoundry-cli@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 }