| // Package azure provides Azure-specific implementations used with AutoRest. |
| // See the included examples for more detail. |
| package azure |
| |
| // Copyright 2017 Microsoft Corporation |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "io/ioutil" |
| "net/http" |
| "regexp" |
| "strconv" |
| "strings" |
| |
| "github.com/Azure/go-autorest/autorest" |
| ) |
| |
| const ( |
| // HeaderClientID is the Azure extension header to set a user-specified request ID. |
| HeaderClientID = "x-ms-client-request-id" |
| |
| // HeaderReturnClientID is the Azure extension header to set if the user-specified request ID |
| // should be included in the response. |
| HeaderReturnClientID = "x-ms-return-client-request-id" |
| |
| // HeaderRequestID is the Azure extension header of the service generated request ID returned |
| // in the response. |
| HeaderRequestID = "x-ms-request-id" |
| ) |
| |
| // ServiceError encapsulates the error response from an Azure service. |
| // It adhears to the OData v4 specification for error responses. |
| type ServiceError struct { |
| Code string `json:"code"` |
| Message string `json:"message"` |
| Target *string `json:"target"` |
| Details []map[string]interface{} `json:"details"` |
| InnerError map[string]interface{} `json:"innererror"` |
| AdditionalInfo []map[string]interface{} `json:"additionalInfo"` |
| } |
| |
| func (se ServiceError) Error() string { |
| result := fmt.Sprintf("Code=%q Message=%q", se.Code, se.Message) |
| |
| if se.Target != nil { |
| result += fmt.Sprintf(" Target=%q", *se.Target) |
| } |
| |
| if se.Details != nil { |
| d, err := json.Marshal(se.Details) |
| if err != nil { |
| result += fmt.Sprintf(" Details=%v", se.Details) |
| } |
| result += fmt.Sprintf(" Details=%v", string(d)) |
| } |
| |
| if se.InnerError != nil { |
| d, err := json.Marshal(se.InnerError) |
| if err != nil { |
| result += fmt.Sprintf(" InnerError=%v", se.InnerError) |
| } |
| result += fmt.Sprintf(" InnerError=%v", string(d)) |
| } |
| |
| if se.AdditionalInfo != nil { |
| d, err := json.Marshal(se.AdditionalInfo) |
| if err != nil { |
| result += fmt.Sprintf(" AdditionalInfo=%v", se.AdditionalInfo) |
| } |
| result += fmt.Sprintf(" AdditionalInfo=%v", string(d)) |
| } |
| |
| return result |
| } |
| |
| // UnmarshalJSON implements the json.Unmarshaler interface for the ServiceError type. |
| func (se *ServiceError) UnmarshalJSON(b []byte) error { |
| // per the OData v4 spec the details field must be an array of JSON objects. |
| // unfortunately not all services adhear to the spec and just return a single |
| // object instead of an array with one object. so we have to perform some |
| // shenanigans to accommodate both cases. |
| // http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793091 |
| |
| type serviceError1 struct { |
| Code string `json:"code"` |
| Message string `json:"message"` |
| Target *string `json:"target"` |
| Details []map[string]interface{} `json:"details"` |
| InnerError map[string]interface{} `json:"innererror"` |
| AdditionalInfo []map[string]interface{} `json:"additionalInfo"` |
| } |
| |
| type serviceError2 struct { |
| Code string `json:"code"` |
| Message string `json:"message"` |
| Target *string `json:"target"` |
| Details map[string]interface{} `json:"details"` |
| InnerError map[string]interface{} `json:"innererror"` |
| AdditionalInfo []map[string]interface{} `json:"additionalInfo"` |
| } |
| |
| se1 := serviceError1{} |
| err := json.Unmarshal(b, &se1) |
| if err == nil { |
| se.populate(se1.Code, se1.Message, se1.Target, se1.Details, se1.InnerError, se1.AdditionalInfo) |
| return nil |
| } |
| |
| se2 := serviceError2{} |
| err = json.Unmarshal(b, &se2) |
| if err == nil { |
| se.populate(se2.Code, se2.Message, se2.Target, nil, se2.InnerError, se2.AdditionalInfo) |
| se.Details = append(se.Details, se2.Details) |
| return nil |
| } |
| return err |
| } |
| |
| func (se *ServiceError) populate(code, message string, target *string, details []map[string]interface{}, inner map[string]interface{}, additional []map[string]interface{}) { |
| se.Code = code |
| se.Message = message |
| se.Target = target |
| se.Details = details |
| se.InnerError = inner |
| se.AdditionalInfo = additional |
| } |
| |
| // RequestError describes an error response returned by Azure service. |
| type RequestError struct { |
| autorest.DetailedError |
| |
| // The error returned by the Azure service. |
| ServiceError *ServiceError `json:"error"` |
| |
| // The request id (from the x-ms-request-id-header) of the request. |
| RequestID string |
| } |
| |
| // Error returns a human-friendly error message from service error. |
| func (e RequestError) Error() string { |
| return fmt.Sprintf("autorest/azure: Service returned an error. Status=%v %v", |
| e.StatusCode, e.ServiceError) |
| } |
| |
| // IsAzureError returns true if the passed error is an Azure Service error; false otherwise. |
| func IsAzureError(e error) bool { |
| _, ok := e.(*RequestError) |
| return ok |
| } |
| |
| // Resource contains details about an Azure resource. |
| type Resource struct { |
| SubscriptionID string |
| ResourceGroup string |
| Provider string |
| ResourceType string |
| ResourceName string |
| } |
| |
| // ParseResourceID parses a resource ID into a ResourceDetails struct. |
| // See https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-template-functions-resource#return-value-4. |
| func ParseResourceID(resourceID string) (Resource, error) { |
| |
| const resourceIDPatternText = `(?i)subscriptions/(.+)/resourceGroups/(.+)/providers/(.+?)/(.+?)/(.+)` |
| resourceIDPattern := regexp.MustCompile(resourceIDPatternText) |
| match := resourceIDPattern.FindStringSubmatch(resourceID) |
| |
| if len(match) == 0 { |
| return Resource{}, fmt.Errorf("parsing failed for %s. Invalid resource Id format", resourceID) |
| } |
| |
| v := strings.Split(match[5], "/") |
| resourceName := v[len(v)-1] |
| |
| result := Resource{ |
| SubscriptionID: match[1], |
| ResourceGroup: match[2], |
| Provider: match[3], |
| ResourceType: match[4], |
| ResourceName: resourceName, |
| } |
| |
| return result, nil |
| } |
| |
| // NewErrorWithError creates a new Error conforming object from the |
| // passed packageType, method, statusCode of the given resp (UndefinedStatusCode |
| // if resp is nil), message, and original error. message is treated as a format |
| // string to which the optional args apply. |
| func NewErrorWithError(original error, packageType string, method string, resp *http.Response, message string, args ...interface{}) RequestError { |
| if v, ok := original.(*RequestError); ok { |
| return *v |
| } |
| |
| statusCode := autorest.UndefinedStatusCode |
| if resp != nil { |
| statusCode = resp.StatusCode |
| } |
| return RequestError{ |
| DetailedError: autorest.DetailedError{ |
| Original: original, |
| PackageType: packageType, |
| Method: method, |
| StatusCode: statusCode, |
| Message: fmt.Sprintf(message, args...), |
| }, |
| } |
| } |
| |
| // WithReturningClientID returns a PrepareDecorator that adds an HTTP extension header of |
| // x-ms-client-request-id whose value is the passed, undecorated UUID (e.g., |
| // "0F39878C-5F76-4DB8-A25D-61D2C193C3CA"). It also sets the x-ms-return-client-request-id |
| // header to true such that UUID accompanies the http.Response. |
| func WithReturningClientID(uuid string) autorest.PrepareDecorator { |
| preparer := autorest.CreatePreparer( |
| WithClientID(uuid), |
| WithReturnClientID(true)) |
| |
| return func(p autorest.Preparer) autorest.Preparer { |
| return autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) { |
| r, err := p.Prepare(r) |
| if err != nil { |
| return r, err |
| } |
| return preparer.Prepare(r) |
| }) |
| } |
| } |
| |
| // WithClientID returns a PrepareDecorator that adds an HTTP extension header of |
| // x-ms-client-request-id whose value is passed, undecorated UUID (e.g., |
| // "0F39878C-5F76-4DB8-A25D-61D2C193C3CA"). |
| func WithClientID(uuid string) autorest.PrepareDecorator { |
| return autorest.WithHeader(HeaderClientID, uuid) |
| } |
| |
| // WithReturnClientID returns a PrepareDecorator that adds an HTTP extension header of |
| // x-ms-return-client-request-id whose boolean value indicates if the value of the |
| // x-ms-client-request-id header should be included in the http.Response. |
| func WithReturnClientID(b bool) autorest.PrepareDecorator { |
| return autorest.WithHeader(HeaderReturnClientID, strconv.FormatBool(b)) |
| } |
| |
| // ExtractClientID extracts the client identifier from the x-ms-client-request-id header set on the |
| // http.Request sent to the service (and returned in the http.Response) |
| func ExtractClientID(resp *http.Response) string { |
| return autorest.ExtractHeaderValue(HeaderClientID, resp) |
| } |
| |
| // ExtractRequestID extracts the Azure server generated request identifier from the |
| // x-ms-request-id header. |
| func ExtractRequestID(resp *http.Response) string { |
| return autorest.ExtractHeaderValue(HeaderRequestID, resp) |
| } |
| |
| // WithErrorUnlessStatusCode returns a RespondDecorator that emits an |
| // azure.RequestError by reading the response body unless the response HTTP status code |
| // is among the set passed. |
| // |
| // If there is a chance service may return responses other than the Azure error |
| // format and the response cannot be parsed into an error, a decoding error will |
| // be returned containing the response body. In any case, the Responder will |
| // return an error if the status code is not satisfied. |
| // |
| // If this Responder returns an error, the response body will be replaced with |
| // an in-memory reader, which needs no further closing. |
| func WithErrorUnlessStatusCode(codes ...int) autorest.RespondDecorator { |
| return func(r autorest.Responder) autorest.Responder { |
| return autorest.ResponderFunc(func(resp *http.Response) error { |
| err := r.Respond(resp) |
| if err == nil && !autorest.ResponseHasStatusCode(resp, codes...) { |
| var e RequestError |
| defer resp.Body.Close() |
| |
| // Copy and replace the Body in case it does not contain an error object. |
| // This will leave the Body available to the caller. |
| b, decodeErr := autorest.CopyAndDecode(autorest.EncodedAsJSON, resp.Body, &e) |
| resp.Body = ioutil.NopCloser(&b) |
| if decodeErr != nil { |
| return fmt.Errorf("autorest/azure: error response cannot be parsed: %q error: %v", b.String(), decodeErr) |
| } |
| if e.ServiceError == nil { |
| // Check if error is unwrapped ServiceError |
| if err := json.Unmarshal(b.Bytes(), &e.ServiceError); err != nil { |
| return err |
| } |
| } |
| if e.ServiceError.Message == "" { |
| // if we're here it means the returned error wasn't OData v4 compliant. |
| // try to unmarshal the body as raw JSON in hopes of getting something. |
| rawBody := map[string]interface{}{} |
| if err := json.Unmarshal(b.Bytes(), &rawBody); err != nil { |
| return err |
| } |
| e.ServiceError = &ServiceError{ |
| Code: "Unknown", |
| Message: "Unknown service error", |
| } |
| if len(rawBody) > 0 { |
| e.ServiceError.Details = []map[string]interface{}{rawBody} |
| } |
| } |
| e.Response = resp |
| e.RequestID = ExtractRequestID(resp) |
| if e.StatusCode == nil { |
| e.StatusCode = resp.StatusCode |
| } |
| err = &e |
| } |
| return err |
| }) |
| } |
| } |