2017-08-05 20:30:20 +02:00
package azure
2017-10-22 10:07:36 +02:00
// 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.
2017-08-05 20:30:20 +02:00
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
}