mirror of https://github.com/restic/restic.git
317 lines
9.0 KiB
Go
317 lines
9.0 KiB
Go
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"
|
|
"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 (
|
|
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 == http.MethodPatch || m == http.MethodPut || m == http.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
|
|
}
|