azure.go raw

   1  // Package azure provides Azure-specific implementations used with AutoRest.
   2  // See the included examples for more detail.
   3  package 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  
  19  import (
  20  	"bytes"
  21  	"encoding/json"
  22  	"fmt"
  23  	"io"
  24  	"net/http"
  25  	"regexp"
  26  	"strconv"
  27  	"strings"
  28  
  29  	"github.com/Azure/go-autorest/autorest"
  30  )
  31  
  32  const (
  33  	// HeaderClientID is the Azure extension header to set a user-specified request ID.
  34  	HeaderClientID = "x-ms-client-request-id"
  35  
  36  	// HeaderReturnClientID is the Azure extension header to set if the user-specified request ID
  37  	// should be included in the response.
  38  	HeaderReturnClientID = "x-ms-return-client-request-id"
  39  
  40  	// HeaderContentType is the type of the content in the HTTP response.
  41  	HeaderContentType = "Content-Type"
  42  
  43  	// HeaderRequestID is the Azure extension header of the service generated request ID returned
  44  	// in the response.
  45  	HeaderRequestID = "x-ms-request-id"
  46  )
  47  
  48  // ServiceError encapsulates the error response from an Azure service.
  49  // It adhears to the OData v4 specification for error responses.
  50  type ServiceError struct {
  51  	Code           string                   `json:"code"`
  52  	Message        string                   `json:"message"`
  53  	Target         *string                  `json:"target"`
  54  	Details        []map[string]interface{} `json:"details"`
  55  	InnerError     map[string]interface{}   `json:"innererror"`
  56  	AdditionalInfo []map[string]interface{} `json:"additionalInfo"`
  57  }
  58  
  59  func (se ServiceError) Error() string {
  60  	result := fmt.Sprintf("Code=%q Message=%q", se.Code, se.Message)
  61  
  62  	if se.Target != nil {
  63  		result += fmt.Sprintf(" Target=%q", *se.Target)
  64  	}
  65  
  66  	if se.Details != nil {
  67  		d, err := json.Marshal(se.Details)
  68  		if err != nil {
  69  			result += fmt.Sprintf(" Details=%v", se.Details)
  70  		}
  71  		result += fmt.Sprintf(" Details=%s", d)
  72  	}
  73  
  74  	if se.InnerError != nil {
  75  		d, err := json.Marshal(se.InnerError)
  76  		if err != nil {
  77  			result += fmt.Sprintf(" InnerError=%v", se.InnerError)
  78  		}
  79  		result += fmt.Sprintf(" InnerError=%s", d)
  80  	}
  81  
  82  	if se.AdditionalInfo != nil {
  83  		d, err := json.Marshal(se.AdditionalInfo)
  84  		if err != nil {
  85  			result += fmt.Sprintf(" AdditionalInfo=%v", se.AdditionalInfo)
  86  		}
  87  		result += fmt.Sprintf(" AdditionalInfo=%s", d)
  88  	}
  89  
  90  	return result
  91  }
  92  
  93  // UnmarshalJSON implements the json.Unmarshaler interface for the ServiceError type.
  94  func (se *ServiceError) UnmarshalJSON(b []byte) error {
  95  	// http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793091
  96  
  97  	type serviceErrorInternal struct {
  98  		Code           string                   `json:"code"`
  99  		Message        string                   `json:"message"`
 100  		Target         *string                  `json:"target,omitempty"`
 101  		AdditionalInfo []map[string]interface{} `json:"additionalInfo,omitempty"`
 102  		// not all services conform to the OData v4 spec.
 103  		// the following fields are where we've seen discrepancies
 104  
 105  		// spec calls for []map[string]interface{} but have seen map[string]interface{}
 106  		Details interface{} `json:"details,omitempty"`
 107  
 108  		// spec calls for map[string]interface{} but have seen []map[string]interface{} and string
 109  		InnerError interface{} `json:"innererror,omitempty"`
 110  	}
 111  
 112  	sei := serviceErrorInternal{}
 113  	if err := json.Unmarshal(b, &sei); err != nil {
 114  		return err
 115  	}
 116  
 117  	// copy the fields we know to be correct
 118  	se.AdditionalInfo = sei.AdditionalInfo
 119  	se.Code = sei.Code
 120  	se.Message = sei.Message
 121  	se.Target = sei.Target
 122  
 123  	// converts an []interface{} to []map[string]interface{}
 124  	arrayOfObjs := func(v interface{}) ([]map[string]interface{}, bool) {
 125  		arrayOf, ok := v.([]interface{})
 126  		if !ok {
 127  			return nil, false
 128  		}
 129  		final := []map[string]interface{}{}
 130  		for _, item := range arrayOf {
 131  			as, ok := item.(map[string]interface{})
 132  			if !ok {
 133  				return nil, false
 134  			}
 135  			final = append(final, as)
 136  		}
 137  		return final, true
 138  	}
 139  
 140  	// convert the remaining fields, falling back to raw JSON if necessary
 141  
 142  	if c, ok := arrayOfObjs(sei.Details); ok {
 143  		se.Details = c
 144  	} else if c, ok := sei.Details.(map[string]interface{}); ok {
 145  		se.Details = []map[string]interface{}{c}
 146  	} else if sei.Details != nil {
 147  		// stuff into Details
 148  		se.Details = []map[string]interface{}{
 149  			{"raw": sei.Details},
 150  		}
 151  	}
 152  
 153  	if c, ok := sei.InnerError.(map[string]interface{}); ok {
 154  		se.InnerError = c
 155  	} else if c, ok := arrayOfObjs(sei.InnerError); ok {
 156  		// if there's only one error extract it
 157  		if len(c) == 1 {
 158  			se.InnerError = c[0]
 159  		} else {
 160  			// multiple errors, stuff them into the value
 161  			se.InnerError = map[string]interface{}{
 162  				"multi": c,
 163  			}
 164  		}
 165  	} else if c, ok := sei.InnerError.(string); ok {
 166  		se.InnerError = map[string]interface{}{"error": c}
 167  	} else if sei.InnerError != nil {
 168  		// stuff into InnerError
 169  		se.InnerError = map[string]interface{}{
 170  			"raw": sei.InnerError,
 171  		}
 172  	}
 173  	return nil
 174  }
 175  
 176  // RequestError describes an error response returned by Azure service.
 177  type RequestError struct {
 178  	autorest.DetailedError
 179  
 180  	// The error returned by the Azure service.
 181  	ServiceError *ServiceError `json:"error" xml:"Error"`
 182  
 183  	// The request id (from the x-ms-request-id-header) of the request.
 184  	RequestID string
 185  }
 186  
 187  // Error returns a human-friendly error message from service error.
 188  func (e RequestError) Error() string {
 189  	return fmt.Sprintf("autorest/azure: Service returned an error. Status=%v %v",
 190  		e.StatusCode, e.ServiceError)
 191  }
 192  
 193  // IsAzureError returns true if the passed error is an Azure Service error; false otherwise.
 194  func IsAzureError(e error) bool {
 195  	_, ok := e.(*RequestError)
 196  	return ok
 197  }
 198  
 199  // Resource contains details about an Azure resource.
 200  type Resource struct {
 201  	SubscriptionID string
 202  	ResourceGroup  string
 203  	Provider       string
 204  	ResourceType   string
 205  	ResourceName   string
 206  }
 207  
 208  // String function returns a string in form of azureResourceID
 209  func (r Resource) String() string {
 210  	return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/%s/%s/%s", r.SubscriptionID, r.ResourceGroup, r.Provider, r.ResourceType, r.ResourceName)
 211  }
 212  
 213  // ParseResourceID parses a resource ID into a ResourceDetails struct.
 214  // See https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/template-functions-resource?tabs=json#resourceid.
 215  func ParseResourceID(resourceID string) (Resource, error) {
 216  
 217  	const resourceIDPatternText = `(?i)^/subscriptions/(.+)/resourceGroups/(.+)/providers/(.+?)/(.+?)/(.+)$`
 218  	resourceIDPattern := regexp.MustCompile(resourceIDPatternText)
 219  	match := resourceIDPattern.FindStringSubmatch(resourceID)
 220  
 221  	if len(match) == 0 {
 222  		return Resource{}, fmt.Errorf("parsing failed for %s. Invalid resource Id format", resourceID)
 223  	}
 224  
 225  	v := strings.Split(match[5], "/")
 226  	resourceName := v[len(v)-1]
 227  
 228  	result := Resource{
 229  		SubscriptionID: match[1],
 230  		ResourceGroup:  match[2],
 231  		Provider:       match[3],
 232  		ResourceType:   match[4],
 233  		ResourceName:   resourceName,
 234  	}
 235  
 236  	return result, nil
 237  }
 238  
 239  // NewErrorWithError creates a new Error conforming object from the
 240  // passed packageType, method, statusCode of the given resp (UndefinedStatusCode
 241  // if resp is nil), message, and original error. message is treated as a format
 242  // string to which the optional args apply.
 243  func NewErrorWithError(original error, packageType string, method string, resp *http.Response, message string, args ...interface{}) RequestError {
 244  	if v, ok := original.(*RequestError); ok {
 245  		return *v
 246  	}
 247  
 248  	statusCode := autorest.UndefinedStatusCode
 249  	if resp != nil {
 250  		statusCode = resp.StatusCode
 251  	}
 252  	return RequestError{
 253  		DetailedError: autorest.DetailedError{
 254  			Original:    original,
 255  			PackageType: packageType,
 256  			Method:      method,
 257  			StatusCode:  statusCode,
 258  			Message:     fmt.Sprintf(message, args...),
 259  		},
 260  	}
 261  }
 262  
 263  // WithReturningClientID returns a PrepareDecorator that adds an HTTP extension header of
 264  // x-ms-client-request-id whose value is the passed, undecorated UUID (e.g.,
 265  // "0F39878C-5F76-4DB8-A25D-61D2C193C3CA"). It also sets the x-ms-return-client-request-id
 266  // header to true such that UUID accompanies the http.Response.
 267  func WithReturningClientID(uuid string) autorest.PrepareDecorator {
 268  	preparer := autorest.CreatePreparer(
 269  		WithClientID(uuid),
 270  		WithReturnClientID(true))
 271  
 272  	return func(p autorest.Preparer) autorest.Preparer {
 273  		return autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) {
 274  			r, err := p.Prepare(r)
 275  			if err != nil {
 276  				return r, err
 277  			}
 278  			return preparer.Prepare(r)
 279  		})
 280  	}
 281  }
 282  
 283  // WithClientID returns a PrepareDecorator that adds an HTTP extension header of
 284  // x-ms-client-request-id whose value is passed, undecorated UUID (e.g.,
 285  // "0F39878C-5F76-4DB8-A25D-61D2C193C3CA").
 286  func WithClientID(uuid string) autorest.PrepareDecorator {
 287  	return autorest.WithHeader(HeaderClientID, uuid)
 288  }
 289  
 290  // WithReturnClientID returns a PrepareDecorator that adds an HTTP extension header of
 291  // x-ms-return-client-request-id whose boolean value indicates if the value of the
 292  // x-ms-client-request-id header should be included in the http.Response.
 293  func WithReturnClientID(b bool) autorest.PrepareDecorator {
 294  	return autorest.WithHeader(HeaderReturnClientID, strconv.FormatBool(b))
 295  }
 296  
 297  // ExtractClientID extracts the client identifier from the x-ms-client-request-id header set on the
 298  // http.Request sent to the service (and returned in the http.Response)
 299  func ExtractClientID(resp *http.Response) string {
 300  	return autorest.ExtractHeaderValue(HeaderClientID, resp)
 301  }
 302  
 303  // ExtractRequestID extracts the Azure server generated request identifier from the
 304  // x-ms-request-id header.
 305  func ExtractRequestID(resp *http.Response) string {
 306  	return autorest.ExtractHeaderValue(HeaderRequestID, resp)
 307  }
 308  
 309  // WithErrorUnlessStatusCode returns a RespondDecorator that emits an
 310  // azure.RequestError by reading the response body unless the response HTTP status code
 311  // is among the set passed.
 312  //
 313  // If there is a chance service may return responses other than the Azure error
 314  // format and the response cannot be parsed into an error, a decoding error will
 315  // be returned containing the response body. In any case, the Responder will
 316  // return an error if the status code is not satisfied.
 317  //
 318  // If this Responder returns an error, the response body will be replaced with
 319  // an in-memory reader, which needs no further closing.
 320  func WithErrorUnlessStatusCode(codes ...int) autorest.RespondDecorator {
 321  	return func(r autorest.Responder) autorest.Responder {
 322  		return autorest.ResponderFunc(func(resp *http.Response) error {
 323  			err := r.Respond(resp)
 324  			if err == nil && !autorest.ResponseHasStatusCode(resp, codes...) {
 325  				var e RequestError
 326  				defer resp.Body.Close()
 327  
 328  				encodedAs := autorest.EncodedAsJSON
 329  				if strings.Contains(resp.Header.Get("Content-Type"), "xml") {
 330  					encodedAs = autorest.EncodedAsXML
 331  				}
 332  
 333  				// Copy and replace the Body in case it does not contain an error object.
 334  				// This will leave the Body available to the caller.
 335  				b, decodeErr := autorest.CopyAndDecode(encodedAs, resp.Body, &e)
 336  				resp.Body = io.NopCloser(&b)
 337  				if decodeErr != nil {
 338  					return fmt.Errorf("autorest/azure: error response cannot be parsed: %q error: %v", b, decodeErr)
 339  				}
 340  				if e.ServiceError == nil {
 341  					// Check if error is unwrapped ServiceError
 342  					decoder := autorest.NewDecoder(encodedAs, bytes.NewReader(b.Bytes()))
 343  					if err := decoder.Decode(&e.ServiceError); err != nil {
 344  						return fmt.Errorf("autorest/azure: error response cannot be parsed: %q error: %v", b, err)
 345  					}
 346  
 347  					// for example, should the API return the literal value `null` as the response
 348  					if e.ServiceError == nil {
 349  						e.ServiceError = &ServiceError{
 350  							Code:    "Unknown",
 351  							Message: "Unknown service error",
 352  							Details: []map[string]interface{}{
 353  								{
 354  									"HttpResponse.Body": b.String(),
 355  								},
 356  							},
 357  						}
 358  					}
 359  				}
 360  
 361  				if e.ServiceError != nil && e.ServiceError.Message == "" {
 362  					// if we're here it means the returned error wasn't OData v4 compliant.
 363  					// try to unmarshal the body in hopes of getting something.
 364  					rawBody := map[string]interface{}{}
 365  					decoder := autorest.NewDecoder(encodedAs, bytes.NewReader(b.Bytes()))
 366  					if err := decoder.Decode(&rawBody); err != nil {
 367  						return fmt.Errorf("autorest/azure: error response cannot be parsed: %q error: %v", b, err)
 368  					}
 369  
 370  					e.ServiceError = &ServiceError{
 371  						Code:    "Unknown",
 372  						Message: "Unknown service error",
 373  					}
 374  					if len(rawBody) > 0 {
 375  						e.ServiceError.Details = []map[string]interface{}{rawBody}
 376  					}
 377  				}
 378  				e.Response = resp
 379  				e.RequestID = ExtractRequestID(resp)
 380  				if e.StatusCode == nil {
 381  					e.StatusCode = resp.StatusCode
 382  				}
 383  				err = &e
 384  			}
 385  			return err
 386  		})
 387  	}
 388  }
 389