aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/github.com/Azure/go-autorest/autorest/azure/async.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/Azure/go-autorest/autorest/azure/async.go')
-rw-r--r--vendor/github.com/Azure/go-autorest/autorest/azure/async.go308
1 files changed, 308 insertions, 0 deletions
diff --git a/vendor/github.com/Azure/go-autorest/autorest/azure/async.go b/vendor/github.com/Azure/go-autorest/autorest/azure/async.go
new file mode 100644
index 000000000..6e076981f
--- /dev/null
+++ b/vendor/github.com/Azure/go-autorest/autorest/azure/async.go
@@ -0,0 +1,308 @@
+package azure
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/Azure/go-autorest/autorest"
+ "github.com/Azure/go-autorest/autorest/date"
+)
+
+const (
+ headerAsyncOperation = "Azure-AsyncOperation"
+)
+
+const (
+ methodDelete = "DELETE"
+ methodPatch = "PATCH"
+ methodPost = "POST"
+ methodPut = "PUT"
+ methodGet = "GET"
+
+ operationInProgress string = "InProgress"
+ operationCanceled string = "Canceled"
+ operationFailed string = "Failed"
+ operationSucceeded string = "Succeeded"
+)
+
+// 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 by
+// closing the optional channel on the http.Request.
+func DoPollForAsynchronous(delay time.Duration) autorest.SendDecorator {
+ return func(s autorest.Sender) autorest.Sender {
+ return autorest.SenderFunc(func(r *http.Request) (resp *http.Response, err error) {
+ resp, err = s.Do(r)
+ if err != nil {
+ return resp, err
+ }
+ pollingCodes := []int{http.StatusAccepted, http.StatusCreated, http.StatusOK}
+ if !autorest.ResponseHasStatusCode(resp, pollingCodes...) {
+ return resp, nil
+ }
+
+ ps := pollingState{}
+ for err == nil {
+ err = updatePollingState(resp, &ps)
+ if err != nil {
+ break
+ }
+ if ps.hasTerminated() {
+ if !ps.hasSucceeded() {
+ err = ps
+ }
+ break
+ }
+
+ r, err = newPollingRequest(resp, ps)
+ if err != nil {
+ return resp, err
+ }
+
+ delay = autorest.GetRetryAfter(resp, delay)
+ resp, err = autorest.SendWithSender(s, r,
+ autorest.AfterDelay(delay))
+ }
+
+ return resp, err
+ })
+ }
+}
+
+func getAsyncOperation(resp *http.Response) string {
+ return resp.Header.Get(http.CanonicalHeaderKey(headerAsyncOperation))
+}
+
+func hasSucceeded(state string) bool {
+ return state == operationSucceeded
+}
+
+func hasTerminated(state string) bool {
+ switch state {
+ case operationCanceled, operationFailed, operationSucceeded:
+ return true
+ default:
+ return false
+ }
+}
+
+func hasFailed(state string) bool {
+ return state == operationFailed
+}
+
+type provisioningTracker interface {
+ state() string
+ hasSucceeded() bool
+ hasTerminated() bool
+}
+
+type operationResource struct {
+ // Note:
+ // The specification states services should return the "id" field. However some return it as
+ // "operationId".
+ ID string `json:"id"`
+ OperationID string `json:"operationId"`
+ Name string `json:"name"`
+ Status string `json:"status"`
+ Properties map[string]interface{} `json:"properties"`
+ OperationError ServiceError `json:"error"`
+ StartTime date.Time `json:"startTime"`
+ EndTime date.Time `json:"endTime"`
+ PercentComplete float64 `json:"percentComplete"`
+}
+
+func (or operationResource) state() string {
+ return or.Status
+}
+
+func (or operationResource) hasSucceeded() bool {
+ return hasSucceeded(or.state())
+}
+
+func (or operationResource) hasTerminated() bool {
+ return hasTerminated(or.state())
+}
+
+type provisioningProperties struct {
+ ProvisioningState string `json:"provisioningState"`
+}
+
+type provisioningStatus struct {
+ Properties provisioningProperties `json:"properties,omitempty"`
+ ProvisioningError ServiceError `json:"error,omitempty"`
+}
+
+func (ps provisioningStatus) state() string {
+ return ps.Properties.ProvisioningState
+}
+
+func (ps provisioningStatus) hasSucceeded() bool {
+ return hasSucceeded(ps.state())
+}
+
+func (ps provisioningStatus) hasTerminated() bool {
+ return hasTerminated(ps.state())
+}
+
+func (ps provisioningStatus) hasProvisioningError() bool {
+ return ps.ProvisioningError != ServiceError{}
+}
+
+type pollingResponseFormat string
+
+const (
+ usesOperationResponse pollingResponseFormat = "OperationResponse"
+ usesProvisioningStatus pollingResponseFormat = "ProvisioningStatus"
+ formatIsUnknown pollingResponseFormat = ""
+)
+
+type pollingState struct {
+ responseFormat pollingResponseFormat
+ uri string
+ state string
+ code string
+ message string
+}
+
+func (ps pollingState) hasSucceeded() bool {
+ return hasSucceeded(ps.state)
+}
+
+func (ps pollingState) hasTerminated() bool {
+ return hasTerminated(ps.state)
+}
+
+func (ps pollingState) hasFailed() bool {
+ return hasFailed(ps.state)
+}
+
+func (ps pollingState) Error() string {
+ return fmt.Sprintf("Long running operation terminated with status '%s': Code=%q Message=%q", ps.state, ps.code, ps.message)
+}
+
+// updatePollingState maps the operation status -- retrieved from either a provisioningState
+// field, the status field of an OperationResource, or inferred from the HTTP status code --
+// into a well-known states. Since the process begins from the initial request, the state
+// always comes from either a the provisioningState returned or is inferred from the HTTP
+// status code. Subsequent requests will read an Azure OperationResource object if the
+// service initially returned the Azure-AsyncOperation header. The responseFormat field notes
+// the expected response format.
+func updatePollingState(resp *http.Response, ps *pollingState) error {
+ // Determine the response shape
+ // -- The first response will always be a provisioningStatus response; only the polling requests,
+ // depending on the header returned, may be something otherwise.
+ var pt provisioningTracker
+ if ps.responseFormat == usesOperationResponse {
+ pt = &operationResource{}
+ } else {
+ pt = &provisioningStatus{}
+ }
+
+ // If this is the first request (that is, the polling response shape is unknown), determine how
+ // to poll and what to expect
+ if ps.responseFormat == formatIsUnknown {
+ req := resp.Request
+ if req == nil {
+ return autorest.NewError("azure", "updatePollingState", "Azure Polling Error - Original HTTP request is missing")
+ }
+
+ // Prefer the Azure-AsyncOperation header
+ ps.uri = getAsyncOperation(resp)
+ if ps.uri != "" {
+ ps.responseFormat = usesOperationResponse
+ } else {
+ ps.responseFormat = usesProvisioningStatus
+ }
+
+ // Else, use the Location header
+ if ps.uri == "" {
+ ps.uri = autorest.GetLocation(resp)
+ }
+
+ // Lastly, requests against an existing resource, use the last request URI
+ if ps.uri == "" {
+ m := strings.ToUpper(req.Method)
+ if m == methodPatch || m == methodPut || m == methodGet {
+ ps.uri = req.URL.String()
+ }
+ }
+ }
+
+ // Read and interpret the response (saving the Body in case no polling is necessary)
+ b := &bytes.Buffer{}
+ err := autorest.Respond(resp,
+ autorest.ByCopying(b),
+ autorest.ByUnmarshallingJSON(pt),
+ autorest.ByClosing())
+ resp.Body = ioutil.NopCloser(b)
+ if err != nil {
+ return err
+ }
+
+ // Interpret the results
+ // -- Terminal states apply regardless
+ // -- Unknown states are per-service inprogress states
+ // -- Otherwise, infer state from HTTP status code
+ if pt.hasTerminated() {
+ ps.state = pt.state()
+ } else if pt.state() != "" {
+ ps.state = operationInProgress
+ } else {
+ switch resp.StatusCode {
+ case http.StatusAccepted:
+ ps.state = operationInProgress
+
+ case http.StatusNoContent, http.StatusCreated, http.StatusOK:
+ ps.state = operationSucceeded
+
+ default:
+ ps.state = operationFailed
+ }
+ }
+
+ if ps.state == operationInProgress && ps.uri == "" {
+ return autorest.NewError("azure", "updatePollingState", "Azure Polling Error - Unable to obtain polling URI for %s %s", resp.Request.Method, resp.Request.URL)
+ }
+
+ // For failed operation, check for error code and message in
+ // -- Operation resource
+ // -- Response
+ // -- Otherwise, Unknown
+ if ps.hasFailed() {
+ if ps.responseFormat == usesOperationResponse {
+ or := pt.(*operationResource)
+ ps.code = or.OperationError.Code
+ ps.message = or.OperationError.Message
+ } else {
+ p := pt.(*provisioningStatus)
+ if p.hasProvisioningError() {
+ ps.code = p.ProvisioningError.Code
+ ps.message = p.ProvisioningError.Message
+ } else {
+ ps.code = "Unknown"
+ ps.message = "None"
+ }
+ }
+ }
+ return nil
+}
+
+func newPollingRequest(resp *http.Response, ps pollingState) (*http.Request, error) {
+ req := resp.Request
+ if req == nil {
+ return nil, autorest.NewError("azure", "newPollingRequest", "Azure Polling Error - Original HTTP request is missing")
+ }
+
+ reqPoll, err := autorest.Prepare(&http.Request{Cancel: req.Cancel},
+ autorest.AsGet(),
+ autorest.WithBaseURL(ps.uri))
+ if err != nil {
+ return nil, autorest.NewErrorWithError(err, "azure", "newPollingRequest", nil, "Failure creating poll request to %s", ps.uri)
+ }
+
+ return reqPoll, nil
+}