1// Package azure provides Azure-specific implementations used with AutoRest. 2// See the included examples for more detail. 3package azure 4 5// Copyright 2017 Microsoft Corporation 6// 7// Licensed under the Apache License, Version 2.0 (the "License"); 8// you may not use this file except in compliance with the License. 9// You may obtain a copy of the License at 10// 11// http://www.apache.org/licenses/LICENSE-2.0 12// 13// Unless required by applicable law or agreed to in writing, software 14// distributed under the License is distributed on an "AS IS" BASIS, 15// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16// See the License for the specific language governing permissions and 17// limitations under the License. 18 19import ( 20 "encoding/json" 21 "fmt" 22 "io/ioutil" 23 "net/http" 24 "regexp" 25 "strconv" 26 "strings" 27 28 "github.com/Azure/go-autorest/autorest" 29) 30 31const ( 32 // HeaderClientID is the Azure extension header to set a user-specified request ID. 33 HeaderClientID = "x-ms-client-request-id" 34 35 // HeaderReturnClientID is the Azure extension header to set if the user-specified request ID 36 // should be included in the response. 37 HeaderReturnClientID = "x-ms-return-client-request-id" 38 39 // HeaderRequestID is the Azure extension header of the service generated request ID returned 40 // in the response. 41 HeaderRequestID = "x-ms-request-id" 42) 43 44// ServiceError encapsulates the error response from an Azure service. 45// It adhears to the OData v4 specification for error responses. 46type ServiceError struct { 47 Code string `json:"code"` 48 Message string `json:"message"` 49 Target *string `json:"target"` 50 Details []map[string]interface{} `json:"details"` 51 InnerError map[string]interface{} `json:"innererror"` 52 AdditionalInfo []map[string]interface{} `json:"additionalInfo"` 53} 54 55func (se ServiceError) Error() string { 56 result := fmt.Sprintf("Code=%q Message=%q", se.Code, se.Message) 57 58 if se.Target != nil { 59 result += fmt.Sprintf(" Target=%q", *se.Target) 60 } 61 62 if se.Details != nil { 63 d, err := json.Marshal(se.Details) 64 if err != nil { 65 result += fmt.Sprintf(" Details=%v", se.Details) 66 } 67 result += fmt.Sprintf(" Details=%v", string(d)) 68 } 69 70 if se.InnerError != nil { 71 d, err := json.Marshal(se.InnerError) 72 if err != nil { 73 result += fmt.Sprintf(" InnerError=%v", se.InnerError) 74 } 75 result += fmt.Sprintf(" InnerError=%v", string(d)) 76 } 77 78 if se.AdditionalInfo != nil { 79 d, err := json.Marshal(se.AdditionalInfo) 80 if err != nil { 81 result += fmt.Sprintf(" AdditionalInfo=%v", se.AdditionalInfo) 82 } 83 result += fmt.Sprintf(" AdditionalInfo=%v", string(d)) 84 } 85 86 return result 87} 88 89// UnmarshalJSON implements the json.Unmarshaler interface for the ServiceError type. 90func (se *ServiceError) UnmarshalJSON(b []byte) error { 91 // per the OData v4 spec the details field must be an array of JSON objects. 92 // unfortunately not all services adhear to the spec and just return a single 93 // object instead of an array with one object. so we have to perform some 94 // shenanigans to accommodate both cases. 95 // http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793091 96 97 type serviceError1 struct { 98 Code string `json:"code"` 99 Message string `json:"message"` 100 Target *string `json:"target"` 101 Details []map[string]interface{} `json:"details"` 102 InnerError map[string]interface{} `json:"innererror"` 103 AdditionalInfo []map[string]interface{} `json:"additionalInfo"` 104 } 105 106 type serviceError2 struct { 107 Code string `json:"code"` 108 Message string `json:"message"` 109 Target *string `json:"target"` 110 Details map[string]interface{} `json:"details"` 111 InnerError map[string]interface{} `json:"innererror"` 112 AdditionalInfo []map[string]interface{} `json:"additionalInfo"` 113 } 114 115 se1 := serviceError1{} 116 err := json.Unmarshal(b, &se1) 117 if err == nil { 118 se.populate(se1.Code, se1.Message, se1.Target, se1.Details, se1.InnerError, se1.AdditionalInfo) 119 return nil 120 } 121 122 se2 := serviceError2{} 123 err = json.Unmarshal(b, &se2) 124 if err == nil { 125 se.populate(se2.Code, se2.Message, se2.Target, nil, se2.InnerError, se2.AdditionalInfo) 126 se.Details = append(se.Details, se2.Details) 127 return nil 128 } 129 return err 130} 131 132func (se *ServiceError) populate(code, message string, target *string, details []map[string]interface{}, inner map[string]interface{}, additional []map[string]interface{}) { 133 se.Code = code 134 se.Message = message 135 se.Target = target 136 se.Details = details 137 se.InnerError = inner 138 se.AdditionalInfo = additional 139} 140 141// RequestError describes an error response returned by Azure service. 142type RequestError struct { 143 autorest.DetailedError 144 145 // The error returned by the Azure service. 146 ServiceError *ServiceError `json:"error"` 147 148 // The request id (from the x-ms-request-id-header) of the request. 149 RequestID string 150} 151 152// Error returns a human-friendly error message from service error. 153func (e RequestError) Error() string { 154 return fmt.Sprintf("autorest/azure: Service returned an error. Status=%v %v", 155 e.StatusCode, e.ServiceError) 156} 157 158// IsAzureError returns true if the passed error is an Azure Service error; false otherwise. 159func IsAzureError(e error) bool { 160 _, ok := e.(*RequestError) 161 return ok 162} 163 164// Resource contains details about an Azure resource. 165type Resource struct { 166 SubscriptionID string 167 ResourceGroup string 168 Provider string 169 ResourceType string 170 ResourceName string 171} 172 173// ParseResourceID parses a resource ID into a ResourceDetails struct. 174// See https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-template-functions-resource#return-value-4. 175func ParseResourceID(resourceID string) (Resource, error) { 176 177 const resourceIDPatternText = `(?i)subscriptions/(.+)/resourceGroups/(.+)/providers/(.+?)/(.+?)/(.+)` 178 resourceIDPattern := regexp.MustCompile(resourceIDPatternText) 179 match := resourceIDPattern.FindStringSubmatch(resourceID) 180 181 if len(match) == 0 { 182 return Resource{}, fmt.Errorf("parsing failed for %s. Invalid resource Id format", resourceID) 183 } 184 185 v := strings.Split(match[5], "/") 186 resourceName := v[len(v)-1] 187 188 result := Resource{ 189 SubscriptionID: match[1], 190 ResourceGroup: match[2], 191 Provider: match[3], 192 ResourceType: match[4], 193 ResourceName: resourceName, 194 } 195 196 return result, nil 197} 198 199// NewErrorWithError creates a new Error conforming object from the 200// passed packageType, method, statusCode of the given resp (UndefinedStatusCode 201// if resp is nil), message, and original error. message is treated as a format 202// string to which the optional args apply. 203func NewErrorWithError(original error, packageType string, method string, resp *http.Response, message string, args ...interface{}) RequestError { 204 if v, ok := original.(*RequestError); ok { 205 return *v 206 } 207 208 statusCode := autorest.UndefinedStatusCode 209 if resp != nil { 210 statusCode = resp.StatusCode 211 } 212 return RequestError{ 213 DetailedError: autorest.DetailedError{ 214 Original: original, 215 PackageType: packageType, 216 Method: method, 217 StatusCode: statusCode, 218 Message: fmt.Sprintf(message, args...), 219 }, 220 } 221} 222 223// WithReturningClientID returns a PrepareDecorator that adds an HTTP extension header of 224// x-ms-client-request-id whose value is the passed, undecorated UUID (e.g., 225// "0F39878C-5F76-4DB8-A25D-61D2C193C3CA"). It also sets the x-ms-return-client-request-id 226// header to true such that UUID accompanies the http.Response. 227func WithReturningClientID(uuid string) autorest.PrepareDecorator { 228 preparer := autorest.CreatePreparer( 229 WithClientID(uuid), 230 WithReturnClientID(true)) 231 232 return func(p autorest.Preparer) autorest.Preparer { 233 return autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) { 234 r, err := p.Prepare(r) 235 if err != nil { 236 return r, err 237 } 238 return preparer.Prepare(r) 239 }) 240 } 241} 242 243// WithClientID returns a PrepareDecorator that adds an HTTP extension header of 244// x-ms-client-request-id whose value is passed, undecorated UUID (e.g., 245// "0F39878C-5F76-4DB8-A25D-61D2C193C3CA"). 246func WithClientID(uuid string) autorest.PrepareDecorator { 247 return autorest.WithHeader(HeaderClientID, uuid) 248} 249 250// WithReturnClientID returns a PrepareDecorator that adds an HTTP extension header of 251// x-ms-return-client-request-id whose boolean value indicates if the value of the 252// x-ms-client-request-id header should be included in the http.Response. 253func WithReturnClientID(b bool) autorest.PrepareDecorator { 254 return autorest.WithHeader(HeaderReturnClientID, strconv.FormatBool(b)) 255} 256 257// ExtractClientID extracts the client identifier from the x-ms-client-request-id header set on the 258// http.Request sent to the service (and returned in the http.Response) 259func ExtractClientID(resp *http.Response) string { 260 return autorest.ExtractHeaderValue(HeaderClientID, resp) 261} 262 263// ExtractRequestID extracts the Azure server generated request identifier from the 264// x-ms-request-id header. 265func ExtractRequestID(resp *http.Response) string { 266 return autorest.ExtractHeaderValue(HeaderRequestID, resp) 267} 268 269// WithErrorUnlessStatusCode returns a RespondDecorator that emits an 270// azure.RequestError by reading the response body unless the response HTTP status code 271// is among the set passed. 272// 273// If there is a chance service may return responses other than the Azure error 274// format and the response cannot be parsed into an error, a decoding error will 275// be returned containing the response body. In any case, the Responder will 276// return an error if the status code is not satisfied. 277// 278// If this Responder returns an error, the response body will be replaced with 279// an in-memory reader, which needs no further closing. 280func WithErrorUnlessStatusCode(codes ...int) autorest.RespondDecorator { 281 return func(r autorest.Responder) autorest.Responder { 282 return autorest.ResponderFunc(func(resp *http.Response) error { 283 err := r.Respond(resp) 284 if err == nil && !autorest.ResponseHasStatusCode(resp, codes...) { 285 var e RequestError 286 defer resp.Body.Close() 287 288 // Copy and replace the Body in case it does not contain an error object. 289 // This will leave the Body available to the caller. 290 b, decodeErr := autorest.CopyAndDecode(autorest.EncodedAsJSON, resp.Body, &e) 291 resp.Body = ioutil.NopCloser(&b) 292 if decodeErr != nil { 293 return fmt.Errorf("autorest/azure: error response cannot be parsed: %q error: %v", b.String(), decodeErr) 294 } 295 if e.ServiceError == nil { 296 // Check if error is unwrapped ServiceError 297 if err := json.Unmarshal(b.Bytes(), &e.ServiceError); err != nil { 298 return err 299 } 300 } 301 if e.ServiceError.Message == "" { 302 // if we're here it means the returned error wasn't OData v4 compliant. 303 // try to unmarshal the body as raw JSON in hopes of getting something. 304 rawBody := map[string]interface{}{} 305 if err := json.Unmarshal(b.Bytes(), &rawBody); err != nil { 306 return err 307 } 308 e.ServiceError = &ServiceError{ 309 Code: "Unknown", 310 Message: "Unknown service error", 311 } 312 if len(rawBody) > 0 { 313 e.ServiceError.Details = []map[string]interface{}{rawBody} 314 } 315 } 316 e.Response = resp 317 e.RequestID = ExtractRequestID(resp) 318 if e.StatusCode == nil { 319 e.StatusCode = resp.StatusCode 320 } 321 err = &e 322 } 323 return err 324 }) 325 } 326} 327