Răsfoiți Sursa

Extract interface / types to allow unit tests / mock

Guillaume Tardif 5 ani în urmă
părinte
comite
69f10fe80c
8 a modificat fișierele cu 278 adăugiri și 118 ștergeri
  1. 1 1
      .golangci.yml
  2. 10 12
      azure/backend.go
  3. 141 93
      azure/login/login.go
  4. 113 0
      azure/login/login_test.go
  5. 6 12
      azure/login/tokenStore.go
  6. 1 0
      cli/cmd/context/login/login.go
  7. 2 0
      errdefs/errors.go
  8. 4 0
      go.sum

+ 1 - 1
.golangci.yml

@@ -2,7 +2,7 @@ linters:
   run:
     concurrency: 2
     skip-dirs:
-      - composefiles
+      - tests/composefiles
   enable-all: false
   disable-all: true
   enable:

+ 10 - 12
azure/backend.go

@@ -3,12 +3,13 @@ package azure
 import (
 	"context"
 	"fmt"
-	"github.com/docker/api/context/cloud"
 	"io"
 	"net/http"
 	"strconv"
 	"strings"
 
+	"github.com/docker/api/context/cloud"
+
 	"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance"
 	"github.com/Azure/go-autorest/autorest/azure/auth"
 	"github.com/compose-spec/compose-go/types"
@@ -69,7 +70,9 @@ func getAciAPIService(cgc containerinstance.ContainerGroupsClient, aciCtx store.
 			containerGroupsClient: cgc,
 			ctx:                   aciCtx,
 		},
-		aciCloudService: aciCloudService{},
+		aciCloudService: aciCloudService{
+			loginService: login.NewAzureLoginService(),
+		},
 	}
 }
 
