| 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 ( |
| "bytes" |
| "context" |
| "encoding/json" |
| "fmt" |
| "io/ioutil" |
| "net/http" |
| "net/url" |
| "strings" |
| "time" |
| |
| "github.com/Azure/go-autorest/autorest" |
| "github.com/Azure/go-autorest/tracing" |
| ) |
| |
| const ( |
| headerAsyncOperation = "Azure-AsyncOperation" |
| ) |
| |
| const ( |
| operationInProgress string = "InProgress" |
| operationCanceled string = "Canceled" |
| operationFailed string = "Failed" |
| operationSucceeded string = "Succeeded" |
| ) |
| |
| var pollingCodes = [...]int{http.StatusNoContent, http.StatusAccepted, http.StatusCreated, http.StatusOK} |
| |
| // Future provides a mechanism to access the status and results of an asynchronous request. |
| // Since futures are stateful they should be passed by value to avoid race conditions. |
| type Future struct { |
| req *http.Request // legacy |
| pt pollingTracker |
| } |
| |
| // NewFuture returns a new Future object initialized with the specified request. |
| // Deprecated: Please use NewFutureFromResponse instead. |
| func NewFuture(req *http.Request) Future { |
| return Future{req: req} |
| } |
| |
| // NewFutureFromResponse returns a new Future object initialized |
| // with the initial response from an asynchronous operation. |
| func NewFutureFromResponse(resp *http.Response) (Future, error) { |
| pt, err := createPollingTracker(resp) |
| return Future{pt: pt}, err |
| } |
| |
| // Response returns the last HTTP response. |
| func (f Future) Response() *http.Response { |
| if f.pt == nil { |
| return nil |
| } |
| return f.pt.latestResponse() |
| } |
| |
| // Status returns the last status message of the operation. |
| func (f Future) Status() string { |
| if f.pt == nil { |
| return "" |
| } |
| return f.pt.pollingStatus() |
| } |
| |
| // PollingMethod returns the method used to monitor the status of the asynchronous operation. |
| func (f Future) PollingMethod() PollingMethodType { |
| if f.pt == nil { |
| return PollingUnknown |
| } |
| return f.pt.pollingMethod() |
| } |
| |
| // Done queries the service to see if the operation has completed. |
| // Deprecated: Use DoneWithContext() |
| func (f *Future) Done(sender autorest.Sender) (bool, error) { |
| return f.DoneWithContext(context.Background(), sender) |
| } |
| |
| // DoneWithContext queries the service to see if the operation has completed. |
| func (f *Future) DoneWithContext(ctx context.Context, sender autorest.Sender) (done bool, err error) { |
| ctx = tracing.StartSpan(ctx, "github.com/Azure/go-autorest/autorest/azure/async.DoneWithContext") |
| defer func() { |
| sc := -1 |
| resp := f.Response() |
| if resp != nil { |
| sc = resp.StatusCode |
| } |
| tracing.EndSpan(ctx, sc, err) |
| }() |
| |
| // support for legacy Future implementation |
| if f.req != nil { |
| resp, err := sender.Do(f.req) |
| if err != nil { |
| return false, err |
| } |
| pt, err := createPollingTracker(resp) |
| if err != nil { |
| return false, err |
| } |
| f.pt = pt |
| f.req = nil |
| } |
| // end legacy |
| if f.pt == nil { |
| return false, autorest.NewError("Future", "Done", "future is not initialized") |
| } |
| if f.pt.hasTerminated() { |
| return true, f.pt.pollingError() |
| } |
| if err := f.pt.pollForStatus(ctx, sender); err != nil { |
| return false, err |
| } |
| if err := f.pt.checkForErrors(); err != nil { |
| return f.pt.hasTerminated(), err |
| } |
| if err := f.pt.updatePollingState(f.pt.provisioningStateApplicable()); err != nil { |
| return false, err |
| } |
| if err := f.pt.initPollingMethod(); err != nil { |
| return false, err |
| } |
| if err := f.pt.updatePollingMethod(); err != nil { |
| return false, err |
| } |
| return f.pt.hasTerminated(), f.pt.pollingError() |
| } |
| |
| // GetPollingDelay returns a duration the application should wait before checking |
| // the status of the asynchronous request and true; this value is returned from |
| // the service via the Retry-After response header. If the header wasn't returned |
| // then the function returns the zero-value time.Duration and false. |
| func (f Future) GetPollingDelay() (time.Duration, bool) { |
| if f.pt == nil { |
| return 0, false |
| } |
| resp := f.pt.latestResponse() |
| if resp == nil { |
| return 0, false |
| } |
| |
| retry := resp.Header.Get(autorest.HeaderRetryAfter) |
| if retry == "" { |
| return 0, false |
| } |
| |
| d, err := time.ParseDuration(retry + "s") |
| if err != nil { |
| panic(err) |
| } |
| |
| return d, true |
| } |
| |
| // WaitForCompletion will return when one of the following conditions is met: the long |
| // running operation has completed, the provided context is cancelled, or the client's |
| // polling duration has been exceeded. It will retry failed polling attempts based on |
| // the retry value defined in the client up to the maximum retry attempts. |
| // Deprecated: Please use WaitForCompletionRef() instead. |
| func (f Future) WaitForCompletion(ctx context.Context, client autorest.Client) error { |
| return f.WaitForCompletionRef(ctx, client) |
| } |
| |
| // WaitForCompletionRef will return when one of the following conditions is met: the long |
| // running operation has completed, the provided context is cancelled, or the client's |
| // polling duration has been exceeded. It will retry failed polling attempts based on |
| // the retry value defined in the client up to the maximum retry attempts. |
| // If no deadline is specified in the context then the client.PollingDuration will be |
| // used to determine if a default deadline should be used. |
| // If PollingDuration is greater than zero the value will be used as the context's timeout. |
| // If PollingDuration is zero then no default deadline will be used. |
| func (f *Future) WaitForCompletionRef(ctx context.Context, client autorest.Client) (err error) { |
| ctx = tracing.StartSpan(ctx, "github.com/Azure/go-autorest/autorest/azure/async.WaitForCompletionRef") |
| defer func() { |
| sc := -1 |
| resp := f.Response() |
| if resp != nil { |
| sc = resp.StatusCode |
| } |
| tracing.EndSpan(ctx, sc, err) |
| }() |
| cancelCtx := ctx |
| // if the provided context already has a deadline don't override it |
| _, hasDeadline := ctx.Deadline() |
| if d := client.PollingDuration; !hasDeadline && d != 0 { |
| var cancel context.CancelFunc |
| cancelCtx, cancel = context.WithTimeout(ctx, d) |
| defer cancel() |
| } |
| |
| done, err := f.DoneWithContext(ctx, client) |
| for attempts := 0; !done; done, err = f.DoneWithContext(ctx, client) { |
| if attempts >= client.RetryAttempts { |
| return autorest.NewErrorWithError(err, "Future", "WaitForCompletion", f.pt.latestResponse(), "the number of retries has been exceeded") |
| } |
| // we want delayAttempt to be zero in the non-error case so |
| // that DelayForBackoff doesn't perform exponential back-off |
| var delayAttempt int |
| var delay time.Duration |
| if err == nil { |
| // check for Retry-After delay, if not present use the client's polling delay |
| var ok bool |
| delay, ok = f.GetPollingDelay() |
| if !ok { |
| delay = client.PollingDelay |
| } |
| } else { |
| // there was an error polling for status so perform exponential |
| // back-off based on the number of attempts using the client's retry |
| // duration. update attempts after delayAttempt to avoid off-by-one. |
| delayAttempt = attempts |
| delay = client.RetryDuration |
| attempts++ |
| } |
| // wait until the delay elapses or the context is cancelled |
| delayElapsed := autorest.DelayForBackoff(delay, delayAttempt, cancelCtx.Done()) |
| if !delayElapsed { |
| return autorest.NewErrorWithError(cancelCtx.Err(), "Future", "WaitForCompletion", f.pt.latestResponse(), "context has been cancelled") |
| } |
| } |
| return |
| } |
| |
| // MarshalJSON implements the json.Marshaler interface. |
| func (f Future) MarshalJSON() ([]byte, error) { |
| return json.Marshal(f.pt) |
| } |
| |
| // UnmarshalJSON implements the json.Unmarshaler interface. |
| func (f *Future) UnmarshalJSON(data []byte) error { |
| // unmarshal into JSON object to determine the tracker type |
| obj := map[string]interface{}{} |
| err := json.Unmarshal(data, &obj) |
| if err != nil { |
| return err |
| } |
| if obj["method"] == nil { |
| return autorest.NewError("Future", "UnmarshalJSON", "missing 'method' property") |
| } |
| method := obj["method"].(string) |
| switch strings.ToUpper(method) { |
| case http.MethodDelete: |
| f.pt = &pollingTrackerDelete{} |
| case http.MethodPatch: |
| f.pt = &pollingTrackerPatch{} |
| case http.MethodPost: |
| f.pt = &pollingTrackerPost{} |
| case http.MethodPut: |
| f.pt = &pollingTrackerPut{} |
| default: |
| return autorest.NewError("Future", "UnmarshalJSON", "unsupoorted method '%s'", method) |
| } |
| // now unmarshal into the tracker |
| return json.Unmarshal(data, &f.pt) |
| } |
| |
| // PollingURL returns the URL used for retrieving the status of the long-running operation. |
| func (f Future) PollingURL() string { |
| if f.pt == nil { |
| return "" |
| } |
| return f.pt.pollingURL() |
| } |
| |
| // GetResult should be called once polling has completed successfully. |
| // It makes the final GET call to retrieve the resultant payload. |
| func (f Future) GetResult(sender autorest.Sender) (*http.Response, error) { |
| if f.pt.finalGetURL() == "" { |
| // we can end up in this situation if the async operation returns a 200 |
| // with no polling URLs. in that case return the response which should |
| // contain the JSON payload (only do this for successful terminal cases). |
| if lr := f.pt.latestResponse(); lr != nil && f.pt.hasSucceeded() { |
| return lr, nil |
| } |
| return nil, autorest.NewError("Future", "GetResult", "missing URL for retrieving result") |
| } |
| req, err := http.NewRequest(http.MethodGet, f.pt.finalGetURL(), nil) |
| if err != nil { |
| return nil, err |
| } |
| return sender.Do(req) |
| } |
| |
| type pollingTracker interface { |
| // these methods can differ per tracker |
| |
| // checks the response headers and status code to determine the polling mechanism |
| updatePollingMethod() error |
| |
| // checks the response for tracker-specific error conditions |
| checkForErrors() error |
| |
| // returns true if provisioning state should be checked |
| provisioningStateApplicable() bool |
| |
| // methods common to all trackers |
| |
| // initializes a tracker's polling URL and method, called for each iteration. |
| // these values can be overridden by each polling tracker as required. |
| initPollingMethod() error |
| |
| // initializes the tracker's internal state, call this when the tracker is created |
| initializeState() error |
| |
| // makes an HTTP request to check the status of the LRO |
| pollForStatus(ctx context.Context, sender autorest.Sender) error |
| |
| // updates internal tracker state, call this after each call to pollForStatus |
| updatePollingState(provStateApl bool) error |
| |
| // returns the error response from the service, can be nil |
| pollingError() error |
| |
| // returns the polling method being used |
| pollingMethod() PollingMethodType |
| |
| // returns the state of the LRO as returned from the service |
| pollingStatus() string |
| |
| // returns the URL used for polling status |
| pollingURL() string |
| |
| // returns the URL used for the final GET to retrieve the resource |
| finalGetURL() string |
| |
| // returns true if the LRO is in a terminal state |
| hasTerminated() bool |
| |
| // returns true if the LRO is in a failed terminal state |
| hasFailed() bool |
| |
| // returns true if the LRO is in a successful terminal state |
| hasSucceeded() bool |
| |
| // returns the cached HTTP response after a call to pollForStatus(), can be nil |
| latestResponse() *http.Response |
| } |
| |
| type pollingTrackerBase struct { |
| // resp is the last response, either from the submission of the LRO or from polling |
| resp *http.Response |
| |
| // method is the HTTP verb, this is needed for deserialization |
| Method string `json:"method"` |
| |
| // rawBody is the raw JSON response body |
| rawBody map[string]interface{} |
| |
| // denotes if polling is using async-operation or location header |
| Pm PollingMethodType `json:"pollingMethod"` |
| |
| // the URL to poll for status |
| URI string `json:"pollingURI"` |
| |
| // the state of the LRO as returned from the service |
| State string `json:"lroState"` |
| |
| // the URL to GET for the final result |
| FinalGetURI string `json:"resultURI"` |
| |
| // used to hold an error object returned from the service |
| Err *ServiceError `json:"error,omitempty"` |
| } |
| |
| func (pt *pollingTrackerBase) initializeState() error { |
| // determine the initial polling state based on response body and/or HTTP status |
| // code. this is applicable to the initial LRO response, not polling responses! |
| pt.Method = pt.resp.Request.Method |
| if err := pt.updateRawBody(); err != nil { |
| return err |
| } |
| switch pt.resp.StatusCode { |
| case http.StatusOK: |
| if ps := pt.getProvisioningState(); ps != nil { |
| pt.State = *ps |
| if pt.hasFailed() { |
| pt.updateErrorFromResponse() |
| return pt.pollingError() |
| } |
| } else { |
| pt.State = operationSucceeded |
| } |
| case http.StatusCreated: |
| if ps := pt.getProvisioningState(); ps != nil { |
| pt.State = *ps |
| } else { |
| pt.State = operationInProgress |
| } |
| case http.StatusAccepted: |
| pt.State = operationInProgress |
| case http.StatusNoContent: |
| pt.State = operationSucceeded |
| default: |
| pt.State = operationFailed |
| pt.updateErrorFromResponse() |
| return pt.pollingError() |
| } |
| return pt.initPollingMethod() |
| } |
| |
| func (pt pollingTrackerBase) getProvisioningState() *string { |
| if pt.rawBody != nil && pt.rawBody["properties"] != nil { |
| p := pt.rawBody["properties"].(map[string]interface{}) |
| if ps := p["provisioningState"]; ps != nil { |
| s := ps.(string) |
| return &s |
| } |
| } |
| return nil |
| } |
| |
| func (pt *pollingTrackerBase) updateRawBody() error { |
| pt.rawBody = map[string]interface{}{} |
| if pt.resp.ContentLength != 0 { |
| defer pt.resp.Body.Close() |
| b, err := ioutil.ReadAll(pt.resp.Body) |
| if err != nil { |
| return autorest.NewErrorWithError(err, "pollingTrackerBase", "updateRawBody", nil, "failed to read response body") |
| } |
| // observed in 204 responses over HTTP/2.0; the content length is -1 but body is empty |
| if len(b) == 0 { |
| return nil |
| } |
| // put the body back so it's available to other callers |
| pt.resp.Body = ioutil.NopCloser(bytes.NewReader(b)) |
| if err = json.Unmarshal(b, &pt.rawBody); err != nil { |
| return autorest.NewErrorWithError(err, "pollingTrackerBase", "updateRawBody", nil, "failed to unmarshal response body") |
| } |
| } |
| return nil |
| } |
| |
| func (pt *pollingTrackerBase) pollForStatus(ctx context.Context, sender autorest.Sender) error { |
| req, err := http.NewRequest(http.MethodGet, pt.URI, nil) |
| if err != nil { |
| return autorest.NewErrorWithError(err, "pollingTrackerBase", "pollForStatus", nil, "failed to create HTTP request") |
| } |
| |
| req = req.WithContext(ctx) |
| pt.resp, err = sender.Do(req) |
| if err != nil { |
| return autorest.NewErrorWithError(err, "pollingTrackerBase", "pollForStatus", nil, "failed to send HTTP request") |
| } |
| if autorest.ResponseHasStatusCode(pt.resp, pollingCodes[:]...) { |
| // reset the service error on success case |
| pt.Err = nil |
| err = pt.updateRawBody() |
| } else { |
| // check response body for error content |
| pt.updateErrorFromResponse() |
| err = pt.pollingError() |
| } |
| return err |
| } |
| |
| // attempts to unmarshal a ServiceError type from the response body. |
| // if that fails then make a best attempt at creating something meaningful. |
| // NOTE: this assumes that the async operation has failed. |
| func (pt *pollingTrackerBase) updateErrorFromResponse() { |
| var err error |
| if pt.resp.ContentLength != 0 { |
| type respErr struct { |
| ServiceError *ServiceError `json:"error"` |
| } |
| re := respErr{} |
| defer pt.resp.Body.Close() |
| var b []byte |
| if b, err = ioutil.ReadAll(pt.resp.Body); err != nil || len(b) == 0 { |
| goto Default |
| } |
| if err = json.Unmarshal(b, &re); err != nil { |
| goto Default |
| } |
| // unmarshalling the error didn't yield anything, try unwrapped error |
| if re.ServiceError == nil { |
| err = json.Unmarshal(b, &re.ServiceError) |
| if err != nil { |
| goto Default |
| } |
| } |
| // the unmarshaller will ensure re.ServiceError is non-nil |
| // even if there was no content unmarshalled so check the code. |
| if re.ServiceError.Code != "" { |
| pt.Err = re.ServiceError |
| return |
| } |
| } |
| Default: |
| se := &ServiceError{ |
| Code: pt.pollingStatus(), |
| Message: "The async operation failed.", |
| } |
| if err != nil { |
| se.InnerError = make(map[string]interface{}) |
| se.InnerError["unmarshalError"] = err.Error() |
| } |
| // stick the response body into the error object in hopes |
| // it contains something useful to help diagnose the failure. |
| if len(pt.rawBody) > 0 { |
| se.AdditionalInfo = []map[string]interface{}{ |
| pt.rawBody, |
| } |
| } |
| pt.Err = se |
| } |
| |
| func (pt *pollingTrackerBase) updatePollingState(provStateApl bool) error { |
| if pt.Pm == PollingAsyncOperation && pt.rawBody["status"] != nil { |
| pt.State = pt.rawBody["status"].(string) |
| } else { |
| if pt.resp.StatusCode == http.StatusAccepted { |
| pt.State = operationInProgress |
| } else if provStateApl { |
| if ps := pt.getProvisioningState(); ps != nil { |
| pt.State = *ps |
| } else { |
| pt.State = operationSucceeded |
| } |
| } else { |
| return autorest.NewError("pollingTrackerBase", "updatePollingState", "the response from the async operation has an invalid status code") |
| } |
| } |
| // if the operation has failed update the error state |
| if pt.hasFailed() { |
| pt.updateErrorFromResponse() |
| } |
| return nil |
| } |
| |
| func (pt pollingTrackerBase) pollingError() error { |
| if pt.Err == nil { |
| return nil |
| } |
| return pt.Err |
| } |
| |
| func (pt pollingTrackerBase) pollingMethod() PollingMethodType { |
| return pt.Pm |
| } |
| |
| func (pt pollingTrackerBase) pollingStatus() string { |
| return pt.State |
| } |
| |
| func (pt pollingTrackerBase) pollingURL() string { |
| return pt.URI |
| } |
| |
| func (pt pollingTrackerBase) finalGetURL() string { |
| return pt.FinalGetURI |
| } |
| |
| func (pt pollingTrackerBase) hasTerminated() bool { |
| return strings.EqualFold(pt.State, operationCanceled) || strings.EqualFold(pt.State, operationFailed) || strings.EqualFold(pt.State, operationSucceeded) |
| } |
| |
| func (pt pollingTrackerBase) hasFailed() bool { |
| return strings.EqualFold(pt.State, operationCanceled) || strings.EqualFold(pt.State, operationFailed) |
| } |
| |
| func (pt pollingTrackerBase) hasSucceeded() bool { |
| return strings.EqualFold(pt.State, operationSucceeded) |
| } |
| |
| func (pt pollingTrackerBase) latestResponse() *http.Response { |
| return pt.resp |
| } |
| |
| // error checking common to all trackers |
| func (pt pollingTrackerBase) baseCheckForErrors() error { |
| // for Azure-AsyncOperations the response body cannot be nil or empty |
| if pt.Pm == PollingAsyncOperation { |
| if pt.resp.Body == nil || pt.resp.ContentLength == 0 { |
| return autorest.NewError("pollingTrackerBase", "baseCheckForErrors", "for Azure-AsyncOperation response body cannot be nil") |
| } |
| if pt.rawBody["status"] == nil { |
| return autorest.NewError("pollingTrackerBase", "baseCheckForErrors", "missing status property in Azure-AsyncOperation response body") |
| } |
| } |
| return nil |
| } |
| |
| // default initialization of polling URL/method. each verb tracker will update this as required. |
| func (pt *pollingTrackerBase) initPollingMethod() error { |
| if ao, err := getURLFromAsyncOpHeader(pt.resp); err != nil { |
| return err |
| } else if ao != "" { |
| pt.URI = ao |
| pt.Pm = PollingAsyncOperation |
| return nil |
| } |
| if lh, err := getURLFromLocationHeader(pt.resp); err != nil { |
| return err |
| } else if lh != "" { |
| pt.URI = lh |
| pt.Pm = PollingLocation |
| return nil |
| } |
| // it's ok if we didn't find a polling header, this will be handled elsewhere |
| return nil |
| } |
| |
| // DELETE |
| |
| type pollingTrackerDelete struct { |
| pollingTrackerBase |
| } |
| |
| func (pt *pollingTrackerDelete) updatePollingMethod() error { |
| // for 201 the Location header is required |
| if pt.resp.StatusCode == http.StatusCreated { |
| if lh, err := getURLFromLocationHeader(pt.resp); err != nil { |
| return err |
| } else if lh == "" { |
| return autorest.NewError("pollingTrackerDelete", "updateHeaders", "missing Location header in 201 response") |
| } else { |
| pt.URI = lh |
| } |
| pt.Pm = PollingLocation |
| pt.FinalGetURI = pt.URI |
| } |
| // for 202 prefer the Azure-AsyncOperation header but fall back to Location if necessary |
| if pt.resp.StatusCode == http.StatusAccepted { |
| ao, err := getURLFromAsyncOpHeader(pt.resp) |
| if err != nil { |
| return err |
| } else if ao != "" { |
| pt.URI = ao |
| pt.Pm = PollingAsyncOperation |
| } |
| // if the Location header is invalid and we already have a polling URL |
| // then we don't care if the Location header URL is malformed. |
| if lh, err := getURLFromLocationHeader(pt.resp); err != nil && pt.URI == "" { |
| return err |
| } else if lh != "" { |
| if ao == "" { |
| pt.URI = lh |
| pt.Pm = PollingLocation |
| } |
| // when both headers are returned we use the value in the Location header for the final GET |
| pt.FinalGetURI = lh |
| } |
| // make sure a polling URL was found |
| if pt.URI == "" { |
| return autorest.NewError("pollingTrackerPost", "updateHeaders", "didn't get any suitable polling URLs in 202 response") |
| } |
| } |
| return nil |
| } |
| |
| func (pt pollingTrackerDelete) checkForErrors() error { |
| return pt.baseCheckForErrors() |
| } |
| |
| func (pt pollingTrackerDelete) provisioningStateApplicable() bool { |
| return pt.resp.StatusCode == http.StatusOK || pt.resp.StatusCode == http.StatusNoContent |
| } |
| |
| // PATCH |
| |
| type pollingTrackerPatch struct { |
| pollingTrackerBase |
| } |
| |
| func (pt *pollingTrackerPatch) updatePollingMethod() error { |
| // by default we can use the original URL for polling and final GET |
| if pt.URI == "" { |
| pt.URI = pt.resp.Request.URL.String() |
| } |
| if pt.FinalGetURI == "" { |
| pt.FinalGetURI = pt.resp.Request.URL.String() |
| } |
| if pt.Pm == PollingUnknown { |
| pt.Pm = PollingRequestURI |
| } |
| // for 201 it's permissible for no headers to be returned |
| if pt.resp.StatusCode == http.StatusCreated { |
| if ao, err := getURLFromAsyncOpHeader(pt.resp); err != nil { |
| return err |
| } else if ao != "" { |
| pt.URI = ao |
| pt.Pm = PollingAsyncOperation |
| } |
| } |
| // for 202 prefer the Azure-AsyncOperation header but fall back to Location if necessary |
| // note the absence of the "final GET" mechanism for PATCH |
| if pt.resp.StatusCode == http.StatusAccepted { |
| ao, err := getURLFromAsyncOpHeader(pt.resp) |
| if err != nil { |
| return err |
| } else if ao != "" { |
| pt.URI = ao |
| pt.Pm = PollingAsyncOperation |
| } |
| if ao == "" { |
| if lh, err := getURLFromLocationHeader(pt.resp); err != nil { |
| return err |
| } else if lh == "" { |
| return autorest.NewError("pollingTrackerPatch", "updateHeaders", "didn't get any suitable polling URLs in 202 response") |
| } else { |
| pt.URI = lh |
| pt.Pm = PollingLocation |
| } |
| } |
| } |
| return nil |
| } |
| |
| func (pt pollingTrackerPatch) checkForErrors() error { |
| return pt.baseCheckForErrors() |
| } |
| |
| func (pt pollingTrackerPatch) provisioningStateApplicable() bool { |
| return pt.resp.StatusCode == http.StatusOK || pt.resp.StatusCode == http.StatusCreated |
| } |
| |
| // POST |
| |
| type pollingTrackerPost struct { |
| pollingTrackerBase |
| } |
| |
| func (pt *pollingTrackerPost) updatePollingMethod() error { |
| // 201 requires Location header |
| if pt.resp.StatusCode == http.StatusCreated { |
| if lh, err := getURLFromLocationHeader(pt.resp); err != nil { |
| return err |
| } else if lh == "" { |
| return autorest.NewError("pollingTrackerPost", "updateHeaders", "missing Location header in 201 response") |
| } else { |
| pt.URI = lh |
| pt.FinalGetURI = lh |
| pt.Pm = PollingLocation |
| } |
| } |
| // for 202 prefer the Azure-AsyncOperation header but fall back to Location if necessary |
| if pt.resp.StatusCode == http.StatusAccepted { |
| ao, err := getURLFromAsyncOpHeader(pt.resp) |
| if err != nil { |
| return err |
| } else if ao != "" { |
| pt.URI = ao |
| pt.Pm = PollingAsyncOperation |
| } |
| // if the Location header is invalid and we already have a polling URL |
| // then we don't care if the Location header URL is malformed. |
| if lh, err := getURLFromLocationHeader(pt.resp); err != nil && pt.URI == "" { |
| return err |
| } else if lh != "" { |
| if ao == "" { |
| pt.URI = lh |
| pt.Pm = PollingLocation |
| } |
| // when both headers are returned we use the value in the Location header for the final GET |
| pt.FinalGetURI = lh |
| } |
| // make sure a polling URL was found |
| if pt.URI == "" { |
| return autorest.NewError("pollingTrackerPost", "updateHeaders", "didn't get any suitable polling URLs in 202 response") |
| } |
| } |
| return nil |
| } |
| |
| func (pt pollingTrackerPost) checkForErrors() error { |
| return pt.baseCheckForErrors() |
| } |
| |
| func (pt pollingTrackerPost) provisioningStateApplicable() bool { |
| return pt.resp.StatusCode == http.StatusOK || pt.resp.StatusCode == http.StatusNoContent |
| } |
| |
| // PUT |
| |
| type pollingTrackerPut struct { |
| pollingTrackerBase |
| } |
| |
| func (pt *pollingTrackerPut) updatePollingMethod() error { |
| // by default we can use the original URL for polling and final GET |
| if pt.URI == "" { |
| pt.URI = pt.resp.Request.URL.String() |
| } |
| if pt.FinalGetURI == "" { |
| pt.FinalGetURI = pt.resp.Request.URL.String() |
| } |
| if pt.Pm == PollingUnknown { |
| pt.Pm = PollingRequestURI |
| } |
| // for 201 it's permissible for no headers to be returned |
| if pt.resp.StatusCode == http.StatusCreated { |
| if ao, err := getURLFromAsyncOpHeader(pt.resp); err != nil { |
| return err |
| } else if ao != "" { |
| pt.URI = ao |
| pt.Pm = PollingAsyncOperation |
| } |
| } |
| // for 202 prefer the Azure-AsyncOperation header but fall back to Location if necessary |
| if pt.resp.StatusCode == http.StatusAccepted { |
| ao, err := getURLFromAsyncOpHeader(pt.resp) |
| if err != nil { |
| return err |
| } else if ao != "" { |
| pt.URI = ao |
| pt.Pm = PollingAsyncOperation |
| } |
| // if the Location header is invalid and we already have a polling URL |
| // then we don't care if the Location header URL is malformed. |
| if lh, err := getURLFromLocationHeader(pt.resp); err != nil && pt.URI == "" { |
| return err |
| } else if lh != "" { |
| if ao == "" { |
| pt.URI = lh |
| pt.Pm = PollingLocation |
| } |
| } |
| // make sure a polling URL was found |
| if pt.URI == "" { |
| return autorest.NewError("pollingTrackerPut", "updateHeaders", "didn't get any suitable polling URLs in 202 response") |
| } |
| } |
| return nil |
| } |
| |
| func (pt pollingTrackerPut) checkForErrors() error { |
| err := pt.baseCheckForErrors() |
| if err != nil { |
| return err |
| } |
| // if there are no LRO headers then the body cannot be empty |
| ao, err := getURLFromAsyncOpHeader(pt.resp) |
| if err != nil { |
| return err |
| } |
| lh, err := getURLFromLocationHeader(pt.resp) |
| if err != nil { |
| return err |
| } |
| if ao == "" && lh == "" && len(pt.rawBody) == 0 { |
| return autorest.NewError("pollingTrackerPut", "checkForErrors", "the response did not contain a body") |
| } |
| return nil |
| } |
| |
| func (pt pollingTrackerPut) provisioningStateApplicable() bool { |
| return pt.resp.StatusCode == http.StatusOK || pt.resp.StatusCode == http.StatusCreated |
| } |
| |
| // creates a polling tracker based on the verb of the original request |
| func createPollingTracker(resp *http.Response) (pollingTracker, error) { |
| var pt pollingTracker |
| switch strings.ToUpper(resp.Request.Method) { |
| case http.MethodDelete: |
| pt = &pollingTrackerDelete{pollingTrackerBase: pollingTrackerBase{resp: resp}} |
| case http.MethodPatch: |
| pt = &pollingTrackerPatch{pollingTrackerBase: pollingTrackerBase{resp: resp}} |
| case http.MethodPost: |
| pt = &pollingTrackerPost{pollingTrackerBase: pollingTrackerBase{resp: resp}} |
| case http.MethodPut: |
| pt = &pollingTrackerPut{pollingTrackerBase: pollingTrackerBase{resp: resp}} |
| default: |
| return nil, autorest.NewError("azure", "createPollingTracker", "unsupported HTTP method %s", resp.Request.Method) |
| } |
| if err := pt.initializeState(); err != nil { |
| return pt, err |
| } |
| // this initializes the polling header values, we do this during creation in case the |
| // initial response send us invalid values; this way the API call will return a non-nil |
| // error (not doing this means the error shows up in Future.Done) |
| return pt, pt.updatePollingMethod() |
| } |
| |
| // gets the polling URL from the Azure-AsyncOperation header. |
| // ensures the URL is well-formed and absolute. |
| func getURLFromAsyncOpHeader(resp *http.Response) (string, error) { |
| s := resp.Header.Get(http.CanonicalHeaderKey(headerAsyncOperation)) |
| if s == "" { |
| return "", nil |
| } |
| if !isValidURL(s) { |
| return "", autorest.NewError("azure", "getURLFromAsyncOpHeader", "invalid polling URL '%s'", s) |
| } |
| return s, nil |
| } |
| |
| // gets the polling URL from the Location header. |
| // ensures the URL is well-formed and absolute. |
| func getURLFromLocationHeader(resp *http.Response) (string, error) { |
| s := resp.Header.Get(http.CanonicalHeaderKey(autorest.HeaderLocation)) |
| if s == "" { |
| return "", nil |
| } |
| if !isValidURL(s) { |
| return "", autorest.NewError("azure", "getURLFromLocationHeader", "invalid polling URL '%s'", s) |
| } |
| return s, nil |
| } |
| |
| // verify that the URL is valid and absolute |
| func isValidURL(s string) bool { |
| u, err := url.Parse(s) |
| return err == nil && u.IsAbs() |
| } |
| |
| // DoPollForAsynchronous returns a SendDecorator that polls if the http.Response is for an Azure |
| // long-running operation. It will delay between requests for the duration specified in the |
| // RetryAfter header or, if the header is absent, the passed delay. Polling may be canceled via |
| // the context associated with the http.Request. |
| // Deprecated: Prefer using Futures to allow for non-blocking async operations. |
| func DoPollForAsynchronous(delay time.Duration) autorest.SendDecorator { |
| return func(s autorest.Sender) autorest.Sender { |
| return autorest.SenderFunc(func(r *http.Request) (*http.Response, error) { |
| resp, err := s.Do(r) |
| if err != nil { |
| return resp, err |
| } |
| if !autorest.ResponseHasStatusCode(resp, pollingCodes[:]...) { |
| return resp, nil |
| } |
| future, err := NewFutureFromResponse(resp) |
| if err != nil { |
| return resp, err |
| } |
| // retry until either the LRO completes or we receive an error |
| var done bool |
| for done, err = future.Done(s); !done && err == nil; done, err = future.Done(s) { |
| // check for Retry-After delay, if not present use the specified polling delay |
| if pd, ok := future.GetPollingDelay(); ok { |
| delay = pd |
| } |
| // wait until the delay elapses or the context is cancelled |
| if delayElapsed := autorest.DelayForBackoff(delay, 0, r.Context().Done()); !delayElapsed { |
| return future.Response(), |
| autorest.NewErrorWithError(r.Context().Err(), "azure", "DoPollForAsynchronous", future.Response(), "context has been cancelled") |
| } |
| } |
| return future.Response(), err |
| }) |
| } |
| } |
| |
| // PollingMethodType defines a type used for enumerating polling mechanisms. |
| type PollingMethodType string |
| |
| const ( |
| // PollingAsyncOperation indicates the polling method uses the Azure-AsyncOperation header. |
| PollingAsyncOperation PollingMethodType = "AsyncOperation" |
| |
| // PollingLocation indicates the polling method uses the Location header. |
| PollingLocation PollingMethodType = "Location" |
| |
| // PollingRequestURI indicates the polling method uses the original request URI. |
| PollingRequestURI PollingMethodType = "RequestURI" |
| |
| // PollingUnknown indicates an unknown polling method and is the default value. |
| PollingUnknown PollingMethodType = "" |
| ) |
| |
| // AsyncOpIncompleteError is the type that's returned from a future that has not completed. |
| type AsyncOpIncompleteError struct { |
| // FutureType is the name of the type composed of a azure.Future. |
| FutureType string |
| } |
| |
| // Error returns an error message including the originating type name of the error. |
| func (e AsyncOpIncompleteError) Error() string { |
| return fmt.Sprintf("%s: asynchronous operation has not completed", e.FutureType) |
| } |
| |
| // NewAsyncOpIncompleteError creates a new AsyncOpIncompleteError with the specified parameters. |
| func NewAsyncOpIncompleteError(futureType string) AsyncOpIncompleteError { |
| return AsyncOpIncompleteError{ |
| FutureType: futureType, |
| } |
| } |