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