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