@@ -80,21 +83,15 @@ type aciAPIService struct {
 }
 
 func (a *aciAPIService) ContainerService() containers.Service {
-	return &aciContainerService{
-		containerGroupsClient: a.aciContainerService.containerGroupsClient,
-		ctx:                   a.aciContainerService.ctx,
-	}
+	return &a.aciContainerService
 }
 
 func (a *aciAPIService) ComposeService() compose.Service {
-	return &aciComposeService{
-		containerGroupsClient: a.aciComposeService.containerGroupsClient,
-		ctx:                   a.aciComposeService.ctx,
-	}
+	return &a.aciComposeService
 }
 
 func (a *aciAPIService) CloudService() cloud.Service {
-	return &aciCloudService{}
+	return &a.aciCloudService
 }
 
 type aciContainerService struct {
@@ -276,8 +273,9 @@ func (cs *aciComposeService) Down(ctx context.Context, opts compose.ProjectOptio
 }
 
 type aciCloudService struct {
+	loginService login.AzureLoginService
 }
 
 func (cs *aciCloudService) Login(ctx context.Context, params map[string]string) error {
-	return login.Login()
+	return cs.loginService.Login()
 }

+ 141 - 93
azure/login/login.go

@@ -19,6 +19,8 @@ import (
 	"syscall"
 	"time"
 
+	"github.com/docker/api/errdefs"
+
 	"github.com/Azure/go-autorest/autorest"
 	"github.com/Azure/go-autorest/autorest/adal"
 	"github.com/Azure/go-autorest/autorest/azure/cli"
@@ -28,6 +30,10 @@ import (
 	"github.com/pkg/errors"
 )
 
+func init() {
+	rand.Seed(time.Now().Unix())
+}
+
 //go login process, derived from code sample provided by MS at https://github.com/devigned/go-az-cli-stuff
 const (
 	authorizeFormat = "https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize?response_type=code&client_id=%s&redirect_uri=%s&state=%s&prompt=select_account&response_mode=query&scope=%s"
@@ -39,7 +45,7 @@ const (
 )
 
 type (
-	Token struct {
+	azureToken struct {
 		Type         string `json:"token_type"`
 		Scope        string `json:"scope"`
 		ExpiresIn    int    `json:"expires_in"`
@@ -49,67 +55,65 @@ type (
 		Foci         string `json:"foci"`
 	}
 
-	TenantResult struct {
-		Value []TenantValue `json:"value"`
+	tenantResult struct {
+		Value []tenantValue `json:"value"`
 	}
-	TenantValue struct {
+	tenantValue struct {
 		TenantID string `json:"tenantId"`
 	}
 )
 
-//AzureLogin login through browser
-func Login() error {
+// AzureLoginService Service to log into azure and get authentifier for azure APIs
+type AzureLoginService struct {
+	tokenStore tokenStore
+	apiHelper  apiHelper
+}
+
+const tokenFilename = "dockerAccessToken.json"
+
+func getTokenStorePath() string {
+	cliPath, _ := cli.AccessTokensPath()
+	return filepath.Join(filepath.Dir(cliPath), tokenFilename)
+}
+
+// NewAzureLoginService creates a NewAzureLoginService
+func NewAzureLoginService() AzureLoginService {
+	return newAzureLoginServiceFromPath(getTokenStorePath(), azureAPIHelper{})
+}
+
+func newAzureLoginServiceFromPath(tokenStorePath string, helper apiHelper) AzureLoginService {
+	return AzureLoginService{
+		tokenStore: tokenStore{
+			filePath: tokenStorePath,
+		},
+		apiHelper: helper,
+	}
+}
+
+type apiHelper interface {
+	queryToken(data url.Values, tenantID string) (token azureToken, err error)
+}
+
+type azureAPIHelper struct{}
+
+//Login perform azure login through browser
+func (login AzureLoginService) Login() error {
 	sigs := make(chan os.Signal, 1)
 	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
 
 	queryCh := make(chan url.Values, 1)
-	queryHandler := func(w http.ResponseWriter, r *http.Request) {
-		queryCh <- r.URL.Query()
-		_, hasCode := r.URL.Query()["code"]
-		if hasCode {
-			w.Write([]byte(`
-<!DOCTYPE html>
-<html>
-<head>
-    <meta charset="utf-8" />
-    <meta http-equiv="refresh" content="10;url=https://docs.microsoft.com/cli/azure/">
-    <title>Login successfully</title>
-</head>
-<body>
-    <h4>You have logged into Microsoft Azure!</h4>
-    <p>You can close this window, or we will redirect you to the <a href="https://docs.microsoft.com/cli/azure/">Azure CLI documents</a> in 10 seconds.</p>
-</body>
-</html>
-		`))
-		} else {
-			w.Write([]byte(`
-<!DOCTYPE html>
-<html>
-<head>
-    <meta charset="utf-8" />
-    <title>Login failed</title>
-</head>
-<body>
-    <h4>Some failures occurred during the authentication</h4>
-    <p>You can log an issue at <a href="https://github.com/azure/azure-cli/issues">Azure CLI GitHub Repository</a> and we will assist you in resolving it.</p>
-</body>
-</html>
-`))
-		}
-	}
-
 	mux := http.NewServeMux()
-	mux.HandleFunc("/", queryHandler)
+	mux.HandleFunc("/", queryHandler(queryCh))
 	server := &http.Server{Addr: ":8401", Handler: mux}
 	go func() {
 		if err := server.ListenAndServe(); err != nil {
-			fmt.Println(fmt.Errorf("error starting http server with: %w", err))
-			os.Exit(1)
+			queryCh <- url.Values{
+				"error": []string{fmt.Sprintf("error starting http server with: %v", err)},
+			}
 		}
 	}()
 
-	state := RandomString("", 10)
-	//nonce := RandomString("", 10)
+	state := randomString("", 10)
 	authURL := fmt.Sprintf(authorizeFormat, clientID, "http://localhost:8401", state, scopes)
 	openbrowser(authURL)
 
@@ -117,9 +121,13 @@ func Login() error {
 	case <-sigs:
 		return nil
 	case qsValues := <-queryCh:
+		errorMsg, hasError := qsValues["error"]
+		if hasError {
+			return fmt.Errorf("login failed : %s", errorMsg)
+		}
 		code, hasCode := qsValues["code"]
 		if !hasCode {
-			return fmt.Errorf("Authentication Error : Login failed")
+			return errdefs.ErrLoginFailed
 		}
 		data := url.Values{
 			"grant_type":   []string{"authorization_code"},
@@ -128,7 +136,7 @@ func Login() error {
 			"scope":        []string{scopes},
 			"redirect_uri": []string{"http://localhost:8401"},
 		}
-		token, err := queryToken(data, "organizations")
+		token, err := login.apiHelper.queryToken(data, "organizations")
 		if err != nil {
 			return errors.Wrap(err, "Access token request failed")
 		}
@@ -141,53 +149,78 @@ func Login() error {
 		req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
 		res, err := http.DefaultClient.Do(req)
 		if err != nil {
-			return errors.Wrap(err, "Authentication Error")
+			return errors.Wrap(err, "login failed")
 		}
 
 		bits, err := ioutil.ReadAll(res.Body)
 		if err != nil {
-			return errors.Wrap(err, "Authentication Error")
+			return errors.Wrap(err, "login failed")
 		}
 
 		if res.StatusCode == 200 {
-			var tenantResult TenantResult
+			var tenantResult tenantResult
 			if err := json.Unmarshal(bits, &tenantResult); err != nil {
-				return errors.Wrap(err, "Authentication Error")
+				return errors.Wrap(err, "login failed")
 			}
 			tenantID := tenantResult.Value[0].TenantID
-			tenantToken, err := refreshToken(token.RefreshToken, tenantID)
+			tenantToken, err := login.refreshToken(token.RefreshToken, tenantID)
 			if err != nil {
-				return errors.Wrap(err, "Authentication Error")
+				return errors.Wrap(err, "login failed")
 			}
-			loginInfo := LoginInfo{TenantID: tenantID, Token: tenantToken}
+			loginInfo := TokenInfo{TenantID: tenantID, Token: tenantToken}
 
-			store := NewTokenStore(getTokenPath())
-			err = store.writeLoginInfo(loginInfo)
+			err = login.tokenStore.writeLoginInfo(loginInfo)
 
 			if err != nil {
-				return errors.Wrap(err, "Authentication Error")
+				return errors.Wrap(err, "login failed")
 			}
-			fmt.Println("Successfully logged in")
+			fmt.Println("Login Succeeded")
 
 			return nil
 		}
 
 		bits, err = httputil.DumpResponse(res, true)
 		if err != nil {
-			return errors.Wrap(err, "Authentication Error")
+			return errors.Wrap(err, "login failed")
 		}
 
-		return fmt.Errorf("Authentication Error: \n" + string(bits))
+		return fmt.Errorf("login failed: \n" + string(bits))
+	}
+}
+
+func queryHandler(queryCh chan url.Values) func(w http.ResponseWriter, r *http.Request) {
+	queryHandler := func(w http.ResponseWriter, r *http.Request) {
+		_, hasCode := r.URL.Query()["code"]
+		if hasCode {
+			_, err := w.Write([]byte(successfullLoginHTML))
+			if err != nil {
+				queryCh <- url.Values{
+					"error": []string{err.Error()},
+				}
+			} else {
+				queryCh <- r.URL.Query()
+			}
+		} else {
+			_, err := w.Write([]byte(loginFailedHTML))
+			if err != nil {
+				queryCh <- url.Values{
+					"error": []string{err.Error()},
+				}
+			} else {
+				queryCh <- r.URL.Query()
+			}
+		}
 	}
+	return queryHandler
 }
 
-func queryToken(data url.Values, tenantID string) (token Token, err error) {
+func (helper azureAPIHelper) queryToken(data url.Values, tenantID string) (token azureToken, err error) {
 	res, err := http.Post(fmt.Sprintf(tokenEndpoint, tenantID), "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
 	if err != nil {
 		return token, err
 	}
 	if res.StatusCode != 200 {
-		return token, err
+		return token, errors.Errorf("error while renewing access token, status : %s", res.Status)
 	}
 	bits, err := ioutil.ReadAll(res.Body)
 	if err != nil {
@@ -199,8 +232,8 @@ func queryToken(data url.Values, tenantID string) (token Token, err error) {
 	return token, nil
 }
 
-func toOAuthToken(token Token) oauth2.Token {
-	expireTime := time.Now().Add(time.Duration(token.ExtExpiresIn) * time.Second)
+func toOAuthToken(token azureToken) oauth2.Token {
+	expireTime := time.Now().Add(time.Duration(token.ExpiresIn) * time.Second)
 	oauthToken := oauth2.Token{
 		RefreshToken: token.RefreshToken,
 		AccessToken:  token.AccessToken,
@@ -210,27 +243,18 @@ func toOAuthToken(token Token) oauth2.Token {
 	return oauthToken
 }
 
-const tokenFilename = "dockerAccessToken.json"
-
-func getTokenPath() string {
-	cliPath, _ := cli.AccessTokensPath()
-
-	return filepath.Join(filepath.Dir(cliPath), tokenFilename)
-}
-
-func NewAuthorizerFromLogin() (autorest.Authorizer, error) {
-	oauthToken, err := GetValidToken()
+// NewAuthorizerFromLogin creates an authorizer based on login access token
+func (login AzureLoginService) NewAuthorizerFromLogin() (autorest.Authorizer, error) {
+	oauthToken, err := login.GetValidToken()
 	if err != nil {
 		return nil, err
 	}
 
-	difference := oauthToken.Expiry.Sub(date.UnixEpoch())
-
 	token := adal.Token{
 		AccessToken:  oauthToken.AccessToken,
 		Type:         oauthToken.TokenType,
-		ExpiresIn:    "3600",
-		ExpiresOn:    json.Number(strconv.Itoa(int(difference.Seconds()))),
+		ExpiresIn:    json.Number(strconv.Itoa(int(oauthToken.Expiry.Sub(time.Now()).Seconds()))),
+		ExpiresOn:    json.Number(strconv.Itoa(int(oauthToken.Expiry.Sub(date.UnixEpoch()).Seconds()))),
 		RefreshToken: "",
 		Resource:     "",
 	}
@@ -238,9 +262,9 @@ func NewAuthorizerFromLogin() (autorest.Authorizer, error) {
 	return autorest.NewBearerAuthorizer(&token), nil
 }
 
-func GetValidToken() (token oauth2.Token, err error) {
-	store := NewTokenStore(getTokenPath())
-	loginInfo, err := store.readToken()
+// GetValidToken returns an access token. Refresh token if needed
+func (login AzureLoginService) GetValidToken() (token oauth2.Token, err error) {
+	loginInfo, err := login.tokenStore.readToken()
 	if err != nil {
 		return token, err
 	}
@@ -249,25 +273,25 @@ func GetValidToken() (token oauth2.Token, err error) {
 		return token, nil
 	}
 	tenantID := loginInfo.TenantID
-	token, err = refreshToken(token.RefreshToken, tenantID)
+	token, err = login.refreshToken(token.RefreshToken, tenantID)
 	if err != nil {
-		return token, errors.Wrap(err, "Access token request failed. Maybe you need to login to azure again.")
+		return token, errors.Wrap(err, "access token request failed. Maybe you need to login to azure again.")
 	}
-	err = store.writeLoginInfo(LoginInfo{TenantID: tenantID, Token: token})
+	err = login.tokenStore.writeLoginInfo(TokenInfo{TenantID: tenantID, Token: token})
 	if err != nil {
 		return token, err
 	}
 	return token, nil
 }
 
-func refreshToken(currentRefreshToken string, tenantID string) (oauthToken oauth2.Token, err error) {
+func (login AzureLoginService) refreshToken(currentRefreshToken string, tenantID string) (oauthToken oauth2.Token, err error) {
 	data := url.Values{
 		"grant_type":    []string{"refresh_token"},
 		"client_id":     []string{clientID},
 		"scope":         []string{scopes},
 		"refresh_token": []string{currentRefreshToken},
 	}
-	token, err := queryToken(data, tenantID)
+	token, err := login.apiHelper.queryToken(data, tenantID)
 	if err != nil {
 		return oauthToken, err
 	}
@@ -297,15 +321,39 @@ var (
 	letterRunes = []rune("abcdefghijklmnopqrstuvwxyz123456789")
 )
 
-func init() {
-	rand.Seed(time.Now().Unix())
-}
-
-// RandomString generates a random string with prefix
-func RandomString(prefix string, length int) string {
+func randomString(prefix string, length int) string {
 	b := make([]rune, length)
 	for i := range b {
 		b[i] = letterRunes[rand.Intn(len(letterRunes))]
 	}
 	return prefix + string(b)
 }
+
+const loginFailedHTML = `
+	<!DOCTYPE html>
+	<html>
+	<head>
+	    <meta charset="utf-8" />
+	    <title>Login failed</title>
+	</head>
+	<body>
+	    <h4>Some failures occurred during the authentication</h4>
+	    <p>You can log an issue at <a href="https://github.com/azure/azure-cli/issues">Azure CLI GitHub Repository</a> and we will assist you in resolving it.</p>
+	</body>
+	</html>
+	`
+
+const successfullLoginHTML = `
+	<!DOCTYPE html>
+	<html>
+	<head>
+	    <meta charset="utf-8" />
+	    <meta http-equiv="refresh" content="10;url=https://docs.microsoft.com/cli/azure/">
+	    <title>Login successfully</title>
+	</head>
+	<body>
+	    <h4>You have logged into Microsoft Azure!</h4>
+	    <p>You can close this window, or we will redirect you to the <a href="https://docs.microsoft.com/cli/azure/">Azure CLI documents</a> in 10 seconds.</p>
+	</body>
+	</html>
+	`

+ 113 - 0
azure/login/login_test.go

@@ -0,0 +1,113 @@
+package login
+
+import (
+	"io/ioutil"
+	"net/url"
+	"os"
+	"path/filepath"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/suite"
+
+	"golang.org/x/oauth2"
+
+	. "github.com/onsi/gomega"
+)
+
+type LoginSuiteTest struct {
+	suite.Suite
+	dir        string
+	mockHelper MockAzureHelper
+	azureLogin AzureLoginService
+}
+
+func (suite *LoginSuiteTest) BeforeTest(suiteName, testName string) {
+	dir, err := ioutil.TempDir("", "test_store")
+	require.Nil(suite.T(), err)
+
+	suite.dir = dir
+	suite.mockHelper = MockAzureHelper{}
+	//nolint copylocks
+	suite.azureLogin = newAzureLoginServiceFromPath(filepath.Join(dir, tokenFilename), suite.mockHelper)
+}
+
+func (suite *LoginSuiteTest) AfterTest(suiteName, testName string) {
+	err := os.RemoveAll(suite.dir)
+	require.Nil(suite.T(), err)
+}
+
+func (suite *LoginSuiteTest) TestRefreshInValidToken() {
+	data := url.Values{
+		"grant_type":    []string{"refresh_token"},
+		"client_id":     []string{clientID},
+		"scope":         []string{scopes},
+		"refresh_token": []string{"refreshToken"},
+	}
+	suite.mockHelper.On("queryToken", data, "123456").Return(azureToken{
+		RefreshToken: "newRefreshToken",
+		AccessToken:  "newAccessToken",
+		ExpiresIn:    3600,
+		Foci:         "1",
+	}, nil)
+
+	//nolint copylocks
+	suite.azureLogin = newAzureLoginServiceFromPath(filepath.Join(suite.dir, tokenFilename), suite.mockHelper)
+	err := suite.azureLogin.tokenStore.writeLoginInfo(TokenInfo{
+		TenantID: "123456",
+		Token: oauth2.Token{
+			AccessToken:  "accessToken",
+			RefreshToken: "refreshToken",
+			Expiry:       time.Now().Add(-1 * time.Hour),
+			TokenType:    "Bearer",
+		},
+	})
+	Expect(err).To(BeNil())
+
+	token, _ := suite.azureLogin.GetValidToken()
+
+	Expect(token.AccessToken).To(Equal("newAccessToken"))
+	Expect(token.Expiry).To(BeTemporally(">", time.Now().Add(3500*time.Second)))
+
+	storedToken, _ := suite.azureLogin.tokenStore.readToken()
+	Expect(storedToken.Token.AccessToken).To(Equal("newAccessToken"))
+	Expect(storedToken.Token.RefreshToken).To(Equal("newRefreshToken"))
+	Expect(storedToken.Token.Expiry).To(BeTemporally(">", time.Now().Add(3500*time.Second)))
+}
+
+func (suite *LoginSuiteTest) TestDoesNotRefreshValidToken() {
+	expiryDate := time.Now().Add(1 * time.Hour)
+	err := suite.azureLogin.tokenStore.writeLoginInfo(TokenInfo{
+		TenantID: "123456",
+		Token: oauth2.Token{
+			AccessToken:  "accessToken",
+			RefreshToken: "refreshToken",
+			Expiry:       expiryDate,
+			TokenType:    "Bearer",
+		},
+	})
+	Expect(err).To(BeNil())
+
+	token, _ := suite.azureLogin.GetValidToken()
+
+	Expect(suite.mockHelper.Calls).To(BeEmpty())
+	Expect(token.AccessToken).To(Equal("accessToken"))
+}
+
+func TestLoginSuite(t *testing.T) {
+	RegisterTestingT(t)
+	suite.Run(t, new(LoginSuiteTest))
+}
+
+type MockAzureHelper struct {
+	mock.Mock
+}
+
+//nolint copylocks
+func (s MockAzureHelper) queryToken(data url.Values, tenantID string) (token azureToken, err error) {
+	args := s.Called(data, tenantID)
+	return args.Get(0).(azureToken), args.Error(1)
+}

+ 6 - 12
azure/login/tokenStore.go

@@ -7,31 +7,25 @@ import (
 	"golang.org/x/oauth2"
 )
 
-type TokenStore struct {
+type tokenStore struct {
 	filePath string
 }
 
-type LoginInfo struct {
+// TokenInfo data stored in tokenStore
+type TokenInfo struct {
 	Token    oauth2.Token `json:"oauthToken"`
 	TenantID string       `json:"tenantId"`
 }
 
-func NewTokenStore(filePath string) TokenStore {
-	return TokenStore{
-		filePath: filePath,
-	}
-}
-
-func (store TokenStore) writeLoginInfo(info LoginInfo) error {
+func (store tokenStore) writeLoginInfo(info TokenInfo) error {
 	bytes, err := json.MarshalIndent(info, "", "  ")
 	if err != nil {
 		return err
 	}
-	ioutil.WriteFile(store.filePath, bytes, 0644)
-	return nil
+	return ioutil.WriteFile(store.filePath, bytes, 0644)
 }
 
-func (store TokenStore) readToken() (loginInfo LoginInfo, err error) {
+func (store tokenStore) readToken() (loginInfo TokenInfo, err error) {
 	bytes, err := ioutil.ReadFile(store.filePath)
 	if err != nil {
 		return loginInfo, err

+ 1 - 0
cli/cmd/context/login/login.go

@@ -3,6 +3,7 @@ package login
 import (
 	"github.com/spf13/cobra"
 	"github.com/pkg/errors"
+
 	"github.com/docker/api/client"
 	apicontext "github.com/docker/api/context"
 )

+ 2 - 0
errdefs/errors.go

@@ -40,6 +40,8 @@ var (
 	ErrForbidden = errors.New("forbidden")
 	// ErrUnknown is returned when the error type is unmapped
 	ErrUnknown = errors.New("unknown")
+	// ErrLoginFailed is returned when login failed
+	ErrLoginFailed = errors.New("login failed")
 )
 
 // IsNotFoundError returns true if the unwrapped error is ErrNotFound

+ 4 - 0
go.sum

@@ -3,10 +3,12 @@ github.com/Azure/azure-sdk-for-go v42.0.0+incompatible h1:yz6sFf5bHZ+gEOQVuK5JhP
 github.com/Azure/azure-sdk-for-go v42.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
 github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
+github.com/Azure/go-autorest v14.1.0+incompatible h1:qROrS0rWxAXGfFdNOI33we8553d7T8v78jXf/8tjLBM=
 github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
 github.com/Azure/go-autorest/autorest v0.9.3/go.mod h1:GsRuLYvwzLjjjRoWEIyMUaYq8GNUx2nRB378IPt/1p0=
 github.com/Azure/go-autorest/autorest v0.10.0 h1:mvdtztBqcL8se7MdrUweNieTNi4kfNG6GOJuurQJpuY=
 github.com/Azure/go-autorest/autorest v0.10.0/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630=
+github.com/Azure/go-autorest/autorest v0.10.1 h1:uaB8A32IZU9YKs9v50+/LWIWTDHJk2vlGzbfd7FfESI=
 github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
 github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc=
 github.com/Azure/go-autorest/autorest/adal v0.8.1/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
@@ -235,6 +237,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -311,6 +314,7 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=