vendor: update all dependencies to latest versions

This commit is contained in:
Nick Craig-Wood
2018-01-16 13:20:59 +00:00
parent 8e83fb6fb9
commit 7d3a17725d
4878 changed files with 1974213 additions and 201199 deletions
+214 -56
View File
@@ -1,7 +1,23 @@
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"
@@ -23,6 +39,152 @@ const (
operationSucceeded string = "Succeeded"
)
var pollingCodes = [...]int{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
resp *http.Response
ps pollingState
}
// NewFuture returns a new Future object initialized with the specified request.
func NewFuture(req *http.Request) Future {
return Future{req: req}
}
// Response returns the last HTTP response or nil if there isn't one.
func (f Future) Response() *http.Response {
return f.resp
}
// Status returns the last status message of the operation.
func (f Future) Status() string {
if f.ps.State == "" {
return "Unknown"
}
return f.ps.State
}
// PollingMethod returns the method used to monitor the status of the asynchronous operation.
func (f Future) PollingMethod() PollingMethodType {
return f.ps.PollingMethod
}
// Done queries the service to see if the operation has completed.
func (f *Future) Done(sender autorest.Sender) (bool, error) {
// exit early if this future has terminated
if f.ps.hasTerminated() {
return true, f.errorInfo()
}
resp, err := sender.Do(f.req)
f.resp = resp
if err != nil || !autorest.ResponseHasStatusCode(resp, pollingCodes[:]...) {
return false, err
}
err = updatePollingState(resp, &f.ps)
if err != nil {
return false, err
}
if f.ps.hasTerminated() {
return true, f.errorInfo()
}
f.req, err = newPollingRequest(f.ps)
return false, err
}
// 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.resp == nil {
return 0, false
}
retry := f.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.
func (f Future) WaitForCompletion(ctx context.Context, client autorest.Client) error {
ctx, cancel := context.WithTimeout(ctx, client.PollingDuration)
defer cancel()
done, err := f.Done(client)
for attempts := 0; !done; done, err = f.Done(client) {
if attempts >= client.RetryAttempts {
return autorest.NewErrorWithError(err, "azure", "WaitForCompletion", f.resp, "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, ctx.Done())
if !delayElapsed {
return autorest.NewErrorWithError(ctx.Err(), "azure", "WaitForCompletion", f.resp, "context has been cancelled")
}
}
return err
}
// if the operation failed the polling state will contain
// error information and implements the error interface
func (f *Future) errorInfo() error {
if !f.ps.hasSucceeded() {
return f.ps
}
return nil
}
// MarshalJSON implements the json.Marshaler interface.
func (f Future) MarshalJSON() ([]byte, error) {
return json.Marshal(&f.ps)
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (f *Future) UnmarshalJSON(data []byte) error {
err := json.Unmarshal(data, &f.ps)
if err != nil {
return err
}
f.req, err = newPollingRequest(f.ps)
return err
}
// 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
@@ -34,8 +196,7 @@ func DoPollForAsynchronous(delay time.Duration) autorest.SendDecorator {
if err != nil {
return resp, err
}
pollingCodes := []int{http.StatusAccepted, http.StatusCreated, http.StatusOK}
if !autorest.ResponseHasStatusCode(resp, pollingCodes...) {
if !autorest.ResponseHasStatusCode(resp, pollingCodes[:]...) {
return resp, nil
}
@@ -52,10 +213,11 @@ func DoPollForAsynchronous(delay time.Duration) autorest.SendDecorator {
break
}
r, err = newPollingRequest(resp, ps)
r, err = newPollingRequest(ps)
if err != nil {
return resp, err
}
r.Cancel = resp.Request.Cancel
delay = autorest.GetRetryAfter(resp, delay)
resp, err = autorest.SendWithSender(s, r,
@@ -72,20 +234,15 @@ func getAsyncOperation(resp *http.Response) string {
}
func hasSucceeded(state string) bool {
return state == operationSucceeded
return strings.EqualFold(state, operationSucceeded)
}
func hasTerminated(state string) bool {
switch state {
case operationCanceled, operationFailed, operationSucceeded:
return true
default:
return false
}
return strings.EqualFold(state, operationCanceled) || strings.EqualFold(state, operationFailed) || strings.EqualFold(state, operationSucceeded)
}
func hasFailed(state string) bool {
return state == operationFailed
return strings.EqualFold(state, operationFailed)
}
type provisioningTracker interface {
@@ -146,36 +303,42 @@ func (ps provisioningStatus) hasProvisioningError() bool {
return ps.ProvisioningError != ServiceError{}
}
type pollingResponseFormat string
// PollingMethodType defines a type used for enumerating polling mechanisms.
type PollingMethodType string
const (
usesOperationResponse pollingResponseFormat = "OperationResponse"
usesProvisioningStatus pollingResponseFormat = "ProvisioningStatus"
formatIsUnknown pollingResponseFormat = ""
// 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"
// PollingUnknown indicates an unknown polling method and is the default value.
PollingUnknown PollingMethodType = ""
)
type pollingState struct {
responseFormat pollingResponseFormat
uri string
state string
code string
message string
PollingMethod PollingMethodType `json:"pollingMethod"`
URI string `json:"uri"`
State string `json:"state"`
Code string `json:"code"`
Message string `json:"message"`
}
func (ps pollingState) hasSucceeded() bool {
return hasSucceeded(ps.state)
return hasSucceeded(ps.State)
}
func (ps pollingState) hasTerminated() bool {
return hasTerminated(ps.state)
return hasTerminated(ps.State)
}
func (ps pollingState) hasFailed() bool {
return hasFailed(ps.state)
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)
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
@@ -190,7 +353,7 @@ func updatePollingState(resp *http.Response, ps *pollingState) error {
// -- 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 {
if ps.PollingMethod == PollingAsyncOperation {
pt = &operationResource{}
} else {
pt = &provisioningStatus{}
@@ -198,30 +361,30 @@ func updatePollingState(resp *http.Response, ps *pollingState) error {
// 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 {
if ps.PollingMethod == PollingUnknown {
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
ps.URI = getAsyncOperation(resp)
if ps.URI != "" {
ps.PollingMethod = PollingAsyncOperation
} else {
ps.responseFormat = usesProvisioningStatus
ps.PollingMethod = PollingLocation
}
// Else, use the Location header
if ps.uri == "" {
ps.uri = autorest.GetLocation(resp)
if ps.URI == "" {
ps.URI = autorest.GetLocation(resp)
}
// Lastly, requests against an existing resource, use the last request URI
if ps.uri == "" {
if ps.URI == "" {
m := strings.ToUpper(req.Method)
if m == http.MethodPatch || m == http.MethodPut || m == http.MethodGet {
ps.uri = req.URL.String()
ps.URI = req.URL.String()
}
}
}
@@ -242,23 +405,23 @@ func updatePollingState(resp *http.Response, ps *pollingState) error {
// -- Unknown states are per-service inprogress states
// -- Otherwise, infer state from HTTP status code
if pt.hasTerminated() {
ps.state = pt.state()
ps.State = pt.state()
} else if pt.state() != "" {
ps.state = operationInProgress
ps.State = operationInProgress
} else {
switch resp.StatusCode {
case http.StatusAccepted:
ps.state = operationInProgress
ps.State = operationInProgress
case http.StatusNoContent, http.StatusCreated, http.StatusOK:
ps.state = operationSucceeded
ps.State = operationSucceeded
default:
ps.state = operationFailed
ps.State = operationFailed
}
}
if ps.state == operationInProgress && ps.uri == "" {
if strings.EqualFold(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)
}
@@ -267,35 +430,30 @@ func updatePollingState(resp *http.Response, ps *pollingState) error {
// -- Response
// -- Otherwise, Unknown
if ps.hasFailed() {
if ps.responseFormat == usesOperationResponse {
if ps.PollingMethod == PollingAsyncOperation {
or := pt.(*operationResource)
ps.code = or.OperationError.Code
ps.message = or.OperationError.Message
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
ps.Code = p.ProvisioningError.Code
ps.Message = p.ProvisioningError.Message
} else {
ps.code = "Unknown"
ps.message = "None"
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},
func newPollingRequest(ps pollingState) (*http.Request, error) {
reqPoll, err := autorest.Prepare(&http.Request{},
autorest.AsGet(),
autorest.WithBaseURL(ps.uri))
autorest.WithBaseURL(ps.URI))
if err != nil {
return nil, autorest.NewErrorWithError(err, "azure", "newPollingRequest", nil, "Failure creating poll request to %s", ps.uri)
return nil, autorest.NewErrorWithError(err, "azure", "newPollingRequest", nil, "Failure creating poll request to %s", ps.URI)
}
return reqPoll, nil
+259 -48
View File
@@ -1,6 +1,23 @@
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 (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
@@ -131,27 +148,27 @@ func TestProvisioningStatus_HasTerminatedReturnsFalseForUnknownStates(t *testing
}
func TestPollingState_HasSucceededReturnsFalseIfNotSuccess(t *testing.T) {
if (pollingState{state: "not a success string"}).hasSucceeded() {
if (pollingState{State: "not a success string"}).hasSucceeded() {
t.Fatalf("azure: pollingState#hasSucceeded failed to return false for a canceled operation")
}
}
func TestPollingState_HasSucceededReturnsTrueIfSuccessful(t *testing.T) {
if !(pollingState{state: operationSucceeded}).hasSucceeded() {
if !(pollingState{State: operationSucceeded}).hasSucceeded() {
t.Fatalf("azure: pollingState#hasSucceeded failed to return true for a successful operation")
}
}
func TestPollingState_HasTerminatedReturnsTrueForKnownStates(t *testing.T) {
for _, state := range []string{operationSucceeded, operationCanceled, operationFailed} {
if !(pollingState{state: state}).hasTerminated() {
if !(pollingState{State: state}).hasTerminated() {
t.Fatalf("azure: pollingState#hasTerminated failed to return true for the '%s' state", state)
}
}
}
func TestPollingState_HasTerminatedReturnsFalseForUnknownStates(t *testing.T) {
if (pollingState{state: "not a known state"}).hasTerminated() {
if (pollingState{State: "not a known state"}).hasTerminated() {
t.Fatalf("azure: pollingState#hasTerminated returned true for a non-terminal operation")
}
}
@@ -168,7 +185,7 @@ func TestUpdatePollingState_ReturnsTerminatedForKnownProvisioningStates(t *testi
for _, state := range []string{operationSucceeded, operationCanceled, operationFailed} {
resp := mocks.NewResponseWithContent(fmt.Sprintf(pollingStateFormat, state))
resp.StatusCode = 42
ps := &pollingState{responseFormat: usesProvisioningStatus}
ps := &pollingState{PollingMethod: PollingLocation}
updatePollingState(resp, ps)
if !ps.hasTerminated() {
t.Fatalf("azure: updatePollingState failed to return a terminating pollingState for the '%s' state", state)
@@ -179,7 +196,7 @@ func TestUpdatePollingState_ReturnsTerminatedForKnownProvisioningStates(t *testi
func TestUpdatePollingState_ReturnsSuccessForSuccessfulProvisioningState(t *testing.T) {
resp := mocks.NewResponseWithContent(fmt.Sprintf(pollingStateFormat, operationSucceeded))
resp.StatusCode = 42
ps := &pollingState{responseFormat: usesProvisioningStatus}
ps := &pollingState{PollingMethod: PollingLocation}
updatePollingState(resp, ps)
if !ps.hasSucceeded() {
t.Fatalf("azure: updatePollingState failed to return a successful pollingState for the '%s' state", operationSucceeded)
@@ -190,7 +207,7 @@ func TestUpdatePollingState_ReturnsInProgressForAllOtherProvisioningStates(t *te
s := "not a recognized state"
resp := mocks.NewResponseWithContent(fmt.Sprintf(pollingStateFormat, s))
resp.StatusCode = 42
ps := &pollingState{responseFormat: usesProvisioningStatus}
ps := &pollingState{PollingMethod: PollingLocation}
updatePollingState(resp, ps)
if ps.hasTerminated() {
t.Fatalf("azure: updatePollingState returned terminated for unknown state '%s'", s)
@@ -201,7 +218,7 @@ func TestUpdatePollingState_ReturnsSuccessWhenProvisioningStateFieldIsAbsentForS
for _, sc := range []int{http.StatusOK, http.StatusCreated, http.StatusNoContent} {
resp := mocks.NewResponseWithContent(pollingStateEmpty)
resp.StatusCode = sc
ps := &pollingState{responseFormat: usesProvisioningStatus}
ps := &pollingState{PollingMethod: PollingLocation}
updatePollingState(resp, ps)
if !ps.hasSucceeded() {
t.Fatalf("azure: updatePollingState failed to return success when the provisionState field is absent for Status Code %d", sc)
@@ -212,7 +229,7 @@ func TestUpdatePollingState_ReturnsSuccessWhenProvisioningStateFieldIsAbsentForS
func TestUpdatePollingState_ReturnsInProgressWhenProvisioningStateFieldIsAbsentForAccepted(t *testing.T) {
resp := mocks.NewResponseWithContent(pollingStateEmpty)
resp.StatusCode = http.StatusAccepted
ps := &pollingState{responseFormat: usesProvisioningStatus}
ps := &pollingState{PollingMethod: PollingLocation}
updatePollingState(resp, ps)
if ps.hasTerminated() {
t.Fatalf("azure: updatePollingState returned terminated when the provisionState field is absent for Status Code Accepted")
@@ -222,7 +239,7 @@ func TestUpdatePollingState_ReturnsInProgressWhenProvisioningStateFieldIsAbsentF
func TestUpdatePollingState_ReturnsFailedWhenProvisioningStateFieldIsAbsentForUnknownStatusCodes(t *testing.T) {
resp := mocks.NewResponseWithContent(pollingStateEmpty)
resp.StatusCode = 42
ps := &pollingState{responseFormat: usesProvisioningStatus}
ps := &pollingState{PollingMethod: PollingLocation}
updatePollingState(resp, ps)
if !ps.hasTerminated() || ps.hasSucceeded() {
t.Fatalf("azure: updatePollingState did not return failed when the provisionState field is absent for an unknown Status Code")
@@ -233,7 +250,7 @@ func TestUpdatePollingState_ReturnsTerminatedForKnownOperationResourceStates(t *
for _, state := range []string{operationSucceeded, operationCanceled, operationFailed} {
resp := mocks.NewResponseWithContent(fmt.Sprintf(operationResourceFormat, state))
resp.StatusCode = 42
ps := &pollingState{responseFormat: usesOperationResponse}
ps := &pollingState{PollingMethod: PollingAsyncOperation}
updatePollingState(resp, ps)
if !ps.hasTerminated() {
t.Fatalf("azure: updatePollingState failed to return a terminating pollingState for the '%s' state", state)
@@ -244,7 +261,7 @@ func TestUpdatePollingState_ReturnsTerminatedForKnownOperationResourceStates(t *
func TestUpdatePollingState_ReturnsSuccessForSuccessfulOperationResourceState(t *testing.T) {
resp := mocks.NewResponseWithContent(fmt.Sprintf(operationResourceFormat, operationSucceeded))
resp.StatusCode = 42
ps := &pollingState{responseFormat: usesOperationResponse}
ps := &pollingState{PollingMethod: PollingAsyncOperation}
updatePollingState(resp, ps)
if !ps.hasSucceeded() {
t.Fatalf("azure: updatePollingState failed to return a successful pollingState for the '%s' state", operationSucceeded)
@@ -255,7 +272,7 @@ func TestUpdatePollingState_ReturnsInProgressForAllOtherOperationResourceStates(
s := "not a recognized state"
resp := mocks.NewResponseWithContent(fmt.Sprintf(operationResourceFormat, s))
resp.StatusCode = 42
ps := &pollingState{responseFormat: usesOperationResponse}
ps := &pollingState{PollingMethod: PollingAsyncOperation}
updatePollingState(resp, ps)
if ps.hasTerminated() {
t.Fatalf("azure: updatePollingState returned terminated for unknown state '%s'", s)
@@ -266,7 +283,7 @@ func TestUpdatePollingState_CopiesTheResponseBody(t *testing.T) {
s := fmt.Sprintf(pollingStateFormat, operationSucceeded)
resp := mocks.NewResponseWithContent(s)
resp.StatusCode = 42
ps := &pollingState{responseFormat: usesOperationResponse}
ps := &pollingState{PollingMethod: PollingAsyncOperation}
updatePollingState(resp, ps)
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
@@ -280,7 +297,7 @@ func TestUpdatePollingState_CopiesTheResponseBody(t *testing.T) {
func TestUpdatePollingState_ClosesTheOriginalResponseBody(t *testing.T) {
resp := mocks.NewResponse()
b := resp.Body.(*mocks.Body)
ps := &pollingState{responseFormat: usesProvisioningStatus}
ps := &pollingState{PollingMethod: PollingLocation}
updatePollingState(resp, ps)
if b.IsOpen() {
t.Fatal("azure: updatePollingState failed to close the original http.Response Body")
@@ -298,23 +315,23 @@ func TestUpdatePollingState_FailsWhenResponseLacksRequest(t *testing.T) {
}
}
func TestUpdatePollingState_SetsTheResponseFormatWhenUsingTheAzureAsyncOperationHeader(t *testing.T) {
func TestUpdatePollingState_SetsThePollingMethodWhenUsingTheAzureAsyncOperationHeader(t *testing.T) {
ps := pollingState{}
updatePollingState(newAsynchronousResponse(), &ps)
if ps.responseFormat != usesOperationResponse {
if ps.PollingMethod != PollingAsyncOperation {
t.Fatal("azure: updatePollingState failed to set the correct response format when using the Azure-AsyncOperation header")
}
}
func TestUpdatePollingState_SetsTheResponseFormatWhenUsingTheAzureAsyncOperationHeaderIsMissing(t *testing.T) {
func TestUpdatePollingState_SetsThePollingMethodWhenUsingTheAzureAsyncOperationHeaderIsMissing(t *testing.T) {
resp := newAsynchronousResponse()
resp.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
ps := pollingState{}
updatePollingState(resp, &ps)
if ps.responseFormat != usesProvisioningStatus {
if ps.PollingMethod != PollingLocation {
t.Fatal("azure: updatePollingState failed to set the correct response format when the Azure-AsyncOperation header is absent")
}
}
@@ -323,10 +340,10 @@ func TestUpdatePollingState_DoesNotChangeAnExistingReponseFormat(t *testing.T) {
resp := newAsynchronousResponse()
resp.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
ps := pollingState{responseFormat: usesOperationResponse}
ps := pollingState{PollingMethod: PollingAsyncOperation}
updatePollingState(resp, &ps)
if ps.responseFormat != usesOperationResponse {
if ps.PollingMethod != PollingAsyncOperation {
t.Fatal("azure: updatePollingState failed to leave an existing response format setting")
}
}
@@ -337,7 +354,7 @@ func TestUpdatePollingState_PrefersTheAzureAsyncOperationHeader(t *testing.T) {
ps := pollingState{}
updatePollingState(resp, &ps)
if ps.uri != mocks.TestAzureAsyncURL {
if ps.URI != mocks.TestAzureAsyncURL {
t.Fatal("azure: updatePollingState failed to prefer the Azure-AsyncOperation header")
}
}
@@ -349,7 +366,7 @@ func TestUpdatePollingState_PrefersLocationWhenTheAzureAsyncOperationHeaderMissi
ps := pollingState{}
updatePollingState(resp, &ps)
if ps.uri != mocks.TestLocationURL {
if ps.URI != mocks.TestLocationURL {
t.Fatal("azure: updatePollingState failed to prefer the Location header when the Azure-AsyncOperation header is missing")
}
}
@@ -363,7 +380,7 @@ func TestUpdatePollingState_UsesTheObjectLocationIfAsyncHeadersAreMissing(t *tes
ps := pollingState{}
updatePollingState(resp, &ps)
if ps.uri != mocks.TestURL {
if ps.URI != mocks.TestURL {
t.Fatal("azure: updatePollingState failed to use the Object URL when the asynchronous headers are missing")
}
}
@@ -378,7 +395,7 @@ func TestUpdatePollingState_RecognizesLowerCaseHTTPVerbs(t *testing.T) {
ps := pollingState{}
updatePollingState(resp, &ps)
if ps.uri != mocks.TestURL {
if ps.URI != mocks.TestURL {
t.Fatalf("azure: updatePollingState failed to recognize the lower-case HTTP verb '%s'", m)
}
}
@@ -398,32 +415,22 @@ func TestUpdatePollingState_ReturnsAnErrorIfAsyncHeadersAreMissingForANewOrDelet
}
}
func TestNewPollingRequest_FailsWhenResponseLacksRequest(t *testing.T) {
resp := newAsynchronousResponse()
resp.Request = nil
_, err := newPollingRequest(resp, pollingState{})
if err == nil {
t.Fatal("azure: newPollingRequest failed to return an error when the http.Response lacked the original http.Request")
}
}
func TestNewPollingRequest_ReturnsAnErrorWhenPrepareFails(t *testing.T) {
_, err := newPollingRequest(newAsynchronousResponse(), pollingState{responseFormat: usesOperationResponse, uri: mocks.TestBadURL})
_, err := newPollingRequest(pollingState{PollingMethod: PollingAsyncOperation, URI: mocks.TestBadURL})
if err == nil {
t.Fatal("azure: newPollingRequest failed to return an error when Prepare fails")
}
}
func TestNewPollingRequest_DoesNotReturnARequestWhenPrepareFails(t *testing.T) {
req, _ := newPollingRequest(newAsynchronousResponse(), pollingState{responseFormat: usesOperationResponse, uri: mocks.TestBadURL})
req, _ := newPollingRequest(pollingState{PollingMethod: PollingAsyncOperation, URI: mocks.TestBadURL})
if req != nil {
t.Fatal("azure: newPollingRequest returned an http.Request when Prepare failed")
}
}
func TestNewPollingRequest_ReturnsAGetRequest(t *testing.T) {
req, _ := newPollingRequest(newAsynchronousResponse(), pollingState{responseFormat: usesOperationResponse, uri: mocks.TestAzureAsyncURL})
req, _ := newPollingRequest(pollingState{PollingMethod: PollingAsyncOperation, URI: mocks.TestAzureAsyncURL})
if req.Method != "GET" {
t.Fatalf("azure: newPollingRequest did not create an HTTP GET request -- actual method %v", req.Method)
}
@@ -629,7 +636,7 @@ func TestDoPollForAsynchronous_PollsForStatusAccepted(t *testing.T) {
r, _ := autorest.SendWithSender(client, mocks.NewRequest(),
DoPollForAsynchronous(time.Millisecond))
if client.Attempts() < 4 {
if client.Attempts() < client.NumResponses() {
t.Fatalf("azure: DoPollForAsynchronous stopped polling before receiving a terminated OperationResource")
}
@@ -652,7 +659,7 @@ func TestDoPollForAsynchronous_PollsForStatusCreated(t *testing.T) {
r, _ := autorest.SendWithSender(client, mocks.NewRequest(),
DoPollForAsynchronous(time.Millisecond))
if client.Attempts() < 4 {
if client.Attempts() < client.NumResponses() {
t.Fatalf("azure: DoPollForAsynchronous stopped polling before receiving a terminated OperationResource")
}
@@ -676,7 +683,7 @@ func TestDoPollForAsynchronous_PollsUntilProvisioningStatusTerminates(t *testing
r, _ := autorest.SendWithSender(client, mocks.NewRequest(),
DoPollForAsynchronous(time.Millisecond))
if client.Attempts() < 4 {
if client.Attempts() < client.NumResponses() {
t.Fatalf("azure: DoPollForAsynchronous stopped polling before receiving a terminated OperationResource")
}
@@ -700,7 +707,7 @@ func TestDoPollForAsynchronous_PollsUntilProvisioningStatusSucceeds(t *testing.T
r, _ := autorest.SendWithSender(client, mocks.NewRequest(),
DoPollForAsynchronous(time.Millisecond))
if client.Attempts() < 4 {
if client.Attempts() < client.NumResponses() {
t.Fatalf("azure: DoPollForAsynchronous stopped polling before receiving a terminated OperationResource")
}
@@ -721,7 +728,7 @@ func TestDoPollForAsynchronous_PollsUntilOperationResourceHasTerminated(t *testi
r, _ := autorest.SendWithSender(client, mocks.NewRequest(),
DoPollForAsynchronous(time.Millisecond))
if client.Attempts() < 4 {
if client.Attempts() < client.NumResponses() {
t.Fatalf("azure: DoPollForAsynchronous stopped polling before receiving a terminated OperationResource")
}
@@ -742,7 +749,7 @@ func TestDoPollForAsynchronous_PollsUntilOperationResourceHasSucceeded(t *testin
r, _ := autorest.SendWithSender(client, mocks.NewRequest(),
DoPollForAsynchronous(time.Millisecond))
if client.Attempts() < 4 {
if client.Attempts() < client.NumResponses() {
t.Fatalf("azure: DoPollForAsynchronous stopped polling before receiving a terminated OperationResource")
}
@@ -871,7 +878,7 @@ func TestDoPollForAsynchronous_ReturnsErrorForLastErrorResponse(t *testing.T) {
r1.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
r2 := newProvisioningStatusResponse("busy")
r2.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
r3 := newAsynchronousResponseWithError()
r3 := newAsynchronousResponseWithError("400 Bad Request", http.StatusBadRequest)
r3.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
client := mocks.NewSender()
@@ -918,7 +925,7 @@ func TestDoPollForAsynchronous_ReturnsOperationResourceErrorForFailedOperations(
func TestDoPollForAsynchronous_ReturnsErrorForFirstPutRequest(t *testing.T) {
// Return 400 bad response with error code and message in first put
r1 := newAsynchronousResponseWithError()
r1 := newAsynchronousResponseWithError("400 Bad Request", http.StatusBadRequest)
client := mocks.NewSender()
client.AppendResponse(r1)
@@ -1010,6 +1017,210 @@ func TestDoPollForAsynchronous_StopsPollingIfItReceivesAnInvalidOperationResourc
autorest.ByClosing())
}
func TestFuture_PollsUntilProvisioningStatusSucceeds(t *testing.T) {
r1 := newAsynchronousResponse()
r1.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
r2 := newProvisioningStatusResponse("busy")
r2.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
r3 := newProvisioningStatusResponse(operationSucceeded)
r3.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
client := mocks.NewSender()
client.AppendResponse(r1)
client.AppendAndRepeatResponse(r2, 2)
client.AppendResponse(r3)
future := NewFuture(mocks.NewRequest())
for done, err := future.Done(client); !done; done, err = future.Done(client) {
if future.PollingMethod() != PollingLocation {
t.Fatalf("azure: wrong future polling method")
}
if err != nil {
t.Fatalf("azure: TestFuture polling Done failed")
}
delay, ok := future.GetPollingDelay()
if !ok {
t.Fatalf("expected Retry-After value")
}
time.Sleep(delay)
}
if client.Attempts() < client.NumResponses() {
t.Fatalf("azure: TestFuture stopped polling before receiving a terminated OperationResource")
}
autorest.Respond(future.Response(),
autorest.ByClosing())
}
func TestFuture_Marshalling(t *testing.T) {
client := mocks.NewSender()
client.AppendResponse(newAsynchronousResponse())
future := NewFuture(mocks.NewRequest())
done, err := future.Done(client)
if err != nil {
t.Fatalf("azure: TestFuture marshalling Done failed")
}
if done {
t.Fatalf("azure: TestFuture marshalling shouldn't be done")
}
if future.PollingMethod() != PollingAsyncOperation {
t.Fatalf("azure: wrong future polling method")
}
data, err := json.Marshal(future)
if err != nil {
t.Fatalf("azure: TestFuture failed to marshal")
}
var future2 Future
err = json.Unmarshal(data, &future2)
if err != nil {
t.Fatalf("azure: TestFuture failed to unmarshal")
}
if future.ps.Code != future2.ps.Code {
t.Fatalf("azure: TestFuture marshalling codes don't match")
}
if future.ps.Message != future2.ps.Message {
t.Fatalf("azure: TestFuture marshalling messages don't match")
}
if future.ps.PollingMethod != future2.ps.PollingMethod {
t.Fatalf("azure: TestFuture marshalling response formats don't match")
}
if future.ps.State != future2.ps.State {
t.Fatalf("azure: TestFuture marshalling states don't match")
}
if future.ps.URI != future2.ps.URI {
t.Fatalf("azure: TestFuture marshalling URIs don't match")
}
}
func TestFuture_WaitForCompletion(t *testing.T) {
r1 := newAsynchronousResponse()
r1.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
r2 := newProvisioningStatusResponse("busy")
r2.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
r3 := newAsynchronousResponseWithError("Internal server error", http.StatusInternalServerError)
r3.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
r3.Header.Del(http.CanonicalHeaderKey("Retry-After"))
r4 := newProvisioningStatusResponse(operationSucceeded)
r4.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
sender := mocks.NewSender()
sender.AppendResponse(r1)
sender.AppendError(errors.New("transient network failure"))
sender.AppendAndRepeatResponse(r2, 2)
sender.AppendResponse(r3)
sender.AppendResponse(r4)
future := NewFuture(mocks.NewRequest())
client := autorest.Client{
PollingDelay: 1 * time.Second,
PollingDuration: autorest.DefaultPollingDuration,
RetryAttempts: autorest.DefaultRetryAttempts,
RetryDuration: 1 * time.Second,
Sender: sender,
}
err := future.WaitForCompletion(context.Background(), client)
if err != nil {
t.Fatalf("azure: WaitForCompletion returned non-nil error")
}
if sender.Attempts() < sender.NumResponses() {
t.Fatalf("azure: TestFuture stopped polling before receiving a terminated OperationResource")
}
autorest.Respond(future.Response(),
autorest.ByClosing())
}
func TestFuture_WaitForCompletionTimedOut(t *testing.T) {
r1 := newAsynchronousResponse()
r1.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
r2 := newProvisioningStatusResponse("busy")
r2.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
sender := mocks.NewSender()
sender.AppendResponse(r1)
sender.AppendAndRepeatResponseWithDelay(r2, 1*time.Second, 5)
future := NewFuture(mocks.NewRequest())
client := autorest.Client{
PollingDelay: autorest.DefaultPollingDelay,
PollingDuration: 2 * time.Second,
RetryAttempts: autorest.DefaultRetryAttempts,
RetryDuration: 1 * time.Second,
Sender: sender,
}
err := future.WaitForCompletion(context.Background(), client)
if err == nil {
t.Fatalf("azure: WaitForCompletion returned nil error, should have timed out")
}
}
func TestFuture_WaitForCompletionRetriesExceeded(t *testing.T) {
r1 := newAsynchronousResponse()
r1.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
sender := mocks.NewSender()
sender.AppendResponse(r1)
sender.AppendAndRepeatError(errors.New("transient network failure"), autorest.DefaultRetryAttempts+1)
future := NewFuture(mocks.NewRequest())
client := autorest.Client{
PollingDelay: autorest.DefaultPollingDelay,
PollingDuration: autorest.DefaultPollingDuration,
RetryAttempts: autorest.DefaultRetryAttempts,
RetryDuration: 100 * time.Millisecond,
Sender: sender,
}
err := future.WaitForCompletion(context.Background(), client)
if err == nil {
t.Fatalf("azure: WaitForCompletion returned nil error, should have errored out")
}
}
func TestFuture_WaitForCompletionCancelled(t *testing.T) {
r1 := newAsynchronousResponse()
r1.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
r2 := newProvisioningStatusResponse("busy")
r2.Header.Del(http.CanonicalHeaderKey(headerAsyncOperation))
sender := mocks.NewSender()
sender.AppendResponse(r1)
sender.AppendAndRepeatResponseWithDelay(r2, 1*time.Second, 5)
future := NewFuture(mocks.NewRequest())
client := autorest.Client{
PollingDelay: autorest.DefaultPollingDelay,
PollingDuration: autorest.DefaultPollingDuration,
RetryAttempts: autorest.DefaultRetryAttempts,
RetryDuration: autorest.DefaultRetryDuration,
Sender: sender,
}
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(2 * time.Second)
cancel()
}()
err := future.WaitForCompletion(ctx, client)
if err == nil {
t.Fatalf("azure: WaitForCompletion returned nil error, should have been cancelled")
}
}
const (
operationResourceIllegal = `
This is not JSON and should fail...badly.
@@ -1085,8 +1296,8 @@ func newAsynchronousResponse() *http.Response {
return r
}
func newAsynchronousResponseWithError() *http.Response {
r := mocks.NewResponseWithStatus("400 Bad Request", http.StatusBadRequest)
func newAsynchronousResponseWithError(response string, status int) *http.Response {
r := mocks.NewResponseWithStatus(response, status)
mocks.SetRetryHeader(r, retryDelay)
r.Request = mocks.NewRequestForURL(mocks.TestURL)
r.Body = mocks.NewBody(errorResponse)
+143
View File
@@ -0,0 +1,143 @@
// 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.
package auth
import (
"bytes"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
"unicode/utf16"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/dimchansky/utfbom"
)
// ClientSetup includes authentication details and cloud specific
// parameters for ARM clients
type ClientSetup struct {
*autorest.BearerAuthorizer
File
BaseURI string
}
// File represents the authentication file
type File struct {
ClientID string `json:"clientId,omitempty"`
ClientSecret string `json:"clientSecret,omitempty"`
SubscriptionID string `json:"subscriptionId,omitempty"`
TenantID string `json:"tenantId,omitempty"`
ActiveDirectoryEndpoint string `json:"activeDirectoryEndpointUrl,omitempty"`
ResourceManagerEndpoint string `json:"resourceManagerEndpointUrl,omitempty"`
GraphResourceID string `json:"activeDirectoryGraphResourceId,omitempty"`
SQLManagementEndpoint string `json:"sqlManagementEndpointUrl,omitempty"`
GalleryEndpoint string `json:"galleryEndpointUrl,omitempty"`
ManagementEndpoint string `json:"managementEndpointUrl,omitempty"`
}
// GetClientSetup provides an authorizer, base URI, subscriptionID and
// tenantID parameters from an Azure CLI auth file
func GetClientSetup(baseURI string) (auth ClientSetup, err error) {
fileLocation := os.Getenv("AZURE_AUTH_LOCATION")
if fileLocation == "" {
return auth, errors.New("auth file not found. Environment variable AZURE_AUTH_LOCATION is not set")
}
contents, err := ioutil.ReadFile(fileLocation)
if err != nil {
return
}
// Auth file might be encoded
decoded, err := decode(contents)
if err != nil {
return
}
err = json.Unmarshal(decoded, &auth.File)
if err != nil {
return
}
resource, err := getResourceForToken(auth.File, baseURI)
if err != nil {
return
}
auth.BaseURI = resource
config, err := adal.NewOAuthConfig(auth.ActiveDirectoryEndpoint, auth.TenantID)
if err != nil {
return
}
spToken, err := adal.NewServicePrincipalToken(*config, auth.ClientID, auth.ClientSecret, resource)
if err != nil {
return
}
auth.BearerAuthorizer = autorest.NewBearerAuthorizer(spToken)
return
}
func decode(b []byte) ([]byte, error) {
reader, enc := utfbom.Skip(bytes.NewReader(b))
switch enc {
case utfbom.UTF16LittleEndian:
u16 := make([]uint16, (len(b)/2)-1)
err := binary.Read(reader, binary.LittleEndian, &u16)
if err != nil {
return nil, err
}
return []byte(string(utf16.Decode(u16))), nil
case utfbom.UTF16BigEndian:
u16 := make([]uint16, (len(b)/2)-1)
err := binary.Read(reader, binary.BigEndian, &u16)
if err != nil {
return nil, err
}
return []byte(string(utf16.Decode(u16))), nil
}
return ioutil.ReadAll(reader)
}
func getResourceForToken(f File, baseURI string) (string, error) {
// Compare dafault base URI from the SDK to the endpoints from the public cloud
// Base URI and token resource are the same string. This func finds the authentication
// file field that matches the SDK base URI. The SDK defines the public cloud
// endpoint as its default base URI
if !strings.HasSuffix(baseURI, "/") {
baseURI += "/"
}
switch baseURI {
case azure.PublicCloud.ServiceManagementEndpoint:
return f.ManagementEndpoint, nil
case azure.PublicCloud.ResourceManagerEndpoint:
return f.ResourceManagerEndpoint, nil
case azure.PublicCloud.ActiveDirectoryEndpoint:
return f.ActiveDirectoryEndpoint, nil
case azure.PublicCloud.GalleryEndpoint:
return f.GalleryEndpoint, nil
case azure.PublicCloud.GraphEndpoint:
return f.GraphResourceID, nil
}
return "", fmt.Errorf("auth: base URI not found in endpoints")
}
@@ -0,0 +1,111 @@
// 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.
package auth
import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
)
var (
expectedFile = File{
ClientID: "client-id-123",
ClientSecret: "client-secret-456",
SubscriptionID: "sub-id-789",
TenantID: "tenant-id-123",
ActiveDirectoryEndpoint: "https://login.microsoftonline.com",
ResourceManagerEndpoint: "https://management.azure.com/",
GraphResourceID: "https://graph.windows.net/",
SQLManagementEndpoint: "https://management.core.windows.net:8443/",
GalleryEndpoint: "https://gallery.azure.com/",
ManagementEndpoint: "https://management.core.windows.net/",
}
)
func TestGetClientSetup(t *testing.T) {
os.Setenv("AZURE_AUTH_LOCATION", filepath.Join(getCredsPath(), "credsutf16le.json"))
setup, err := GetClientSetup("https://management.azure.com")
if err != nil {
t.Logf("GetClientSetup failed, got error %v", err)
t.Fail()
}
if setup.BaseURI != "https://management.azure.com/" {
t.Logf("auth.BaseURI not set correctly, expected 'https://management.azure.com/', got '%s'", setup.BaseURI)
t.Fail()
}
if !reflect.DeepEqual(expectedFile, setup.File) {
t.Logf("auth.File not set correctly, expected %v, got %v", expectedFile, setup.File)
t.Fail()
}
if setup.BearerAuthorizer == nil {
t.Log("auth.Authorizer not set correctly, got nil")
t.Fail()
}
}
func TestDecodeAndUnmarshal(t *testing.T) {
tests := []string{
"credsutf8.json",
"credsutf16le.json",
"credsutf16be.json",
}
creds := getCredsPath()
for _, test := range tests {
b, err := ioutil.ReadFile(filepath.Join(creds, test))
if err != nil {
t.Logf("error reading file '%s': %s", test, err)
t.Fail()
}
decoded, err := decode(b)
if err != nil {
t.Logf("error decoding file '%s': %s", test, err)
t.Fail()
}
var got File
err = json.Unmarshal(decoded, &got)
if err != nil {
t.Logf("error unmarshaling file '%s': %s", test, err)
t.Fail()
}
if !reflect.DeepEqual(expectedFile, got) {
t.Logf("unmarshaled map expected %v, got %v", expectedFile, got)
t.Fail()
}
}
}
func getCredsPath() string {
gopath := os.Getenv("GOPATH")
return filepath.Join(gopath, "src", "github.com", "Azure", "go-autorest", "testdata")
}
func areMapsEqual(a, b map[string]string) bool {
if len(a) != len(b) {
return false
}
for k := range a {
if a[k] != b[k] {
return false
}
}
return true
}
+21 -1
View File
@@ -5,6 +5,20 @@ See the included examples for more detail.
*/
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 (
"encoding/json"
"fmt"
@@ -165,7 +179,13 @@ func WithErrorUnlessStatusCode(codes ...int) autorest.RespondDecorator {
if decodeErr != nil {
return fmt.Errorf("autorest/azure: error response cannot be parsed: %q error: %v", b.String(), decodeErr)
} else if e.ServiceError == nil {
e.ServiceError = &ServiceError{Code: "Unknown", Message: "Unknown service error"}
// Check if error is unwrapped ServiceError
if err := json.Unmarshal(b.Bytes(), &e.ServiceError); err != nil || e.ServiceError.Message == "" {
e.ServiceError = &ServiceError{
Code: "Unknown",
Message: "Unknown service error",
}
}
}
e.RequestID = ExtractRequestID(resp)
+82
View File
@@ -1,5 +1,19 @@
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 (
"encoding/json"
"fmt"
@@ -333,6 +347,74 @@ func TestWithErrorUnlessStatusCode_NoAzureError(t *testing.T) {
}
func TestWithErrorUnlessStatusCode_UnwrappedError(t *testing.T) {
j := `{
"target": null,
"code": "InternalError",
"message": "Azure is having trouble right now.",
"details": [{"code": "conflict1", "message":"error message1"},
{"code": "conflict2", "message":"error message2"}],
"innererror": []
}`
uuid := "71FDB9F4-5E49-4C12-B266-DE7B4FD999A6"
r := mocks.NewResponseWithContent(j)
mocks.SetResponseHeader(r, HeaderRequestID, uuid)
r.Request = mocks.NewRequest()
r.StatusCode = http.StatusInternalServerError
r.Status = http.StatusText(r.StatusCode)
err := autorest.Respond(r,
WithErrorUnlessStatusCode(http.StatusOK),
autorest.ByClosing())
if err == nil {
t.Fatal("azure: returned nil error for proper error response")
}
azErr, ok := err.(*RequestError)
if !ok {
t.Fatalf("returned error is not azure.RequestError: %T", err)
}
if expected := http.StatusInternalServerError; azErr.StatusCode != expected {
t.Logf("Incorrect StatusCode got: %v want: %d", azErr.StatusCode, expected)
t.Fail()
}
if expected := "Azure is having trouble right now."; azErr.ServiceError.Message != expected {
t.Logf("Incorrect Message\n\tgot: %q\n\twant: %q", azErr.Message, expected)
t.Fail()
}
if expected := uuid; azErr.RequestID != expected {
t.Logf("Incorrect request ID\n\tgot: %q\n\twant: %q", azErr.RequestID, expected)
t.Fail()
}
expectedServiceErrorDetails := `[{"code":"conflict1","message":"error message1"},{"code":"conflict2","message":"error message2"}]`
if azErr.ServiceError == nil {
t.Logf("`ServiceError` was nil when it shouldn't have been.")
t.Fail()
} else if azErr.ServiceError.Details == nil {
t.Logf("`ServiceError.Details` was nil when it should have been %q", expectedServiceErrorDetails)
t.Fail()
} else if details, _ := json.Marshal(*azErr.ServiceError.Details); expectedServiceErrorDetails != string(details) {
t.Logf("Error detaisl was not unmarshaled properly.\n\tgot: %q\n\twant: %q", string(details), expectedServiceErrorDetails)
t.Fail()
}
// the error body should still be there
defer r.Body.Close()
b, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Error(err)
}
if string(b) != j {
t.Fatalf("response body is wrong. got=%q expected=%q", string(b), j)
}
}
func TestRequestErrorString_WithError(t *testing.T) {
j := `{
"error": {
+14
View File
@@ -1,5 +1,19 @@
package cli
// 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"
"encoding/json"
+26 -1
View File
@@ -1,5 +1,19 @@
package cli
// 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 (
"encoding/json"
"fmt"
@@ -48,8 +62,19 @@ func (t Token) ToADALToken() (converted adal.Token, err error) {
}
// AccessTokensPath returns the path where access tokens are stored from the Azure CLI
// TODO(#199): add unit test.
func AccessTokensPath() (string, error) {
return homedir.Expand("~/.azure/accessTokens.json")
// Azure-CLI allows user to customize the path of access tokens thorugh environment variable.
var accessTokenPath = os.Getenv("AZURE_ACCESS_TOKEN_FILE")
var err error
// Fallback logic to default path on non-cloud-shell environment.
// TODO(#200): remove the dependency on hard-coding path.
if accessTokenPath == "" {
accessTokenPath, err = homedir.Expand("~/.azure/accessTokens.json")
}
return accessTokenPath, err
}
// ParseExpirationDate parses either a Azure CLI or CloudShell date into a time object
+49 -3
View File
@@ -1,10 +1,31 @@
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 (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
)
// EnvironmentFilepathName captures the name of the environment variable containing the path to the file
// to be used while populating the Azure Environment.
const EnvironmentFilepathName = "AZURE_ENVIRONMENT_FILEPATH"
var environments = map[string]Environment{
"AZURECHINACLOUD": ChinaCloud,
"AZUREGERMANCLOUD": GermanCloud,
@@ -62,10 +83,10 @@ var (
PublishSettingsURL: "https://manage.windowsazure.us/publishsettings/index",
ServiceManagementEndpoint: "https://management.core.usgovcloudapi.net/",
ResourceManagerEndpoint: "https://management.usgovcloudapi.net/",
ActiveDirectoryEndpoint: "https://login.microsoftonline.com/",
ActiveDirectoryEndpoint: "https://login.microsoftonline.us/",
GalleryEndpoint: "https://gallery.usgovcloudapi.net/",
KeyVaultEndpoint: "https://vault.usgovcloudapi.net/",
GraphEndpoint: "https://graph.usgovcloudapi.net/",
GraphEndpoint: "https://graph.windows.net/",
StorageEndpointSuffix: "core.usgovcloudapi.net",
SQLDatabaseDNSSuffix: "database.usgovcloudapi.net",
TrafficManagerDNSSuffix: "usgovtrafficmanager.net",
@@ -119,12 +140,37 @@ var (
}
)
// EnvironmentFromName returns an Environment based on the common name specified
// EnvironmentFromName returns an Environment based on the common name specified.
func EnvironmentFromName(name string) (Environment, error) {
// IMPORTANT
// As per @radhikagupta5:
// This is technical debt, fundamentally here because Kubernetes is not currently accepting
// contributions to the providers. Once that is an option, the provider should be updated to
// directly call `EnvironmentFromFile`. Until then, we rely on dispatching Azure Stack environment creation
// from this method based on the name that is provided to us.
if strings.EqualFold(name, "AZURESTACKCLOUD") {
return EnvironmentFromFile(os.Getenv(EnvironmentFilepathName))
}
name = strings.ToUpper(name)
env, ok := environments[name]
if !ok {
return env, fmt.Errorf("autorest/azure: There is no cloud environment matching the name %q", name)
}
return env, nil
}
// EnvironmentFromFile loads an Environment from a configuration file available on disk.
// This function is particularly useful in the Hybrid Cloud model, where one must define their own
// endpoints.
func EnvironmentFromFile(location string) (unmarshaled Environment, err error) {
fileContents, err := ioutil.ReadFile(location)
if err != nil {
return
}
err = json.Unmarshal(fileContents, &unmarshaled)
return
}
@@ -1,11 +1,79 @@
// test
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 (
"encoding/json"
"os"
"path"
"path/filepath"
"runtime"
"testing"
)
// This correlates to the expected contents of ./testdata/test_environment_1.json
var testEnvironment1 = Environment{
Name: "--unit-test--",
ManagementPortalURL: "--management-portal-url",
PublishSettingsURL: "--publish-settings-url--",
ServiceManagementEndpoint: "--service-management-endpoint--",
ResourceManagerEndpoint: "--resource-management-endpoint--",
ActiveDirectoryEndpoint: "--active-directory-endpoint--",
GalleryEndpoint: "--gallery-endpoint--",
KeyVaultEndpoint: "--key-vault--endpoint--",
GraphEndpoint: "--graph-endpoint--",
StorageEndpointSuffix: "--storage-endpoint-suffix--",
SQLDatabaseDNSSuffix: "--sql-database-dns-suffix--",
TrafficManagerDNSSuffix: "--traffic-manager-dns-suffix--",
KeyVaultDNSSuffix: "--key-vault-dns-suffix--",
ServiceBusEndpointSuffix: "--service-bus-endpoint-suffix--",
ServiceManagementVMDNSSuffix: "--asm-vm-dns-suffix--",
ResourceManagerVMDNSSuffix: "--arm-vm-dns-suffix--",
ContainerRegistryDNSSuffix: "--container-registry-dns-suffix--",
}
func TestEnvironment_EnvironmentFromFile(t *testing.T) {
got, err := EnvironmentFromFile(filepath.Join("testdata", "test_environment_1.json"))
if err != nil {
t.Error(err)
}
if got != testEnvironment1 {
t.Logf("got: %v want: %v", got, testEnvironment1)
t.Fail()
}
}
func TestEnvironment_EnvironmentFromName_Stack(t *testing.T) {
_, currentFile, _, _ := runtime.Caller(0)
prevEnvFilepathValue := os.Getenv(EnvironmentFilepathName)
os.Setenv(EnvironmentFilepathName, filepath.Join(path.Dir(currentFile), "testdata", "test_environment_1.json"))
defer os.Setenv(EnvironmentFilepathName, prevEnvFilepathValue)
got, err := EnvironmentFromName("AZURESTACKCLOUD")
if err != nil {
t.Error(err)
}
if got != testEnvironment1 {
t.Logf("got: %v want: %v", got, testEnvironment1)
t.Fail()
}
}
func TestEnvironmentFromName(t *testing.T) {
name := "azurechinacloud"
if env, _ := EnvironmentFromName(name); env != ChinaCloud {
+14
View File
@@ -1,5 +1,19 @@
package main
// 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 (
"crypto/rsa"
"crypto/x509"
+203
View File
@@ -0,0 +1,203 @@
// 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.
package azure
import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/Azure/go-autorest/autorest"
)
// DoRetryWithRegistration tries to register the resource provider in case it is unregistered.
// It also handles request retries
func DoRetryWithRegistration(client autorest.Client) autorest.SendDecorator {
return func(s autorest.Sender) autorest.Sender {
return autorest.SenderFunc(func(r *http.Request) (resp *http.Response, err error) {
rr := autorest.NewRetriableRequest(r)
for currentAttempt := 0; currentAttempt < client.RetryAttempts; currentAttempt++ {
err = rr.Prepare()
if err != nil {
return resp, err
}
resp, err = autorest.SendWithSender(s, rr.Request(),
autorest.DoRetryForStatusCodes(client.RetryAttempts, client.RetryDuration, autorest.StatusCodesForRetry...),
)
if err != nil {
return resp, err
}
if resp.StatusCode != http.StatusConflict || client.SkipResourceProviderRegistration {
return resp, err
}
var re RequestError
err = autorest.Respond(
resp,
autorest.ByUnmarshallingJSON(&re),
)
if err != nil {
return resp, err
}
err = re
if re.ServiceError != nil && re.ServiceError.Code == "MissingSubscriptionRegistration" {
regErr := register(client, r, re)
if regErr != nil {
return resp, fmt.Errorf("failed auto registering Resource Provider: %s. Original error: %s", regErr, err)
}
}
}
return resp, fmt.Errorf("failed request: %s", err)
})
}
}
func getProvider(re RequestError) (string, error) {
if re.ServiceError != nil {
if re.ServiceError.Details != nil && len(*re.ServiceError.Details) > 0 {
detail := (*re.ServiceError.Details)[0].(map[string]interface{})
return detail["target"].(string), nil
}
}
return "", errors.New("provider was not found in the response")
}
func register(client autorest.Client, originalReq *http.Request, re RequestError) error {
subID := getSubscription(originalReq.URL.Path)
if subID == "" {
return errors.New("missing parameter subscriptionID to register resource provider")
}
providerName, err := getProvider(re)
if err != nil {
return fmt.Errorf("missing parameter provider to register resource provider: %s", err)
}
newURL := url.URL{
Scheme: originalReq.URL.Scheme,
Host: originalReq.URL.Host,
}
// taken from the resources SDK
// with almost identical code, this sections are easier to mantain
// It is also not a good idea to import the SDK here
// https://github.com/Azure/azure-sdk-for-go/blob/9f366792afa3e0ddaecdc860e793ba9d75e76c27/arm/resources/resources/providers.go#L252
pathParameters := map[string]interface{}{
"resourceProviderNamespace": autorest.Encode("path", providerName),
"subscriptionId": autorest.Encode("path", subID),
}
const APIVersion = "2016-09-01"
queryParameters := map[string]interface{}{
"api-version": APIVersion,
}
preparer := autorest.CreatePreparer(
autorest.AsPost(),
autorest.WithBaseURL(newURL.String()),
autorest.WithPathParameters("/subscriptions/{subscriptionId}/providers/{resourceProviderNamespace}/register", pathParameters),
autorest.WithQueryParameters(queryParameters),
)
req, err := preparer.Prepare(&http.Request{})
if err != nil {
return err
}
req.Cancel = originalReq.Cancel
resp, err := autorest.SendWithSender(client, req,
autorest.DoRetryForStatusCodes(client.RetryAttempts, client.RetryDuration, autorest.StatusCodesForRetry...),
)
if err != nil {
return err
}
type Provider struct {
RegistrationState *string `json:"registrationState,omitempty"`
}
var provider Provider
err = autorest.Respond(
resp,
WithErrorUnlessStatusCode(http.StatusOK),
autorest.ByUnmarshallingJSON(&provider),
autorest.ByClosing(),
)
if err != nil {
return err
}
// poll for registered provisioning state
now := time.Now()
for err == nil && time.Since(now) < client.PollingDuration {
// taken from the resources SDK
// https://github.com/Azure/azure-sdk-for-go/blob/9f366792afa3e0ddaecdc860e793ba9d75e76c27/arm/resources/resources/providers.go#L45
preparer := autorest.CreatePreparer(
autorest.AsGet(),
autorest.WithBaseURL(newURL.String()),
autorest.WithPathParameters("/subscriptions/{subscriptionId}/providers/{resourceProviderNamespace}", pathParameters),
autorest.WithQueryParameters(queryParameters),
)
req, err = preparer.Prepare(&http.Request{})
if err != nil {
return err
}
req.Cancel = originalReq.Cancel
resp, err := autorest.SendWithSender(client, req,
autorest.DoRetryForStatusCodes(client.RetryAttempts, client.RetryDuration, autorest.StatusCodesForRetry...),
)
if err != nil {
return err
}
err = autorest.Respond(
resp,
WithErrorUnlessStatusCode(http.StatusOK),
autorest.ByUnmarshallingJSON(&provider),
autorest.ByClosing(),
)
if err != nil {
return err
}
if provider.RegistrationState != nil &&
*provider.RegistrationState == "Registered" {
break
}
delayed := autorest.DelayWithRetryAfter(resp, originalReq.Cancel)
if !delayed {
autorest.DelayForBackoff(client.PollingDelay, 0, originalReq.Cancel)
}
}
if !(time.Since(now) < client.PollingDuration) {
return errors.New("polling for resource provider registration has exceeded the polling duration")
}
return err
}
func getSubscription(path string) string {
parts := strings.Split(path, "/")
for i, v := range parts {
if v == "subscriptions" && (i+1) < len(parts) {
return parts[i+1]
}
}
return ""
}
+126
View File
@@ -0,0 +1,126 @@
// 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.
package azure
import (
"net/http"
"testing"
"time"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/mocks"
)
func TestDoRetryWithRegistration(t *testing.T) {
client := mocks.NewSender()
// first response, should retry because it is a transient error
client.AppendResponse(mocks.NewResponseWithStatus("Internal server error", http.StatusInternalServerError))
// response indicates the resource provider has not been registered
client.AppendResponse(mocks.NewResponseWithBodyAndStatus(mocks.NewBody(`{
"error":{
"code":"MissingSubscriptionRegistration",
"message":"The subscription registration is in 'Unregistered' state. The subscription must be registered to use namespace 'Microsoft.EventGrid'. See https://aka.ms/rps-not-found for how to register subscriptions.",
"details":[
{
"code":"MissingSubscriptionRegistration",
"target":"Microsoft.EventGrid",
"message":"The subscription registration is in 'Unregistered' state. The subscription must be registered to use namespace 'Microsoft.EventGrid'. See https://aka.ms/rps-not-found for how to register subscriptions."
}
]
}
}
`), http.StatusConflict, "MissingSubscriptionRegistration"))
// first poll response, still not ready
client.AppendResponse(mocks.NewResponseWithBodyAndStatus(mocks.NewBody(`{
"registrationState": "Registering"
}
`), http.StatusOK, "200 OK"))
// last poll response, respurce provider has been registered
client.AppendResponse(mocks.NewResponseWithBodyAndStatus(mocks.NewBody(`{
"registrationState": "Registered"
}
`), http.StatusOK, "200 OK"))
// retry original request, response is successful
client.AppendResponse(mocks.NewResponseWithStatus("200 OK", http.StatusOK))
req := mocks.NewRequestForURL("https://lol/subscriptions/rofl")
req.Body = mocks.NewBody("lolol")
r, err := autorest.SendWithSender(client, req,
DoRetryWithRegistration(autorest.Client{
PollingDelay: time.Second,
PollingDuration: time.Second * 10,
RetryAttempts: 5,
RetryDuration: time.Second,
Sender: client,
}),
)
if err != nil {
t.Fatalf("got error: %v", err)
}
autorest.Respond(r,
autorest.ByDiscardingBody(),
autorest.ByClosing(),
)
if r.StatusCode != http.StatusOK {
t.Fatalf("azure: Sender#DoRetryWithRegistration -- Got: StatusCode %v; Want: StatusCode 200 OK", r.StatusCode)
}
}
func TestDoRetrySkipRegistration(t *testing.T) {
client := mocks.NewSender()
// first response, should retry because it is a transient error
client.AppendResponse(mocks.NewResponseWithStatus("Internal server error", http.StatusInternalServerError))
// response indicates the resource provider has not been registered
client.AppendResponse(mocks.NewResponseWithBodyAndStatus(mocks.NewBody(`{
"error":{
"code":"MissingSubscriptionRegistration",
"message":"The subscription registration is in 'Unregistered' state. The subscription must be registered to use namespace 'Microsoft.EventGrid'. See https://aka.ms/rps-not-found for how to register subscriptions.",
"details":[
{
"code":"MissingSubscriptionRegistration",
"target":"Microsoft.EventGrid",
"message":"The subscription registration is in 'Unregistered' state. The subscription must be registered to use namespace 'Microsoft.EventGrid'. See https://aka.ms/rps-not-found for how to register subscriptions."
}
]
}
}`), http.StatusConflict, "MissingSubscriptionRegistration"))
req := mocks.NewRequestForURL("https://lol/subscriptions/rofl")
req.Body = mocks.NewBody("lolol")
r, err := autorest.SendWithSender(client, req,
DoRetryWithRegistration(autorest.Client{
PollingDelay: time.Second,
PollingDuration: time.Second * 10,
RetryAttempts: 5,
RetryDuration: time.Second,
Sender: client,
SkipResourceProviderRegistration: true,
}),
)
if err != nil {
t.Fatalf("got error: %v", err)
}
autorest.Respond(r,
autorest.ByDiscardingBody(),
autorest.ByClosing(),
)
if r.StatusCode != http.StatusConflict {
t.Fatalf("azure: Sender#DoRetryWithRegistration -- Got: StatusCode %v; Want: StatusCode 409 Conflict", r.StatusCode)
}
}
@@ -0,0 +1,19 @@
{
"name": "--unit-test--",
"managementPortalURL": "--management-portal-url",
"publishSettingsURL": "--publish-settings-url--",
"serviceManagementEndpoint": "--service-management-endpoint--",
"resourceManagerEndpoint": "--resource-management-endpoint--",
"activeDirectoryEndpoint": "--active-directory-endpoint--",
"galleryEndpoint": "--gallery-endpoint--",
"keyVaultEndpoint": "--key-vault--endpoint--",
"graphEndpoint": "--graph-endpoint--",
"storageEndpointSuffix": "--storage-endpoint-suffix--",
"sqlDatabaseDNSSuffix": "--sql-database-dns-suffix--",
"trafficManagerDNSSuffix": "--traffic-manager-dns-suffix--",
"keyVaultDNSSuffix": "--key-vault-dns-suffix--",
"serviceBusEndpointSuffix": "--service-bus-endpoint-suffix--",
"serviceManagementVMDNSSuffix": "--asm-vm-dns-suffix--",
"resourceManagerVMDNSSuffix": "--arm-vm-dns-suffix--",
"containerRegistryDNSSuffix": "--container-registry-dns-suffix--"
}