1 package azure
2 3 // Copyright 2017 Microsoft Corporation
4 //
5 // Licensed under the Apache License, Version 2.0 (the "License");
6 // you may not use this file except in compliance with the License.
7 // You may obtain a copy of the License at
8 //
9 // http://www.apache.org/licenses/LICENSE-2.0
10 //
11 // Unless required by applicable law or agreed to in writing, software
12 // distributed under the License is distributed on an "AS IS" BASIS,
13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 // See the License for the specific language governing permissions and
15 // limitations under the License.
16 17 import (
18 "bytes"
19 "context"
20 "encoding/json"
21 "fmt"
22 "io"
23 "net/http"
24 "net/url"
25 "strings"
26 "time"
27 28 "github.com/Azure/go-autorest/autorest"
29 "github.com/Azure/go-autorest/logger"
30 "github.com/Azure/go-autorest/tracing"
31 )
32 33 const (
34 headerAsyncOperation = "Azure-AsyncOperation"
35 )
36 37 const (
38 operationInProgress string = "InProgress"
39 operationCanceled string = "Canceled"
40 operationFailed string = "Failed"
41 operationSucceeded string = "Succeeded"
42 )
43 44 var pollingCodes = [...]int{http.StatusNoContent, http.StatusAccepted, http.StatusCreated, http.StatusOK}
45 46 // FutureAPI contains the set of methods on the Future type.
47 type FutureAPI interface {
48 // Response returns the last HTTP response.
49 Response() *http.Response
50 51 // Status returns the last status message of the operation.
52 Status() string
53 54 // PollingMethod returns the method used to monitor the status of the asynchronous operation.
55 PollingMethod() PollingMethodType
56 57 // DoneWithContext queries the service to see if the operation has completed.
58 DoneWithContext(context.Context, autorest.Sender) (bool, error)
59 60 // GetPollingDelay returns a duration the application should wait before checking
61 // the status of the asynchronous request and true; this value is returned from
62 // the service via the Retry-After response header. If the header wasn't returned
63 // then the function returns the zero-value time.Duration and false.
64 GetPollingDelay() (time.Duration, bool)
65 66 // WaitForCompletionRef will return when one of the following conditions is met: the long
67 // running operation has completed, the provided context is cancelled, or the client's
68 // polling duration has been exceeded. It will retry failed polling attempts based on
69 // the retry value defined in the client up to the maximum retry attempts.
70 // If no deadline is specified in the context then the client.PollingDuration will be
71 // used to determine if a default deadline should be used.
72 // If PollingDuration is greater than zero the value will be used as the context's timeout.
73 // If PollingDuration is zero then no default deadline will be used.
74 WaitForCompletionRef(context.Context, autorest.Client) error
75 76 // MarshalJSON implements the json.Marshaler interface.
77 MarshalJSON() ([]byte, error)
78 79 // MarshalJSON implements the json.Unmarshaler interface.
80 UnmarshalJSON([]byte) error
81 82 // PollingURL returns the URL used for retrieving the status of the long-running operation.
83 PollingURL() string
84 85 // GetResult should be called once polling has completed successfully.
86 // It makes the final GET call to retrieve the resultant payload.
87 GetResult(autorest.Sender) (*http.Response, error)
88 }
89 90 var _ FutureAPI = (*Future)(nil)
91 92 // Future provides a mechanism to access the status and results of an asynchronous request.
93 // Since futures are stateful they should be passed by value to avoid race conditions.
94 type Future struct {
95 pt pollingTracker
96 }
97 98 // NewFutureFromResponse returns a new Future object initialized
99 // with the initial response from an asynchronous operation.
100 func NewFutureFromResponse(resp *http.Response) (Future, error) {
101 pt, err := createPollingTracker(resp)
102 return Future{pt: pt}, err
103 }
104 105 // Response returns the last HTTP response.
106 func (f Future) Response() *http.Response {
107 if f.pt == nil {
108 return nil
109 }
110 return f.pt.latestResponse()
111 }
112 113 // Status returns the last status message of the operation.
114 func (f Future) Status() string {
115 if f.pt == nil {
116 return ""
117 }
118 return f.pt.pollingStatus()
119 }
120 121 // PollingMethod returns the method used to monitor the status of the asynchronous operation.
122 func (f Future) PollingMethod() PollingMethodType {
123 if f.pt == nil {
124 return PollingUnknown
125 }
126 return f.pt.pollingMethod()
127 }
128 129 // DoneWithContext queries the service to see if the operation has completed.
130 func (f *Future) DoneWithContext(ctx context.Context, sender autorest.Sender) (done bool, err error) {
131 ctx = tracing.StartSpan(ctx, "github.com/Azure/go-autorest/autorest/azure/async.DoneWithContext")
132 defer func() {
133 sc := -1
134 resp := f.Response()
135 if resp != nil {
136 sc = resp.StatusCode
137 }
138 tracing.EndSpan(ctx, sc, err)
139 }()
140 141 if f.pt == nil {
142 return false, autorest.NewError("Future", "Done", "future is not initialized")
143 }
144 if f.pt.hasTerminated() {
145 return true, f.pt.pollingError()
146 }
147 if err := f.pt.pollForStatus(ctx, sender); err != nil {
148 return false, err
149 }
150 if err := f.pt.checkForErrors(); err != nil {
151 return f.pt.hasTerminated(), err
152 }
153 if err := f.pt.updatePollingState(f.pt.provisioningStateApplicable()); err != nil {
154 return false, err
155 }
156 if err := f.pt.initPollingMethod(); err != nil {
157 return false, err
158 }
159 if err := f.pt.updatePollingMethod(); err != nil {
160 return false, err
161 }
162 return f.pt.hasTerminated(), f.pt.pollingError()
163 }
164 165 // GetPollingDelay returns a duration the application should wait before checking
166 // the status of the asynchronous request and true; this value is returned from
167 // the service via the Retry-After response header. If the header wasn't returned
168 // then the function returns the zero-value time.Duration and false.
169 func (f Future) GetPollingDelay() (time.Duration, bool) {
170 if f.pt == nil {
171 return 0, false
172 }
173 resp := f.pt.latestResponse()
174 if resp == nil {
175 return 0, false
176 }
177 178 retry := resp.Header.Get(autorest.HeaderRetryAfter)
179 if retry == "" {
180 return 0, false
181 }
182 183 d, err := time.ParseDuration(retry + "s")
184 if err != nil {
185 panic(err)
186 }
187 188 return d, true
189 }
190 191 // WaitForCompletionRef will return when one of the following conditions is met: the long
192 // running operation has completed, the provided context is cancelled, or the client's
193 // polling duration has been exceeded. It will retry failed polling attempts based on
194 // the retry value defined in the client up to the maximum retry attempts.
195 // If no deadline is specified in the context then the client.PollingDuration will be
196 // used to determine if a default deadline should be used.
197 // If PollingDuration is greater than zero the value will be used as the context's timeout.
198 // If PollingDuration is zero then no default deadline will be used.
199 func (f *Future) WaitForCompletionRef(ctx context.Context, client autorest.Client) (err error) {
200 ctx = tracing.StartSpan(ctx, "github.com/Azure/go-autorest/autorest/azure/async.WaitForCompletionRef")
201 defer func() {
202 sc := -1
203 resp := f.Response()
204 if resp != nil {
205 sc = resp.StatusCode
206 }
207 tracing.EndSpan(ctx, sc, err)
208 }()
209 cancelCtx := ctx
210 // if the provided context already has a deadline don't override it
211 _, hasDeadline := ctx.Deadline()
212 if d := client.PollingDuration; !hasDeadline && d != 0 {
213 var cancel context.CancelFunc
214 cancelCtx, cancel = context.WithTimeout(ctx, d)
215 defer cancel()
216 }
217 // if the initial response has a Retry-After, sleep for the specified amount of time before starting to poll
218 if delay, ok := f.GetPollingDelay(); ok {
219 logger.Instance.Writeln(logger.LogInfo, "WaitForCompletionRef: initial polling delay")
220 if delayElapsed := autorest.DelayForBackoff(delay, 0, cancelCtx.Done()); !delayElapsed {
221 err = cancelCtx.Err()
222 return
223 }
224 }
225 done, err := f.DoneWithContext(ctx, client)
226 for attempts := 0; !done; done, err = f.DoneWithContext(ctx, client) {
227 if attempts >= client.RetryAttempts {
228 return autorest.NewErrorWithError(err, "Future", "WaitForCompletion", f.pt.latestResponse(), "the number of retries has been exceeded")
229 }
230 // we want delayAttempt to be zero in the non-error case so
231 // that DelayForBackoff doesn't perform exponential back-off
232 var delayAttempt int
233 var delay time.Duration
234 if err == nil {
235 // check for Retry-After delay, if not present use the client's polling delay
236 var ok bool
237 delay, ok = f.GetPollingDelay()
238 if !ok {
239 logger.Instance.Writeln(logger.LogInfo, "WaitForCompletionRef: Using client polling delay")
240 delay = client.PollingDelay
241 }
242 } else {
243 // there was an error polling for status so perform exponential
244 // back-off based on the number of attempts using the client's retry
245 // duration. update attempts after delayAttempt to avoid off-by-one.
246 logger.Instance.Writef(logger.LogError, "WaitForCompletionRef: %s\n", err)
247 delayAttempt = attempts
248 delay = client.RetryDuration
249 attempts++
250 }
251 // wait until the delay elapses or the context is cancelled
252 delayElapsed := autorest.DelayForBackoff(delay, delayAttempt, cancelCtx.Done())
253 if !delayElapsed {
254 return autorest.NewErrorWithError(cancelCtx.Err(), "Future", "WaitForCompletion", f.pt.latestResponse(), "context has been cancelled")
255 }
256 }
257 return
258 }
259 260 // MarshalJSON implements the json.Marshaler interface.
261 func (f Future) MarshalJSON() ([]byte, error) {
262 return json.Marshal(f.pt)
263 }
264 265 // UnmarshalJSON implements the json.Unmarshaler interface.
266 func (f *Future) UnmarshalJSON(data []byte) error {
267 // unmarshal into JSON object to determine the tracker type
268 obj := map[string]interface{}{}
269 err := json.Unmarshal(data, &obj)
270 if err != nil {
271 return err
272 }
273 if obj["method"] == nil {
274 return autorest.NewError("Future", "UnmarshalJSON", "missing 'method' property")
275 }
276 method := obj["method"].(string)
277 switch strings.ToUpper(method) {
278 case http.MethodDelete:
279 f.pt = &pollingTrackerDelete{}
280 case http.MethodPatch:
281 f.pt = &pollingTrackerPatch{}
282 case http.MethodPost:
283 f.pt = &pollingTrackerPost{}
284 case http.MethodPut:
285 f.pt = &pollingTrackerPut{}
286 default:
287 return autorest.NewError("Future", "UnmarshalJSON", "unsupoorted method '%s'", method)
288 }
289 // now unmarshal into the tracker
290 return json.Unmarshal(data, &f.pt)
291 }
292 293 // PollingURL returns the URL used for retrieving the status of the long-running operation.
294 func (f Future) PollingURL() string {
295 if f.pt == nil {
296 return ""
297 }
298 return f.pt.pollingURL()
299 }
300 301 // GetResult should be called once polling has completed successfully.
302 // It makes the final GET call to retrieve the resultant payload.
303 func (f Future) GetResult(sender autorest.Sender) (*http.Response, error) {
304 if f.pt.finalGetURL() == "" {
305 // we can end up in this situation if the async operation returns a 200
306 // with no polling URLs. in that case return the response which should
307 // contain the JSON payload (only do this for successful terminal cases).
308 if lr := f.pt.latestResponse(); lr != nil && f.pt.hasSucceeded() {
309 return lr, nil
310 }
311 return nil, autorest.NewError("Future", "GetResult", "missing URL for retrieving result")
312 }
313 req, err := http.NewRequest(http.MethodGet, f.pt.finalGetURL(), nil)
314 if err != nil {
315 return nil, err
316 }
317 resp, err := sender.Do(req)
318 if err == nil && resp.Body != nil {
319 // copy the body and close it so callers don't have to
320 defer resp.Body.Close()
321 b, err := io.ReadAll(resp.Body)
322 if err != nil {
323 return resp, err
324 }
325 resp.Body = io.NopCloser(bytes.NewReader(b))
326 }
327 return resp, err
328 }
329 330 type pollingTracker interface {
331 // these methods can differ per tracker
332 333 // checks the response headers and status code to determine the polling mechanism
334 updatePollingMethod() error
335 336 // checks the response for tracker-specific error conditions
337 checkForErrors() error
338 339 // returns true if provisioning state should be checked
340 provisioningStateApplicable() bool
341 342 // methods common to all trackers
343 344 // initializes a tracker's polling URL and method, called for each iteration.
345 // these values can be overridden by each polling tracker as required.
346 initPollingMethod() error
347 348 // initializes the tracker's internal state, call this when the tracker is created
349 initializeState() error
350 351 // makes an HTTP request to check the status of the LRO
352 pollForStatus(ctx context.Context, sender autorest.Sender) error
353 354 // updates internal tracker state, call this after each call to pollForStatus
355 updatePollingState(provStateApl bool) error
356 357 // returns the error response from the service, can be nil
358 pollingError() error
359 360 // returns the polling method being used
361 pollingMethod() PollingMethodType
362 363 // returns the state of the LRO as returned from the service
364 pollingStatus() string
365 366 // returns the URL used for polling status
367 pollingURL() string
368 369 // returns the URL used for the final GET to retrieve the resource
370 finalGetURL() string
371 372 // returns true if the LRO is in a terminal state
373 hasTerminated() bool
374 375 // returns true if the LRO is in a failed terminal state
376 hasFailed() bool
377 378 // returns true if the LRO is in a successful terminal state
379 hasSucceeded() bool
380 381 // returns the cached HTTP response after a call to pollForStatus(), can be nil
382 latestResponse() *http.Response
383 }
384 385 type pollingTrackerBase struct {
386 // resp is the last response, either from the submission of the LRO or from polling
387 resp *http.Response
388 389 // method is the HTTP verb, this is needed for deserialization
390 Method string `json:"method"`
391 392 // rawBody is the raw JSON response body
393 rawBody map[string]interface{}
394 395 // denotes if polling is using async-operation or location header
396 Pm PollingMethodType `json:"pollingMethod"`
397 398 // the URL to poll for status
399 URI string `json:"pollingURI"`
400 401 // the state of the LRO as returned from the service
402 State string `json:"lroState"`
403 404 // the URL to GET for the final result
405 FinalGetURI string `json:"resultURI"`
406 407 // used to hold an error object returned from the service
408 Err *ServiceError `json:"error,omitempty"`
409 }
410 411 func (pt *pollingTrackerBase) initializeState() error {
412 // determine the initial polling state based on response body and/or HTTP status
413 // code. this is applicable to the initial LRO response, not polling responses!
414 pt.Method = pt.resp.Request.Method
415 if err := pt.updateRawBody(); err != nil {
416 return err
417 }
418 switch pt.resp.StatusCode {
419 case http.StatusOK:
420 if ps := pt.getProvisioningState(); ps != nil {
421 pt.State = *ps
422 if pt.hasFailed() {
423 pt.updateErrorFromResponse()
424 return pt.pollingError()
425 }
426 } else {
427 pt.State = operationSucceeded
428 }
429 case http.StatusCreated:
430 if ps := pt.getProvisioningState(); ps != nil {
431 pt.State = *ps
432 } else {
433 pt.State = operationInProgress
434 }
435 case http.StatusAccepted:
436 pt.State = operationInProgress
437 case http.StatusNoContent:
438 pt.State = operationSucceeded
439 default:
440 pt.State = operationFailed
441 pt.updateErrorFromResponse()
442 return pt.pollingError()
443 }
444 return pt.initPollingMethod()
445 }
446 447 func (pt pollingTrackerBase) getProvisioningState() *string {
448 if pt.rawBody != nil && pt.rawBody["properties"] != nil {
449 p := pt.rawBody["properties"].(map[string]interface{})
450 if ps := p["provisioningState"]; ps != nil {
451 s := ps.(string)
452 return &s
453 }
454 }
455 return nil
456 }
457 458 func (pt *pollingTrackerBase) updateRawBody() error {
459 pt.rawBody = map[string]interface{}{}
460 if pt.resp.ContentLength != 0 {
461 defer pt.resp.Body.Close()
462 b, err := io.ReadAll(pt.resp.Body)
463 if err != nil {
464 return autorest.NewErrorWithError(err, "pollingTrackerBase", "updateRawBody", nil, "failed to read response body")
465 }
466 // put the body back so it's available to other callers
467 pt.resp.Body = io.NopCloser(bytes.NewReader(b))
468 // observed in 204 responses over HTTP/2.0; the content length is -1 but body is empty
469 if len(b) == 0 {
470 return nil
471 }
472 if err = json.Unmarshal(b, &pt.rawBody); err != nil {
473 return autorest.NewErrorWithError(err, "pollingTrackerBase", "updateRawBody", nil, "failed to unmarshal response body")
474 }
475 }
476 return nil
477 }
478 479 func (pt *pollingTrackerBase) pollForStatus(ctx context.Context, sender autorest.Sender) error {
480 req, err := http.NewRequest(http.MethodGet, pt.URI, nil)
481 if err != nil {
482 return autorest.NewErrorWithError(err, "pollingTrackerBase", "pollForStatus", nil, "failed to create HTTP request")
483 }
484 485 req = req.WithContext(ctx)
486 preparer := autorest.CreatePreparer(autorest.GetPrepareDecorators(ctx)...)
487 req, err = preparer.Prepare(req)
488 if err != nil {
489 return autorest.NewErrorWithError(err, "pollingTrackerBase", "pollForStatus", nil, "failed preparing HTTP request")
490 }
491 pt.resp, err = sender.Do(req)
492 if err != nil {
493 return autorest.NewErrorWithError(err, "pollingTrackerBase", "pollForStatus", nil, "failed to send HTTP request")
494 }
495 if autorest.ResponseHasStatusCode(pt.resp, pollingCodes[:]...) {
496 // reset the service error on success case
497 pt.Err = nil
498 err = pt.updateRawBody()
499 } else {
500 // check response body for error content
501 pt.updateErrorFromResponse()
502 err = pt.pollingError()
503 }
504 return err
505 }
506 507 // attempts to unmarshal a ServiceError type from the response body.
508 // if that fails then make a best attempt at creating something meaningful.
509 // NOTE: this assumes that the async operation has failed.
510 func (pt *pollingTrackerBase) updateErrorFromResponse() {
511 var err error
512 if pt.resp.ContentLength != 0 {
513 type respErr struct {
514 ServiceError *ServiceError `json:"error"`
515 }
516 re := respErr{}
517 defer pt.resp.Body.Close()
518 var b []byte
519 if b, err = io.ReadAll(pt.resp.Body); err != nil {
520 goto Default
521 }
522 // put the body back so it's available to other callers
523 pt.resp.Body = io.NopCloser(bytes.NewReader(b))
524 if len(b) == 0 {
525 goto Default
526 }
527 if err = json.Unmarshal(b, &re); err != nil {
528 goto Default
529 }
530 // unmarshalling the error didn't yield anything, try unwrapped error
531 if re.ServiceError == nil {
532 err = json.Unmarshal(b, &re.ServiceError)
533 if err != nil {
534 goto Default
535 }
536 }
537 // the unmarshaller will ensure re.ServiceError is non-nil
538 // even if there was no content unmarshalled so check the code.
539 if re.ServiceError.Code != "" {
540 pt.Err = re.ServiceError
541 return
542 }
543 }
544 Default:
545 se := &ServiceError{
546 Code: pt.pollingStatus(),
547 Message: "The async operation failed.",
548 }
549 if err != nil {
550 se.InnerError = make(map[string]interface{})
551 se.InnerError["unmarshalError"] = err.Error()
552 }
553 // stick the response body into the error object in hopes
554 // it contains something useful to help diagnose the failure.
555 if len(pt.rawBody) > 0 {
556 se.AdditionalInfo = []map[string]interface{}{
557 pt.rawBody,
558 }
559 }
560 pt.Err = se
561 }
562 563 func (pt *pollingTrackerBase) updatePollingState(provStateApl bool) error {
564 if pt.Pm == PollingAsyncOperation && pt.rawBody["status"] != nil {
565 pt.State = pt.rawBody["status"].(string)
566 } else {
567 if pt.resp.StatusCode == http.StatusAccepted {
568 pt.State = operationInProgress
569 } else if provStateApl {
570 if ps := pt.getProvisioningState(); ps != nil {
571 pt.State = *ps
572 } else {
573 pt.State = operationSucceeded
574 }
575 } else {
576 return autorest.NewError("pollingTrackerBase", "updatePollingState", "the response from the async operation has an invalid status code")
577 }
578 }
579 // if the operation has failed update the error state
580 if pt.hasFailed() {
581 pt.updateErrorFromResponse()
582 }
583 return nil
584 }
585 586 func (pt pollingTrackerBase) pollingError() error {
587 if pt.Err == nil {
588 return nil
589 }
590 return pt.Err
591 }
592 593 func (pt pollingTrackerBase) pollingMethod() PollingMethodType {
594 return pt.Pm
595 }
596 597 func (pt pollingTrackerBase) pollingStatus() string {
598 return pt.State
599 }
600 601 func (pt pollingTrackerBase) pollingURL() string {
602 return pt.URI
603 }
604 605 func (pt pollingTrackerBase) finalGetURL() string {
606 return pt.FinalGetURI
607 }
608 609 func (pt pollingTrackerBase) hasTerminated() bool {
610 return strings.EqualFold(pt.State, operationCanceled) || strings.EqualFold(pt.State, operationFailed) || strings.EqualFold(pt.State, operationSucceeded)
611 }
612 613 func (pt pollingTrackerBase) hasFailed() bool {
614 return strings.EqualFold(pt.State, operationCanceled) || strings.EqualFold(pt.State, operationFailed)
615 }
616 617 func (pt pollingTrackerBase) hasSucceeded() bool {
618 return strings.EqualFold(pt.State, operationSucceeded)
619 }
620 621 func (pt pollingTrackerBase) latestResponse() *http.Response {
622 return pt.resp
623 }
624 625 // error checking common to all trackers
626 func (pt pollingTrackerBase) baseCheckForErrors() error {
627 // for Azure-AsyncOperations the response body cannot be nil or empty
628 if pt.Pm == PollingAsyncOperation {
629 if pt.resp.Body == nil || pt.resp.ContentLength == 0 {
630 return autorest.NewError("pollingTrackerBase", "baseCheckForErrors", "for Azure-AsyncOperation response body cannot be nil")
631 }
632 if pt.rawBody["status"] == nil {
633 return autorest.NewError("pollingTrackerBase", "baseCheckForErrors", "missing status property in Azure-AsyncOperation response body")
634 }
635 }
636 return nil
637 }
638 639 // default initialization of polling URL/method. each verb tracker will update this as required.
640 func (pt *pollingTrackerBase) initPollingMethod() error {
641 if ao, err := getURLFromAsyncOpHeader(pt.resp); err != nil {
642 return err
643 } else if ao != "" {
644 pt.URI = ao
645 pt.Pm = PollingAsyncOperation
646 return nil
647 }
648 if lh, err := getURLFromLocationHeader(pt.resp); err != nil {
649 return err
650 } else if lh != "" {
651 pt.URI = lh
652 pt.Pm = PollingLocation
653 return nil
654 }
655 // it's ok if we didn't find a polling header, this will be handled elsewhere
656 return nil
657 }
658 659 // DELETE
660 661 type pollingTrackerDelete struct {
662 pollingTrackerBase
663 }
664 665 func (pt *pollingTrackerDelete) updatePollingMethod() error {
666 // for 201 the Location header is required
667 if pt.resp.StatusCode == http.StatusCreated {
668 if lh, err := getURLFromLocationHeader(pt.resp); err != nil {
669 return err
670 } else if lh == "" {
671 return autorest.NewError("pollingTrackerDelete", "updateHeaders", "missing Location header in 201 response")
672 } else {
673 pt.URI = lh
674 }
675 pt.Pm = PollingLocation
676 pt.FinalGetURI = pt.URI
677 }
678 // for 202 prefer the Azure-AsyncOperation header but fall back to Location if necessary
679 if pt.resp.StatusCode == http.StatusAccepted {
680 ao, err := getURLFromAsyncOpHeader(pt.resp)
681 if err != nil {
682 return err
683 } else if ao != "" {
684 pt.URI = ao
685 pt.Pm = PollingAsyncOperation
686 }
687 // if the Location header is invalid and we already have a polling URL
688 // then we don't care if the Location header URL is malformed.
689 if lh, err := getURLFromLocationHeader(pt.resp); err != nil && pt.URI == "" {
690 return err
691 } else if lh != "" {
692 if ao == "" {
693 pt.URI = lh
694 pt.Pm = PollingLocation
695 }
696 // when both headers are returned we use the value in the Location header for the final GET
697 pt.FinalGetURI = lh
698 }
699 // make sure a polling URL was found
700 if pt.URI == "" {
701 return autorest.NewError("pollingTrackerPost", "updateHeaders", "didn't get any suitable polling URLs in 202 response")
702 }
703 }
704 return nil
705 }
706 707 func (pt pollingTrackerDelete) checkForErrors() error {
708 return pt.baseCheckForErrors()
709 }
710 711 func (pt pollingTrackerDelete) provisioningStateApplicable() bool {
712 return pt.resp.StatusCode == http.StatusOK || pt.resp.StatusCode == http.StatusNoContent
713 }
714 715 // PATCH
716 717 type pollingTrackerPatch struct {
718 pollingTrackerBase
719 }
720 721 func (pt *pollingTrackerPatch) updatePollingMethod() error {
722 // by default we can use the original URL for polling and final GET
723 if pt.URI == "" {
724 pt.URI = pt.resp.Request.URL.String()
725 }
726 if pt.FinalGetURI == "" {
727 pt.FinalGetURI = pt.resp.Request.URL.String()
728 }
729 if pt.Pm == PollingUnknown {
730 pt.Pm = PollingRequestURI
731 }
732 // for 201 it's permissible for no headers to be returned
733 if pt.resp.StatusCode == http.StatusCreated {
734 if ao, err := getURLFromAsyncOpHeader(pt.resp); err != nil {
735 return err
736 } else if ao != "" {
737 pt.URI = ao
738 pt.Pm = PollingAsyncOperation
739 }
740 }
741 // for 202 prefer the Azure-AsyncOperation header but fall back to Location if necessary
742 // note the absence of the "final GET" mechanism for PATCH
743 if pt.resp.StatusCode == http.StatusAccepted {
744 ao, err := getURLFromAsyncOpHeader(pt.resp)
745 if err != nil {
746 return err
747 } else if ao != "" {
748 pt.URI = ao
749 pt.Pm = PollingAsyncOperation
750 }
751 if ao == "" {
752 if lh, err := getURLFromLocationHeader(pt.resp); err != nil {
753 return err
754 } else if lh == "" {
755 return autorest.NewError("pollingTrackerPatch", "updateHeaders", "didn't get any suitable polling URLs in 202 response")
756 } else {
757 pt.URI = lh
758 pt.Pm = PollingLocation
759 }
760 }
761 }
762 return nil
763 }
764 765 func (pt pollingTrackerPatch) checkForErrors() error {
766 return pt.baseCheckForErrors()
767 }
768 769 func (pt pollingTrackerPatch) provisioningStateApplicable() bool {
770 return pt.resp.StatusCode == http.StatusOK || pt.resp.StatusCode == http.StatusCreated
771 }
772 773 // POST
774 775 type pollingTrackerPost struct {
776 pollingTrackerBase
777 }
778 779 func (pt *pollingTrackerPost) updatePollingMethod() error {
780 // 201 requires Location header
781 if pt.resp.StatusCode == http.StatusCreated {
782 if lh, err := getURLFromLocationHeader(pt.resp); err != nil {
783 return err
784 } else if lh == "" {
785 return autorest.NewError("pollingTrackerPost", "updateHeaders", "missing Location header in 201 response")
786 } else {
787 pt.URI = lh
788 pt.FinalGetURI = lh
789 pt.Pm = PollingLocation
790 }
791 }
792 // for 202 prefer the Azure-AsyncOperation header but fall back to Location if necessary
793 if pt.resp.StatusCode == http.StatusAccepted {
794 ao, err := getURLFromAsyncOpHeader(pt.resp)
795 if err != nil {
796 return err
797 } else if ao != "" {
798 pt.URI = ao
799 pt.Pm = PollingAsyncOperation
800 }
801 // if the Location header is invalid and we already have a polling URL
802 // then we don't care if the Location header URL is malformed.
803 if lh, err := getURLFromLocationHeader(pt.resp); err != nil && pt.URI == "" {
804 return err
805 } else if lh != "" {
806 if ao == "" {
807 pt.URI = lh
808 pt.Pm = PollingLocation
809 }
810 // when both headers are returned we use the value in the Location header for the final GET
811 pt.FinalGetURI = lh
812 }
813 // make sure a polling URL was found
814 if pt.URI == "" {
815 return autorest.NewError("pollingTrackerPost", "updateHeaders", "didn't get any suitable polling URLs in 202 response")
816 }
817 }
818 return nil
819 }
820 821 func (pt pollingTrackerPost) checkForErrors() error {
822 return pt.baseCheckForErrors()
823 }
824 825 func (pt pollingTrackerPost) provisioningStateApplicable() bool {
826 return pt.resp.StatusCode == http.StatusOK || pt.resp.StatusCode == http.StatusNoContent
827 }
828 829 // PUT
830 831 type pollingTrackerPut struct {
832 pollingTrackerBase
833 }
834 835 func (pt *pollingTrackerPut) updatePollingMethod() error {
836 // by default we can use the original URL for polling and final GET
837 if pt.URI == "" {
838 pt.URI = pt.resp.Request.URL.String()
839 }
840 if pt.FinalGetURI == "" {
841 pt.FinalGetURI = pt.resp.Request.URL.String()
842 }
843 if pt.Pm == PollingUnknown {
844 pt.Pm = PollingRequestURI
845 }
846 // for 201 it's permissible for no headers to be returned
847 if pt.resp.StatusCode == http.StatusCreated {
848 if ao, err := getURLFromAsyncOpHeader(pt.resp); err != nil {
849 return err
850 } else if ao != "" {
851 pt.URI = ao
852 pt.Pm = PollingAsyncOperation
853 }
854 }
855 // for 202 prefer the Azure-AsyncOperation header but fall back to Location if necessary
856 if pt.resp.StatusCode == http.StatusAccepted {
857 ao, err := getURLFromAsyncOpHeader(pt.resp)
858 if err != nil {
859 return err
860 } else if ao != "" {
861 pt.URI = ao
862 pt.Pm = PollingAsyncOperation
863 }
864 // if the Location header is invalid and we already have a polling URL
865 // then we don't care if the Location header URL is malformed.
866 if lh, err := getURLFromLocationHeader(pt.resp); err != nil && pt.URI == "" {
867 return err
868 } else if lh != "" {
869 if ao == "" {
870 pt.URI = lh
871 pt.Pm = PollingLocation
872 }
873 }
874 // make sure a polling URL was found
875 if pt.URI == "" {
876 return autorest.NewError("pollingTrackerPut", "updateHeaders", "didn't get any suitable polling URLs in 202 response")
877 }
878 }
879 return nil
880 }
881 882 func (pt pollingTrackerPut) checkForErrors() error {
883 err := pt.baseCheckForErrors()
884 if err != nil {
885 return err
886 }
887 // if there are no LRO headers then the body cannot be empty
888 ao, err := getURLFromAsyncOpHeader(pt.resp)
889 if err != nil {
890 return err
891 }
892 lh, err := getURLFromLocationHeader(pt.resp)
893 if err != nil {
894 return err
895 }
896 if ao == "" && lh == "" && len(pt.rawBody) == 0 {
897 return autorest.NewError("pollingTrackerPut", "checkForErrors", "the response did not contain a body")
898 }
899 return nil
900 }
901 902 func (pt pollingTrackerPut) provisioningStateApplicable() bool {
903 return pt.resp.StatusCode == http.StatusOK || pt.resp.StatusCode == http.StatusCreated
904 }
905 906 // creates a polling tracker based on the verb of the original request
907 func createPollingTracker(resp *http.Response) (pollingTracker, error) {
908 var pt pollingTracker
909 switch strings.ToUpper(resp.Request.Method) {
910 case http.MethodDelete:
911 pt = &pollingTrackerDelete{pollingTrackerBase: pollingTrackerBase{resp: resp}}
912 case http.MethodPatch:
913 pt = &pollingTrackerPatch{pollingTrackerBase: pollingTrackerBase{resp: resp}}
914 case http.MethodPost:
915 pt = &pollingTrackerPost{pollingTrackerBase: pollingTrackerBase{resp: resp}}
916 case http.MethodPut:
917 pt = &pollingTrackerPut{pollingTrackerBase: pollingTrackerBase{resp: resp}}
918 default:
919 return nil, autorest.NewError("azure", "createPollingTracker", "unsupported HTTP method %s", resp.Request.Method)
920 }
921 if err := pt.initializeState(); err != nil {
922 return pt, err
923 }
924 // this initializes the polling header values, we do this during creation in case the
925 // initial response send us invalid values; this way the API call will return a non-nil
926 // error (not doing this means the error shows up in Future.Done)
927 return pt, pt.updatePollingMethod()
928 }
929 930 // gets the polling URL from the Azure-AsyncOperation header.
931 // ensures the URL is well-formed and absolute.
932 func getURLFromAsyncOpHeader(resp *http.Response) (string, error) {
933 s := resp.Header.Get(http.CanonicalHeaderKey(headerAsyncOperation))
934 if s == "" {
935 return "", nil
936 }
937 if !isValidURL(s) {
938 return "", autorest.NewError("azure", "getURLFromAsyncOpHeader", "invalid polling URL '%s'", s)
939 }
940 return s, nil
941 }
942 943 // gets the polling URL from the Location header.
944 // ensures the URL is well-formed and absolute.
945 func getURLFromLocationHeader(resp *http.Response) (string, error) {
946 s := resp.Header.Get(http.CanonicalHeaderKey(autorest.HeaderLocation))
947 if s == "" {
948 return "", nil
949 }
950 if !isValidURL(s) {
951 return "", autorest.NewError("azure", "getURLFromLocationHeader", "invalid polling URL '%s'", s)
952 }
953 return s, nil
954 }
955 956 // verify that the URL is valid and absolute
957 func isValidURL(s string) bool {
958 u, err := url.Parse(s)
959 return err == nil && u.IsAbs()
960 }
961 962 // PollingMethodType defines a type used for enumerating polling mechanisms.
963 type PollingMethodType string
964 965 const (
966 // PollingAsyncOperation indicates the polling method uses the Azure-AsyncOperation header.
967 PollingAsyncOperation PollingMethodType = "AsyncOperation"
968 969 // PollingLocation indicates the polling method uses the Location header.
970 PollingLocation PollingMethodType = "Location"
971 972 // PollingRequestURI indicates the polling method uses the original request URI.
973 PollingRequestURI PollingMethodType = "RequestURI"
974 975 // PollingUnknown indicates an unknown polling method and is the default value.
976 PollingUnknown PollingMethodType = ""
977 )
978 979 // AsyncOpIncompleteError is the type that's returned from a future that has not completed.
980 type AsyncOpIncompleteError struct {
981 // FutureType is the name of the type composed of a azure.Future.
982 FutureType string
983 }
984 985 // Error returns an error message including the originating type name of the error.
986 func (e AsyncOpIncompleteError) Error() string {
987 return fmt.Sprintf("%s: asynchronous operation has not completed", e.FutureType)
988 }
989 990 // NewAsyncOpIncompleteError creates a new AsyncOpIncompleteError with the specified parameters.
991 func NewAsyncOpIncompleteError(futureType string) AsyncOpIncompleteError {
992 return AsyncOpIncompleteError{
993 FutureType: futureType,
994 }
995 }
996