485 lines
13 KiB
Go
485 lines
13 KiB
Go
|
package scw
|
||
|
|
||
|
import (
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"io/ioutil"
|
||
|
"net/http"
|
||
|
"sort"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"github.com/scaleway/scaleway-sdk-go/internal/errors"
|
||
|
"github.com/scaleway/scaleway-sdk-go/validation"
|
||
|
)
|
||
|
|
||
|
// SdkError is a base interface for all Scaleway SDK errors.
|
||
|
type SdkError interface {
|
||
|
Error() string
|
||
|
IsScwSdkError()
|
||
|
}
|
||
|
|
||
|
// ResponseError is an error type for the Scaleway API
|
||
|
type ResponseError struct {
|
||
|
// Message is a human-friendly error message
|
||
|
Message string `json:"message"`
|
||
|
|
||
|
// Type is a string code that defines the kind of error. This field is only used by instance API
|
||
|
Type string `json:"type,omitempty"`
|
||
|
|
||
|
// Resource is a string code that defines the resource concerned by the error. This field is only used by instance API
|
||
|
Resource string `json:"resource,omitempty"`
|
||
|
|
||
|
// Fields contains detail about validation error. This field is only used by instance API
|
||
|
Fields map[string][]string `json:"fields,omitempty"`
|
||
|
|
||
|
// StatusCode is the HTTP status code received
|
||
|
StatusCode int `json:"-"`
|
||
|
|
||
|
// Status is the HTTP status received
|
||
|
Status string `json:"-"`
|
||
|
|
||
|
RawBody json.RawMessage `json:"-"`
|
||
|
}
|
||
|
|
||
|
func (e *ResponseError) UnmarshalJSON(b []byte) error {
|
||
|
type tmpResponseError ResponseError
|
||
|
tmp := tmpResponseError(*e)
|
||
|
|
||
|
err := json.Unmarshal(b, &tmp)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
tmp.Message = strings.ToLower(tmp.Message)
|
||
|
|
||
|
*e = ResponseError(tmp)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// IsScwSdkError implement SdkError interface
|
||
|
func (e *ResponseError) IsScwSdkError() {}
|
||
|
func (e *ResponseError) Error() string {
|
||
|
s := fmt.Sprintf("scaleway-sdk-go: http error %s", e.Status)
|
||
|
|
||
|
if e.Resource != "" {
|
||
|
s = fmt.Sprintf("%s: resource %s", s, e.Resource)
|
||
|
}
|
||
|
|
||
|
if e.Message != "" {
|
||
|
s = fmt.Sprintf("%s: %s", s, e.Message)
|
||
|
}
|
||
|
|
||
|
if len(e.Fields) > 0 {
|
||
|
s = fmt.Sprintf("%s: %v", s, e.Fields)
|
||
|
}
|
||
|
|
||
|
return s
|
||
|
}
|
||
|
func (e *ResponseError) GetRawBody() json.RawMessage {
|
||
|
return e.RawBody
|
||
|
}
|
||
|
|
||
|
// hasResponseError returns an SdkError when the HTTP status is not OK.
|
||
|
func hasResponseError(res *http.Response) error {
|
||
|
if res.StatusCode >= 200 && res.StatusCode <= 299 {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
newErr := &ResponseError{
|
||
|
StatusCode: res.StatusCode,
|
||
|
Status: res.Status,
|
||
|
}
|
||
|
|
||
|
if res.Body == nil {
|
||
|
return newErr
|
||
|
}
|
||
|
|
||
|
body, err := ioutil.ReadAll(res.Body)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, "cannot read error response body")
|
||
|
}
|
||
|
newErr.RawBody = body
|
||
|
|
||
|
// The error content is not encoded in JSON, only returns HTTP data.
|
||
|
if res.Header.Get("Content-Type") != "application/json" {
|
||
|
newErr.Message = res.Status
|
||
|
return newErr
|
||
|
}
|
||
|
|
||
|
err = json.Unmarshal(body, newErr)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, "could not parse error response body")
|
||
|
}
|
||
|
|
||
|
err = unmarshalStandardError(newErr.Type, body)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
err = unmarshalNonStandardError(newErr.Type, body)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return newErr
|
||
|
}
|
||
|
|
||
|
func unmarshalStandardError(errorType string, body []byte) error {
|
||
|
var stdErr SdkError
|
||
|
|
||
|
switch errorType {
|
||
|
case "invalid_arguments":
|
||
|
stdErr = &InvalidArgumentsError{RawBody: body}
|
||
|
case "quotas_exceeded":
|
||
|
stdErr = &QuotasExceededError{RawBody: body}
|
||
|
case "transient_state":
|
||
|
stdErr = &TransientStateError{RawBody: body}
|
||
|
case "not_found":
|
||
|
stdErr = &ResourceNotFoundError{RawBody: body}
|
||
|
case "locked":
|
||
|
stdErr = &ResourceLockedError{RawBody: body}
|
||
|
case "permissions_denied":
|
||
|
stdErr = &PermissionsDeniedError{RawBody: body}
|
||
|
case "out_of_stock":
|
||
|
stdErr = &OutOfStockError{RawBody: body}
|
||
|
case "resource_expired":
|
||
|
stdErr = &ResourceExpiredError{RawBody: body}
|
||
|
default:
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
err := json.Unmarshal(body, stdErr)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, "could not parse error %s response body", errorType)
|
||
|
}
|
||
|
|
||
|
return stdErr
|
||
|
}
|
||
|
|
||
|
func unmarshalNonStandardError(errorType string, body []byte) error {
|
||
|
switch errorType {
|
||
|
// Only in instance API.
|
||
|
|
||
|
case "unknown_resource":
|
||
|
unknownResourceError := &UnknownResource{RawBody: body}
|
||
|
err := json.Unmarshal(body, unknownResourceError)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, "could not parse error %s response body", errorType)
|
||
|
}
|
||
|
return unknownResourceError.ToResourceNotFoundError()
|
||
|
|
||
|
case "invalid_request_error":
|
||
|
invalidRequestError := &InvalidRequestError{RawBody: body}
|
||
|
err := json.Unmarshal(body, invalidRequestError)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, "could not parse error %s response body", errorType)
|
||
|
}
|
||
|
|
||
|
invalidArgumentsError := invalidRequestError.ToInvalidArgumentsError()
|
||
|
if invalidArgumentsError != nil {
|
||
|
return invalidArgumentsError
|
||
|
}
|
||
|
|
||
|
quotasExceededError := invalidRequestError.ToQuotasExceededError()
|
||
|
if quotasExceededError != nil {
|
||
|
return quotasExceededError
|
||
|
}
|
||
|
|
||
|
// At this point, the invalid_request_error is not an InvalidArgumentsError and
|
||
|
// the default marshalling will be used.
|
||
|
return nil
|
||
|
|
||
|
default:
|
||
|
return nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type InvalidArgumentsErrorDetail struct {
|
||
|
ArgumentName string `json:"argument_name"`
|
||
|
Reason string `json:"reason"`
|
||
|
HelpMessage string `json:"help_message"`
|
||
|
}
|
||
|
|
||
|
type InvalidArgumentsError struct {
|
||
|
Details []InvalidArgumentsErrorDetail `json:"details"`
|
||
|
|
||
|
RawBody json.RawMessage `json:"-"`
|
||
|
}
|
||
|
|
||
|
// IsScwSdkError implements the SdkError interface
|
||
|
func (e *InvalidArgumentsError) IsScwSdkError() {}
|
||
|
func (e *InvalidArgumentsError) Error() string {
|
||
|
invalidArgs := make([]string, len(e.Details))
|
||
|
for i, d := range e.Details {
|
||
|
invalidArgs[i] = d.ArgumentName
|
||
|
switch d.Reason {
|
||
|
case "unknown":
|
||
|
invalidArgs[i] += " is invalid for unexpected reason"
|
||
|
case "required":
|
||
|
invalidArgs[i] += " is required"
|
||
|
case "format":
|
||
|
invalidArgs[i] += " is wrongly formatted"
|
||
|
case "constraint":
|
||
|
invalidArgs[i] += " does not respect constraint"
|
||
|
}
|
||
|
if d.HelpMessage != "" {
|
||
|
invalidArgs[i] += ", " + d.HelpMessage
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return "scaleway-sdk-go: invalid argument(s): " + strings.Join(invalidArgs, "; ")
|
||
|
}
|
||
|
func (e *InvalidArgumentsError) GetRawBody() json.RawMessage {
|
||
|
return e.RawBody
|
||
|
}
|
||
|
|
||
|
// UnknownResource is only returned by the instance API.
|
||
|
// Warning: this is not a standard error.
|
||
|
type UnknownResource struct {
|
||
|
Message string `json:"message"`
|
||
|
RawBody json.RawMessage `json:"-"`
|
||
|
}
|
||
|
|
||
|
// ToSdkError returns a standard error InvalidArgumentsError or nil Fields is nil.
|
||
|
func (e *UnknownResource) ToResourceNotFoundError() SdkError {
|
||
|
resourceNotFound := &ResourceNotFoundError{
|
||
|
RawBody: e.RawBody,
|
||
|
}
|
||
|
|
||
|
messageParts := strings.Split(e.Message, `"`)
|
||
|
|
||
|
// Some errors uses ' and not "
|
||
|
if len(messageParts) == 1 {
|
||
|
messageParts = strings.Split(e.Message, "'")
|
||
|
}
|
||
|
|
||
|
switch len(messageParts) {
|
||
|
case 2: // message like: `"111..." not found`
|
||
|
resourceNotFound.ResourceID = messageParts[0]
|
||
|
case 3: // message like: `Security Group "111..." not found`
|
||
|
resourceNotFound.ResourceID = messageParts[1]
|
||
|
// transform `Security group ` to `security_group`
|
||
|
resourceNotFound.Resource = strings.ReplaceAll(strings.ToLower(strings.TrimSpace(messageParts[0])), " ", "_")
|
||
|
default:
|
||
|
return nil
|
||
|
}
|
||
|
if !validation.IsUUID(resourceNotFound.ResourceID) {
|
||
|
return nil
|
||
|
}
|
||
|
return resourceNotFound
|
||
|
}
|
||
|
|
||
|
// InvalidRequestError is only returned by the instance API.
|
||
|
// Warning: this is not a standard error.
|
||
|
type InvalidRequestError struct {
|
||
|
Message string `json:"message"`
|
||
|
|
||
|
Fields map[string][]string `json:"fields"`
|
||
|
|
||
|
Resource string `json:"resource"`
|
||
|
|
||
|
RawBody json.RawMessage `json:"-"`
|
||
|
}
|
||
|
|
||
|
// ToSdkError returns a standard error InvalidArgumentsError or nil Fields is nil.
|
||
|
func (e *InvalidRequestError) ToInvalidArgumentsError() SdkError {
|
||
|
// If error has no fields, it is not an InvalidArgumentsError.
|
||
|
if e.Fields == nil || len(e.Fields) == 0 {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
invalidArguments := &InvalidArgumentsError{
|
||
|
RawBody: e.RawBody,
|
||
|
}
|
||
|
fieldNames := []string(nil)
|
||
|
for fieldName := range e.Fields {
|
||
|
fieldNames = append(fieldNames, fieldName)
|
||
|
}
|
||
|
sort.Strings(fieldNames)
|
||
|
for _, fieldName := range fieldNames {
|
||
|
for _, message := range e.Fields[fieldName] {
|
||
|
invalidArguments.Details = append(invalidArguments.Details, InvalidArgumentsErrorDetail{
|
||
|
ArgumentName: fieldName,
|
||
|
Reason: "constraint",
|
||
|
HelpMessage: message,
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
return invalidArguments
|
||
|
}
|
||
|
|
||
|
func (e *InvalidRequestError) ToQuotasExceededError() SdkError {
|
||
|
if !strings.Contains(strings.ToLower(e.Message), "quota exceeded for this resource") {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
return &QuotasExceededError{
|
||
|
Details: []QuotasExceededErrorDetail{
|
||
|
{
|
||
|
Resource: e.Resource,
|
||
|
Quota: 0,
|
||
|
Current: 0,
|
||
|
},
|
||
|
},
|
||
|
RawBody: e.RawBody,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type QuotasExceededErrorDetail struct {
|
||
|
Resource string `json:"resource"`
|
||
|
Quota uint32 `json:"quota"`
|
||
|
Current uint32 `json:"current"`
|
||
|
}
|
||
|
|
||
|
type QuotasExceededError struct {
|
||
|
Details []QuotasExceededErrorDetail `json:"details"`
|
||
|
RawBody json.RawMessage `json:"-"`
|
||
|
}
|
||
|
|
||
|
// IsScwSdkError implements the SdkError interface
|
||
|
func (e *QuotasExceededError) IsScwSdkError() {}
|
||
|
func (e *QuotasExceededError) Error() string {
|
||
|
invalidArgs := make([]string, len(e.Details))
|
||
|
for i, d := range e.Details {
|
||
|
invalidArgs[i] = fmt.Sprintf("%s has reached its quota (%d/%d)", d.Resource, d.Current, d.Current)
|
||
|
}
|
||
|
|
||
|
return "scaleway-sdk-go: quota exceeded(s): " + strings.Join(invalidArgs, "; ")
|
||
|
}
|
||
|
func (e *QuotasExceededError) GetRawBody() json.RawMessage {
|
||
|
return e.RawBody
|
||
|
}
|
||
|
|
||
|
type PermissionsDeniedError struct {
|
||
|
Details []struct {
|
||
|
Resource string `json:"resource"`
|
||
|
Action string `json:"action"`
|
||
|
} `json:"details"`
|
||
|
|
||
|
RawBody json.RawMessage `json:"-"`
|
||
|
}
|
||
|
|
||
|
// IsScwSdkError implements the SdkError interface
|
||
|
func (e *PermissionsDeniedError) IsScwSdkError() {}
|
||
|
func (e *PermissionsDeniedError) Error() string {
|
||
|
invalidArgs := make([]string, len(e.Details))
|
||
|
for i, d := range e.Details {
|
||
|
invalidArgs[i] = fmt.Sprintf("%s %s", d.Action, d.Resource)
|
||
|
}
|
||
|
|
||
|
return "scaleway-sdk-go: insufficient permissions: " + strings.Join(invalidArgs, "; ")
|
||
|
}
|
||
|
func (e *PermissionsDeniedError) GetRawBody() json.RawMessage {
|
||
|
return e.RawBody
|
||
|
}
|
||
|
|
||
|
type TransientStateError struct {
|
||
|
Resource string `json:"resource"`
|
||
|
ResourceID string `json:"resource_id"`
|
||
|
CurrentState string `json:"current_state"`
|
||
|
|
||
|
RawBody json.RawMessage `json:"-"`
|
||
|
}
|
||
|
|
||
|
// IsScwSdkError implements the SdkError interface
|
||
|
func (e *TransientStateError) IsScwSdkError() {}
|
||
|
func (e *TransientStateError) Error() string {
|
||
|
return fmt.Sprintf("scaleway-sdk-go: resource %s with ID %s is in a transient state: %s", e.Resource, e.ResourceID, e.CurrentState)
|
||
|
}
|
||
|
func (e *TransientStateError) GetRawBody() json.RawMessage {
|
||
|
return e.RawBody
|
||
|
}
|
||
|
|
||
|
type ResourceNotFoundError struct {
|
||
|
Resource string `json:"resource"`
|
||
|
ResourceID string `json:"resource_id"`
|
||
|
|
||
|
RawBody json.RawMessage `json:"-"`
|
||
|
}
|
||
|
|
||
|
// IsScwSdkError implements the SdkError interface
|
||
|
func (e *ResourceNotFoundError) IsScwSdkError() {}
|
||
|
func (e *ResourceNotFoundError) Error() string {
|
||
|
return fmt.Sprintf("scaleway-sdk-go: resource %s with ID %s is not found", e.Resource, e.ResourceID)
|
||
|
}
|
||
|
func (e *ResourceNotFoundError) GetRawBody() json.RawMessage {
|
||
|
return e.RawBody
|
||
|
}
|
||
|
|
||
|
type ResourceLockedError struct {
|
||
|
Resource string `json:"resource"`
|
||
|
ResourceID string `json:"resource_id"`
|
||
|
|
||
|
RawBody json.RawMessage `json:"-"`
|
||
|
}
|
||
|
|
||
|
// IsScwSdkError implements the SdkError interface
|
||
|
func (e *ResourceLockedError) IsScwSdkError() {}
|
||
|
func (e *ResourceLockedError) Error() string {
|
||
|
return fmt.Sprintf("scaleway-sdk-go: resource %s with ID %s is locked", e.Resource, e.ResourceID)
|
||
|
}
|
||
|
func (e *ResourceLockedError) GetRawBody() json.RawMessage {
|
||
|
return e.RawBody
|
||
|
}
|
||
|
|
||
|
type OutOfStockError struct {
|
||
|
Resource string `json:"resource"`
|
||
|
|
||
|
RawBody json.RawMessage `json:"-"`
|
||
|
}
|
||
|
|
||
|
// IsScwSdkError implements the SdkError interface
|
||
|
func (e *OutOfStockError) IsScwSdkError() {}
|
||
|
func (e *OutOfStockError) Error() string {
|
||
|
return fmt.Sprintf("scaleway-sdk-go: resource %s is out of stock", e.Resource)
|
||
|
}
|
||
|
func (e *OutOfStockError) GetRawBody() json.RawMessage {
|
||
|
return e.RawBody
|
||
|
}
|
||
|
|
||
|
// InvalidClientOptionError indicates that at least one of client data has been badly provided for the client creation.
|
||
|
type InvalidClientOptionError struct {
|
||
|
errorType string
|
||
|
}
|
||
|
|
||
|
func NewInvalidClientOptionError(format string, a ...interface{}) *InvalidClientOptionError {
|
||
|
return &InvalidClientOptionError{errorType: fmt.Sprintf(format, a...)}
|
||
|
}
|
||
|
|
||
|
// IsScwSdkError implements the SdkError interface
|
||
|
func (e InvalidClientOptionError) IsScwSdkError() {}
|
||
|
func (e InvalidClientOptionError) Error() string {
|
||
|
return fmt.Sprintf("scaleway-sdk-go: %s", e.errorType)
|
||
|
}
|
||
|
|
||
|
// ConfigFileNotFound indicates that the config file could not be found
|
||
|
type ConfigFileNotFoundError struct {
|
||
|
path string
|
||
|
}
|
||
|
|
||
|
func configFileNotFound(path string) *ConfigFileNotFoundError {
|
||
|
return &ConfigFileNotFoundError{path: path}
|
||
|
}
|
||
|
|
||
|
// ConfigFileNotFoundError implements the SdkError interface
|
||
|
func (e ConfigFileNotFoundError) IsScwSdkError() {}
|
||
|
func (e ConfigFileNotFoundError) Error() string {
|
||
|
return fmt.Sprintf("scaleway-sdk-go: cannot read config file %s: no such file or directory", e.path)
|
||
|
}
|
||
|
|
||
|
// ResourceExpiredError implements the SdkError interface
|
||
|
type ResourceExpiredError struct {
|
||
|
Resource string `json:"resource"`
|
||
|
ResourceID string `json:"resource_id"`
|
||
|
ExpiredSince time.Time `json:"expired_since"`
|
||
|
|
||
|
RawBody json.RawMessage `json:"-"`
|
||
|
}
|
||
|
|
||
|
func (r ResourceExpiredError) Error() string {
|
||
|
return fmt.Sprintf("scaleway-sdk-go: resource %s with ID %s expired since %s", r.Resource, r.ResourceID, r.ExpiredSince.String())
|
||
|
}
|
||
|
|
||
|
func (r ResourceExpiredError) IsScwSdkError() {}
|