Browse Source

Merge pull request #164 from docker/interactive_context_create

Interactive context create
Guillaume Tardif 5 years ago
parent
commit
c37f666c07

+ 0 - 40
azure/aci.go

@@ -9,8 +9,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/resources/mgmt/resources"
-	"github.com/Azure/azure-sdk-for-go/profiles/preview/preview/subscription/mgmt/subscription"
 	"github.com/Azure/azure-sdk-for-go/services/containerinstance/mgmt/2018-10-01/containerinstance"
 	"github.com/Azure/go-autorest/autorest"
 	"github.com/Azure/go-autorest/autorest/to"
@@ -20,8 +18,6 @@ import (
 	"github.com/pkg/errors"
 
 	"github.com/docker/api/azure/login"
-	"github.com/docker/api/errdefs"
-
 	"github.com/docker/api/context/store"
 )
 
@@ -257,39 +253,3 @@ func getContainerClient(subscriptionID string) (containerinstance.ContainerClien
 	containerClient.Authorizer = auth
 	return containerClient, nil
 }
-
-func getSubscriptionsClient() (subscription.SubscriptionsClient, error) {
-	subc := subscription.NewSubscriptionsClient()
-	authorizer, err := login.NewAuthorizerFromLogin()
-	if err != nil {
-		return subscription.SubscriptionsClient{}, errors.Wrap(errdefs.ErrLoginFailed, err.Error())
-	}
-	subc.Authorizer = authorizer
-	return subc, nil
-}
-
-// GetGroupsClient ...
-func GetGroupsClient(subscriptionID string) resources.GroupsClient {
-	groupsClient := resources.NewGroupsClient(subscriptionID)
-	authorizer, _ := login.NewAuthorizerFromLogin()
-	groupsClient.Authorizer = authorizer
-	return groupsClient
-}
-
-// GetSubscriptionID ...
-func GetSubscriptionID(ctx context.Context) (string, error) {
-	c, err := getSubscriptionsClient()
-	if err != nil {
-		return "", err
-	}
-	res, err := c.List(ctx)
-	if err != nil {
-		return "", err
-	}
-	subs := res.Values()
-	if len(subs) == 0 {
-		return "", errors.New("no subscriptions found")
-	}
-	sub := subs[0]
-	return *sub.SubscriptionID, nil
-}

+ 5 - 0
azure/backend.go

@@ -282,3 +282,8 @@ type aciCloudService struct {
 func (cs *aciCloudService) Login(ctx context.Context, params map[string]string) error {
 	return cs.loginService.Login(ctx)
 }
+
+func (cs *aciCloudService) CreateContextData(ctx context.Context, params map[string]string) (interface{}, string, error) {
+	contextHelper := newContextCreateHelper()
+	return contextHelper.createContextData(ctx, params)
+}

+ 181 - 0
azure/context.go

@@ -0,0 +1,181 @@
+/*
+	Copyright (c) 2020 Docker Inc.
+
+	Permission is hereby granted, free of charge, to any person
+	obtaining a copy of this software and associated documentation
+	files (the "Software"), to deal in the Software without
+	restriction, including without limitation the rights to use, copy,
+	modify, merge, publish, distribute, sublicense, and/or sell copies
+	of the Software, and to permit persons to whom the Software is
+	furnished to do so, subject to the following conditions:
+
+	The above copyright notice and this permission notice shall be
+	included in all copies or substantial portions of the Software.
+
+	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+	EXPRESS OR IMPLIED,
+	INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+	IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+	HOLDERS BE LIABLE FOR ANY CLAIM,
+	DAMAGES OR OTHER LIABILITY,
+	WHETHER IN AN ACTION OF CONTRACT,
+	TORT OR OTHERWISE,
+	ARISING FROM, OUT OF OR IN CONNECTION WITH
+	THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+package azure
+
+import (
+	"context"
+	"fmt"
+	"os"
+
+	"github.com/AlecAivazis/survey/v2"
+	"github.com/Azure/azure-sdk-for-go/profiles/preview/preview/subscription/mgmt/subscription"
+	"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-05-01/resources"
+	"github.com/google/uuid"
+	"github.com/pkg/errors"
+	"github.com/tj/survey/terminal"
+
+	"github.com/docker/api/context/store"
+)
+
+type contextCreateACIHelper struct {
+	selector            userSelector
+	resourceGroupHelper ACIResourceGroupHelper
+}
+
+func newContextCreateHelper() contextCreateACIHelper {
+	return contextCreateACIHelper{
+		selector:            cliUserSelector{},
+		resourceGroupHelper: aciResourceGroupHelperImpl{},
+	}
+}
+
+func (helper contextCreateACIHelper) createContextData(ctx context.Context, opts map[string]string) (interface{}, string, error) {
+	var subscriptionID string
+	if opts["aciSubscriptionID"] != "" {
+		subscriptionID = opts["aciSubscriptionID"]
+	} else {
+		subs, err := helper.resourceGroupHelper.GetSubscriptionIDs(ctx)
+		if err != nil {
+			return nil, "", err
+		}
+		subscriptionID, err = helper.chooseSub(subs)
+		if err != nil {
+			return nil, "", err
+		}
+	}
+
+	var group resources.Group
+	var err error
+
+	if opts["aciResourceGroup"] != "" {
+		group, err = helper.resourceGroupHelper.GetGroup(ctx, subscriptionID, opts["aciResourceGroup"])
+		if err != nil {
+			return nil, "", errors.Wrapf(err, "Could not find resource group %q", opts["aciResourceGroup"])
+		}
+	} else {
+		groups, err := helper.resourceGroupHelper.ListGroups(ctx, subscriptionID)
+		if err != nil {
+			return nil, "", err
+		}
+		group, err = helper.chooseGroup(ctx, subscriptionID, opts, groups)
+		if err != nil {
+			return nil, "", err
+		}
+	}
+
+	location := opts["aciLocation"]
+	if location == "" {
+		location = *group.Location
+	}
+
+	description := fmt.Sprintf("%s@%s", *group.Name, location)
+	if opts["description"] != "" {
+		description = fmt.Sprintf("%s (%s)", opts["description"], description)
+	}
+
+	return store.AciContext{
+		SubscriptionID: subscriptionID,
+		Location:       location,
+		ResourceGroup:  *group.Name,
+	}, description, nil
+}
+
+func (helper contextCreateACIHelper) createGroup(ctx context.Context, subscriptionID, location string) (resources.Group, error) {
+	if location == "" {
+		location = "eastus"
+	}
+	gid := uuid.New().String()
+	g, err := helper.resourceGroupHelper.CreateOrUpdate(ctx, subscriptionID, gid, resources.Group{
+		Location: &location,
+	})
+	if err != nil {
+		return resources.Group{}, err
+	}
+
+	fmt.Printf("Resource group %q (%s) created\n", *g.Name, *g.Location)
+
+	return g, nil
+}
+
+func (helper contextCreateACIHelper) chooseGroup(ctx context.Context, subscriptionID string, opts map[string]string, groups []resources.Group) (resources.Group, error) {
+	groupNames := []string{"create a new resource group"}
+	for _, g := range groups {
+		groupNames = append(groupNames, fmt.Sprintf("%s (%s)", *g.Name, *g.Location))
+	}
+
+	group, err := helper.selector.userSelect("Select a resource group", groupNames)
+	if err != nil {
+		if err == terminal.InterruptErr {
+			os.Exit(0)
+		}
+
+		return resources.Group{}, err
+	}
+
+	if group == 0 {
+		return helper.createGroup(ctx, subscriptionID, opts["aciLocation"])
+	}
+
+	return groups[group-1], nil
+}
+
+func (helper contextCreateACIHelper) chooseSub(subs []subscription.Model) (string, error) {
+	if len(subs) == 1 {
+		sub := subs[0]
+		fmt.Println("Using only available subscription : " + *sub.DisplayName + "(" + *sub.SubscriptionID + ")")
+		return *sub.SubscriptionID, nil
+	}
+	var options []string
+	for _, sub := range subs {
+		options = append(options, *sub.DisplayName+"("+*sub.SubscriptionID+")")
+	}
+	selected, err := helper.selector.userSelect("Select a subscription ID", options)
+	if err != nil {
+		if err == terminal.InterruptErr {
+			os.Exit(0)
+		}
+		return "", err
+	}
+	return *subs[selected].SubscriptionID, nil
+}
+
+type userSelector interface {
+	userSelect(message string, options []string) (int, error)
+}
+
+type cliUserSelector struct{}
+
+func (us cliUserSelector) userSelect(message string, options []string) (int, error) {
+	qs := &survey.Select{
+		Message: message,
+		Options: options,
+	}
+	var selected int
+	err := survey.AskOne(qs, &selected, nil)
+	return selected, err
+}

+ 210 - 0
azure/context_test.go

@@ -0,0 +1,210 @@
+package azure
+
+import (
+	"context"
+	"testing"
+
+	"github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/resources/mgmt/resources"
+	"github.com/Azure/azure-sdk-for-go/profiles/preview/preview/subscription/mgmt/subscription"
+	"github.com/Azure/go-autorest/autorest/to"
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/suite"
+
+	"github.com/docker/api/context/store"
+
+	. "github.com/onsi/gomega"
+)
+
+type ContextSuiteTest struct {
+	suite.Suite
+	mockUserSelector       *MockUserSelector
+	mockResourceGroupHeper *MockResourceGroupHelper
+	contextCreateHelper    contextCreateACIHelper
+}
+
+func (suite *ContextSuiteTest) BeforeTest(suiteName, testName string) {
+	suite.mockUserSelector = &MockUserSelector{}
+	suite.mockResourceGroupHeper = &MockResourceGroupHelper{}
+	suite.contextCreateHelper = contextCreateACIHelper{
+		suite.mockUserSelector,
+		suite.mockResourceGroupHeper,
+	}
+}
+
+func (suite *ContextSuiteTest) TestCreateSpecifiedSubscriptionAndGroup() {
+	ctx := context.TODO()
+	opts := options("1234", "myResourceGroup")
+	suite.mockResourceGroupHeper.On("GetGroup", ctx, "1234", "myResourceGroup").Return(group("myResourceGroup", "eastus"), nil)
+
+	data, description, err := suite.contextCreateHelper.createContextData(ctx, opts)
+
+	Expect(err).To(BeNil())
+	Expect(description).To(Equal("myResourceGroup@eastus"))
+	Expect(data).To(Equal(aciContext("1234", "myResourceGroup", "eastus")))
+}
+
+func (suite *ContextSuiteTest) TestErrorOnNonExistentResourceGroup() {
+	ctx := context.TODO()
+	opts := options("1234", "myResourceGroup")
+	notFoundError := errors.New(`Not Found: "myResourceGroup"`)
+	suite.mockResourceGroupHeper.On("GetGroup", ctx, "1234", "myResourceGroup").Return(resources.Group{}, notFoundError)
+
+	data, description, err := suite.contextCreateHelper.createContextData(ctx, opts)
+
+	Expect(data).To(BeNil())
+	Expect(description).To(Equal(""))
+	Expect(err.Error()).To(Equal("Could not find resource group \"myResourceGroup\": Not Found: \"myResourceGroup\""))
+}
+
+func (suite *ContextSuiteTest) TestCreateNewResourceGroup() {
+	ctx := context.TODO()
+	opts := options("1234", "")
+	suite.mockResourceGroupHeper.On("GetGroup", ctx, "1234", "myResourceGroup").Return(group("myResourceGroup", "eastus"), nil)
+
+	selectOptions := []string{"create a new resource group", "group1 (eastus)", "group2 (westeurope)"}
+	suite.mockUserSelector.On("userSelect", "Select a resource group", selectOptions).Return(0, nil)
+	suite.mockResourceGroupHeper.On("CreateOrUpdate", ctx, "1234", mock.AnythingOfType("string"), mock.AnythingOfType("resources.Group")).Return(group("newResourceGroup", "eastus"), nil)
+	suite.mockResourceGroupHeper.On("ListGroups", ctx, "1234").Return([]resources.Group{
+		group("group1", "eastus"),
+		group("group2", "westeurope"),
+	}, nil)
+
+	data, description, err := suite.contextCreateHelper.createContextData(ctx, opts)
+
+	Expect(err).To(BeNil())
+	Expect(description).To(Equal("newResourceGroup@eastus"))
+	Expect(data).To(Equal(aciContext("1234", "newResourceGroup", "eastus")))
+}
+
+func (suite *ContextSuiteTest) TestSelectExistingResourceGroup() {
+	ctx := context.TODO()
+	opts := options("1234", "")
+	selectOptions := []string{"create a new resource group", "group1 (eastus)", "group2 (westeurope)"}
+	suite.mockUserSelector.On("userSelect", "Select a resource group", selectOptions).Return(2, nil)
+	suite.mockResourceGroupHeper.On("ListGroups", ctx, "1234").Return([]resources.Group{
+		group("group1", "eastus"),
+		group("group2", "westeurope"),
+	}, nil)
+
+	data, description, err := suite.contextCreateHelper.createContextData(ctx, opts)
+
+	Expect(err).To(BeNil())
+	Expect(description).To(Equal("group2@westeurope"))
+	Expect(data).To(Equal(aciContext("1234", "group2", "westeurope")))
+}
+
+func (suite *ContextSuiteTest) TestSelectSingleSubscriptionIdAndExistingResourceGroup() {
+	ctx := context.TODO()
+	opts := options("", "")
+	suite.mockResourceGroupHeper.On("GetSubscriptionIDs", ctx).Return([]subscription.Model{subModel("123456", "Subscription1")}, nil)
+
+	selectOptions := []string{"create a new resource group", "group1 (eastus)", "group2 (westeurope)"}
+	suite.mockUserSelector.On("userSelect", "Select a resource group", selectOptions).Return(2, nil)
+	suite.mockResourceGroupHeper.On("ListGroups", ctx, "123456").Return([]resources.Group{
+		group("group1", "eastus"),
+		group("group2", "westeurope"),
+	}, nil)
+
+	data, description, err := suite.contextCreateHelper.createContextData(ctx, opts)
+
+	Expect(err).To(BeNil())
+	Expect(description).To(Equal("group2@westeurope"))
+	Expect(data).To(Equal(aciContext("123456", "group2", "westeurope")))
+}
+
+func (suite *ContextSuiteTest) TestSelectSubscriptionIdAndExistingResourceGroup() {
+	ctx := context.TODO()
+	opts := options("", "")
+	sub1 := subModel("1234", "Subscription1")
+	sub2 := subModel("5678", "Subscription2")
+
+	suite.mockResourceGroupHeper.On("GetSubscriptionIDs", ctx).Return([]subscription.Model{sub1, sub2}, nil)
+
+	selectOptions := []string{"Subscription1(1234)", "Subscription2(5678)"}
+	suite.mockUserSelector.On("userSelect", "Select a subscription ID", selectOptions).Return(1, nil)
+	selectOptions = []string{"create a new resource group", "group1 (eastus)", "group2 (westeurope)"}
+	suite.mockUserSelector.On("userSelect", "Select a resource group", selectOptions).Return(2, nil)
+	suite.mockResourceGroupHeper.On("ListGroups", ctx, "5678").Return([]resources.Group{
+		group("group1", "eastus"),
+		group("group2", "westeurope"),
+	}, nil)
+
+	data, description, err := suite.contextCreateHelper.createContextData(ctx, opts)
+
+	Expect(err).To(BeNil())
+	Expect(description).To(Equal("group2@westeurope"))
+	Expect(data).To(Equal(aciContext("5678", "group2", "westeurope")))
+}
+
+func subModel(subID string, display string) subscription.Model {
+	return subscription.Model{
+		SubscriptionID: to.StringPtr(subID),
+		DisplayName:    to.StringPtr(display),
+	}
+}
+
+func group(groupName string, location string) resources.Group {
+	return resources.Group{
+		Name:     to.StringPtr(groupName),
+		Location: to.StringPtr(location),
+	}
+}
+
+func aciContext(subscriptionID string, resourceGroupName string, location string) store.AciContext {
+	return store.AciContext{
+		SubscriptionID: subscriptionID,
+		Location:       location,
+		ResourceGroup:  resourceGroupName,
+	}
+}
+
+func options(subscriptionID string, resourceGroupName string) map[string]string {
+	return map[string]string{
+		"aciSubscriptionID": subscriptionID,
+		"aciResourceGroup":  resourceGroupName,
+	}
+}
+
+func TestContextSuite(t *testing.T) {
+	RegisterTestingT(t)
+	suite.Run(t, new(ContextSuiteTest))
+}
+
+type MockUserSelector struct {
+	mock.Mock
+}
+
+func (s *MockUserSelector) userSelect(message string, options []string) (int, error) {
+	args := s.Called(message, options)
+	return args.Int(0), args.Error(1)
+}
+
+type MockResourceGroupHelper struct {
+	mock.Mock
+}
+
+func (s *MockResourceGroupHelper) GetSubscriptionIDs(ctx context.Context) ([]subscription.Model, error) {
+	args := s.Called(ctx)
+	return args.Get(0).([]subscription.Model), args.Error(1)
+}
+
+func (s *MockResourceGroupHelper) ListGroups(ctx context.Context, subscriptionID string) ([]resources.Group, error) {
+	args := s.Called(ctx, subscriptionID)
+	return args.Get(0).([]resources.Group), args.Error(1)
+}
+
+func (s *MockResourceGroupHelper) GetGroup(ctx context.Context, subscriptionID string, groupName string) (resources.Group, error) {
+	args := s.Called(ctx, subscriptionID, groupName)
+	return args.Get(0).(resources.Group), args.Error(1)
+}
+
+func (s *MockResourceGroupHelper) CreateOrUpdate(ctx context.Context, subscriptionID string, resourceGroupName string, parameters resources.Group) (result resources.Group, err error) {
+	args := s.Called(ctx, subscriptionID, resourceGroupName, parameters)
+	return args.Get(0).(resources.Group), args.Error(1)
+}
+
+func (s *MockResourceGroupHelper) Delete(ctx context.Context, subscriptionID string, resourceGroupName string) (err error) {
+	args := s.Called(ctx, subscriptionID, resourceGroupName)
+	return args.Error(0)
+}

+ 105 - 0
azure/resourcegroup.go

@@ -0,0 +1,105 @@
+package azure
+
+import (
+	"context"
+
+	"github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/resources/mgmt/resources"
+	"github.com/Azure/azure-sdk-for-go/profiles/preview/preview/subscription/mgmt/subscription"
+	"github.com/pkg/errors"
+
+	"github.com/docker/api/azure/login"
+	"github.com/docker/api/errdefs"
+)
+
+// ACIResourceGroupHelper interface to manage resource groups and subscription IDs
+type ACIResourceGroupHelper interface {
+	GetSubscriptionIDs(ctx context.Context) ([]subscription.Model, error)
+	ListGroups(ctx context.Context, subscriptionID string) ([]resources.Group, error)
+	GetGroup(ctx context.Context, subscriptionID string, groupName string) (resources.Group, error)
+	CreateOrUpdate(ctx context.Context, subscriptionID string, resourceGroupName string, parameters resources.Group) (result resources.Group, err error)
+	Delete(ctx context.Context, subscriptionID string, resourceGroupName string) error
+}
+
+type aciResourceGroupHelperImpl struct {
+}
+
+// NewACIResourceGroupHelper create a new ACIResourceGroupHelper
+func NewACIResourceGroupHelper() ACIResourceGroupHelper {
+	return aciResourceGroupHelperImpl{}
+}
+
+// GetGroup get a resource group from its name
+func (mgt aciResourceGroupHelperImpl) GetGroup(ctx context.Context, subscriptionID string, groupName string) (resources.Group, error) {
+	gc := getGroupsClient(subscriptionID)
+	return gc.Get(ctx, groupName)
+}
+
+// ListGroups list resource groups
+func (mgt aciResourceGroupHelperImpl) ListGroups(ctx context.Context, subscriptionID string) ([]resources.Group, error) {
+	gc := getGroupsClient(subscriptionID)
+	groupResponse, err := gc.List(ctx, "", nil)
+	if err != nil {
+		return nil, err
+	}
+
+	groups := groupResponse.Values()
+	return groups, nil
+}
+
+// CreateOrUpdate create or update a resource group
+func (mgt aciResourceGroupHelperImpl) CreateOrUpdate(ctx context.Context, subscriptionID string, resourceGroupName string, parameters resources.Group) (result resources.Group, err error) {
+	gc := getGroupsClient(subscriptionID)
+	return gc.CreateOrUpdate(ctx, resourceGroupName, parameters)
+}
+
+// Delete deletes a resource group
+func (mgt aciResourceGroupHelperImpl) Delete(ctx context.Context, subscriptionID string, resourceGroupName string) (err error) {
+	gc := getGroupsClient(subscriptionID)
+	future, err := gc.Delete(ctx, resourceGroupName)
+	if err != nil {
+		return err
+	}
+	return future.WaitForCompletionRef(ctx, gc.Client)
+}
+
+// GetSubscriptionIDs Return available subscription IDs based on azure login
+func (mgt aciResourceGroupHelperImpl) GetSubscriptionIDs(ctx context.Context) ([]subscription.Model, error) {
+	c, err := getSubscriptionsClient()
+	if err != nil {
+		return nil, err
+	}
+	res, err := c.List(ctx)
+	if err != nil {
+		return nil, err
+	}
+	subs := res.Values()
+
+	if len(subs) == 0 {
+		return nil, errors.New("no subscriptions found")
+	}
+	for res.NotDone() {
+		err = res.NextWithContext(ctx)
+		if err != nil {
+			return nil, err
+		}
+		subs = append(subs, res.Values()...)
+	}
+	return subs, nil
+}
+
+func getSubscriptionsClient() (subscription.SubscriptionsClient, error) {
+	subc := subscription.NewSubscriptionsClient()
+	authorizer, err := login.NewAuthorizerFromLogin()
+	if err != nil {
+		return subscription.SubscriptionsClient{}, errors.Wrap(errdefs.ErrLoginFailed, err.Error())
+	}
+	subc.Authorizer = authorizer
+	return subc, nil
+}
+
+func getGroupsClient(subscriptionID string) resources.GroupsClient {
+	groupsClient := resources.NewGroupsClient(subscriptionID)
+	authorizer, _ := login.NewAuthorizerFromLogin()
+	groupsClient.Authorizer = authorizer
+	return groupsClient
+}

+ 36 - 8
cli/cmd/context/create.go

@@ -30,12 +30,17 @@ package context
 import (
 	"context"
 
+	"github.com/pkg/errors"
+
+	"github.com/docker/api/client"
+
 	"github.com/spf13/cobra"
 
 	"github.com/docker/api/context/store"
 )
 
-type createOpts struct {
+// AciCreateOpts Options for ACI context create
+type AciCreateOpts struct {
 	description       string
 	aciLocation       string
 	aciSubscriptionID string
@@ -43,7 +48,7 @@ type createOpts struct {
 }
 
 func createCommand() *cobra.Command {
-	var opts createOpts
+	var opts AciCreateOpts
 	cmd := &cobra.Command{
 		Use:   "create CONTEXT BACKEND [OPTIONS]",
 		Short: "Create a context",
@@ -61,13 +66,36 @@ func createCommand() *cobra.Command {
 	return cmd
 }
 
-func runCreate(ctx context.Context, opts createOpts, name string, contextType string) error {
+func runCreate(ctx context.Context, opts AciCreateOpts, name string, contextType string) error {
+	var description string
+	var contextData interface{}
+
 	switch contextType {
 	case "aci":
-		return createACIContext(ctx, name, opts)
-	default:
-		s := store.ContextStore(ctx)
-		// TODO: we need to implement different contexts for known backends
-		return s.Create(name, contextType, opts.description, store.ExampleContext{})
+		cs, err := client.GetCloudService(ctx, "aci")
+		if err != nil {
+			return errors.Wrap(err, "cannot connect to backend")
+		}
+		params := map[string]string{
+			"aciSubscriptionId": opts.aciSubscriptionID,
+			"aciResourceGroup":  opts.aciResourceGroup,
+			"aciLocation":       opts.aciLocation,
+			"description":       opts.description,
+		}
+		contextData, description, err = cs.CreateContextData(ctx, params)
+		if err != nil {
+			return errors.Wrap(err, "cannot create context")
+		}
+	default: // TODO: we need to implement different contexts for known backends
+		description = opts.description
+		contextData = store.ExampleContext{}
 	}
+
+	s := store.ContextStore(ctx)
+	return s.Create(
+		name,
+		contextType,
+		description,
+		contextData,
+	)
 }

+ 0 - 55
cli/cmd/context/createaci.go

@@ -1,55 +0,0 @@
-/*
-	Copyright (c) 2020 Docker Inc.
-
-	Permission is hereby granted, free of charge, to any person
-	obtaining a copy of this software and associated documentation
-	files (the "Software"), to deal in the Software without
-	restriction, including without limitation the rights to use, copy,
-	modify, merge, publish, distribute, sublicense, and/or sell copies
-	of the Software, and to permit persons to whom the Software is
-	furnished to do so, subject to the following conditions:
-
-	The above copyright notice and this permission notice shall be
-	included in all copies or substantial portions of the Software.
-
-	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-	EXPRESS OR IMPLIED,
-	INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-	IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-	HOLDERS BE LIABLE FOR ANY CLAIM,
-	DAMAGES OR OTHER LIABILITY,
-	WHETHER IN AN ACTION OF CONTRACT,
-	TORT OR OTHERWISE,
-	ARISING FROM, OUT OF OR IN CONNECTION WITH
-	THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-*/
-
-package context
-
-import (
-	"context"
-	"fmt"
-
-	"github.com/docker/api/context/store"
-)
-
-func createACIContext(ctx context.Context, name string, opts createOpts) error {
-	s := store.ContextStore(ctx)
-
-	description := fmt.Sprintf("%s@%s", opts.aciResourceGroup, opts.aciLocation)
-	if opts.description != "" {
-		description = fmt.Sprintf("%s (%s)", opts.description, description)
-	}
-
-	return s.Create(
-		name,
-		store.AciContextType,
-		description,
-		store.AciContext{
-			SubscriptionID: opts.aciSubscriptionID,
-			Location:       opts.aciLocation,
-			ResourceGroup:  opts.aciResourceGroup,
-		},
-	)
-}

+ 6 - 0
context/cloud/api.go

@@ -10,6 +10,8 @@ import (
 type Service interface {
 	// Login login to cloud provider
 	Login(ctx context.Context, params map[string]string) error
+	// Login login to cloud provider
+	CreateContextData(ctx context.Context, params map[string]string) (contextData interface{}, description string, err error)
 }
 
 // NotImplementedCloudService to use for backend that don't provide cloud services
@@ -23,3 +25,7 @@ type notImplementedCloudService struct {
 func (cs notImplementedCloudService) Login(ctx context.Context, params map[string]string) error {
 	return errdefs.ErrNotImplemented
 }
+
+func (cs notImplementedCloudService) CreateContextData(ctx context.Context, params map[string]string) (interface{}, string, error) {
+	return nil, "", errdefs.ErrNotImplemented
+}

+ 3 - 0
go.mod

@@ -3,6 +3,7 @@ module github.com/docker/api
 go 1.14
 
 require (
+	github.com/AlecAivazis/survey/v2 v2.0.7
 	github.com/Azure/azure-sdk-for-go v42.0.0+incompatible
 	github.com/Azure/azure-storage-file-go v0.7.0
 	github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
@@ -24,6 +25,7 @@ require (
 	github.com/gobwas/pool v0.2.0 // indirect
 	github.com/gobwas/ws v1.0.3
 	github.com/golang/protobuf v1.4.1
+	github.com/google/uuid v1.1.1
 	github.com/gorilla/mux v1.7.4 // indirect
 	github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
 	github.com/hashicorp/go-multierror v1.1.0
@@ -38,6 +40,7 @@ require (
 	github.com/spf13/cobra v1.0.0
 	github.com/spf13/pflag v1.0.5
 	github.com/stretchr/testify v1.5.1
+	github.com/tj/survey v2.0.6+incompatible
 	golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0
 	golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be
 	golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 // indirect

+ 24 - 0
go.sum

@@ -1,4 +1,6 @@
 cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/AlecAivazis/survey/v2 v2.0.7 h1:+f825XHLse/hWd2tE/V5df04WFGimk34Eyg/z35w/rc=
+github.com/AlecAivazis/survey/v2 v2.0.7/go.mod h1:mlizQTaPjnR4jcpwRSaSlkbsRfYFEyKgLQvYTzxxiHA=
 github.com/Azure/azure-pipeline-go v0.2.1 h1:OLBdZJ3yvOn2MezlWvbrBMTEUQC72zAftRZOMdj5HYo=
 github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4=
 github.com/Azure/azure-sdk-for-go v42.0.0+incompatible h1:yz6sFf5bHZ+gEOQVuK5JhPqTTAmv+OvSLSaqgzqaCwY=
@@ -34,6 +36,8 @@ github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbt
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
 github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
+github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
+github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
@@ -126,6 +130,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
 github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
 github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
 github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
@@ -138,6 +144,8 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv
 github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
 github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
+github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
@@ -148,6 +156,8 @@ github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
 github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
@@ -158,16 +168,24 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.5 h1:hyz3dwM5QLc1Rfoz4FuWJQG5BN7tc6K1MndAUnGpQr4=
 github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
+github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149 h1:HfxbT6/JcvIljmERptWhwa8XzP7H3T+Z2N26gTsaDaA=
 github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
+github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw=
 github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
+github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
@@ -245,11 +263,14 @@ github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/y
 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.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/tj/survey v2.0.6+incompatible h1:tVgc1+kmYX9R0CEoHaTczapjdc4GaJla0VAB7O+w1So=
+github.com/tj/survey v2.0.6+incompatible/go.mod h1:vLPzQYAOKWgXqr5jV9luQXJuoXKHOg0ltn5FEw1Nz0c=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
@@ -266,6 +287,7 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
 golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -298,9 +320,11 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

+ 15 - 11
tests/aci-e2e/e2e-aci_test.go

@@ -7,6 +7,8 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/docker/api/azure"
+
 	"github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/resources/mgmt/resources"
 	azure_storage "github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/storage/mgmt/storage"
 	"github.com/Azure/azure-storage-file-go/azfile"
@@ -15,7 +17,6 @@ import (
 	log "github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/suite"
 
-	"github.com/docker/api/azure"
 	"github.com/docker/api/context/store"
 	"github.com/docker/api/tests/aci-e2e/storage"
 	. "github.com/docker/api/tests/framework"
@@ -64,9 +65,10 @@ func (s *E2eACISuite) TestContextDefault() {
 func (s *E2eACISuite) TestACIBackend() {
 	It("creates a new aci context for tests", func() {
 		setupTestResourceGroup(resourceGroupName)
-		var err error
-		subscriptionID, err = azure.GetSubscriptionID(context.TODO())
+		helper := azure.NewACIResourceGroupHelper()
+		models, err := helper.GetSubscriptionIDs(context.TODO())
 		Expect(err).To(BeNil())
+		subscriptionID = *models[0].SubscriptionID
 
 		s.NewDockerCommand("context", "create", contextName, "aci", "--aci-subscription-id", subscriptionID, "--aci-resource-group", resourceGroupName, "--aci-location", location).ExecOrDie()
 		// Expect(output).To(ContainSubstring("ACI context acitest created"))
@@ -173,13 +175,14 @@ func (s *E2eACISuite) TestACIBackend() {
 }
 
 const (
-	testStorageAccountName = "dockertestaccountname"
-	testShareName          = "dockertestsharename"
+	testStorageAccountName = "dockertestaccount"
+	testShareName          = "dockertestshare"
 	testFileContent        = "Volume mounted with success!"
 	testFileName           = "index.html"
 )
 
 func createStorageAccount(aciContext store.AciContext, accountName string) azure_storage.Account {
+	log.Println("Creating storage account " + accountName)
 	storageAccount, err := storage.CreateStorageAccount(context.TODO(), aciContext, accountName)
 	Expect(err).To(BeNil())
 	Expect(*storageAccount.Name).To(Equal(accountName))
@@ -196,6 +199,7 @@ func getStorageKeys(aciContext store.AciContext, storageAccountName string) []az
 }
 
 func deleteStorageAccount(aciContext store.AciContext) {
+	log.Println("Deleting storage account " + testStorageAccountName)
 	_, err := storage.DeleteStorageAccount(context.TODO(), aciContext, testStorageAccountName)
 	Expect(err).To(BeNil())
 }
@@ -228,10 +232,10 @@ func TestE2eACI(t *testing.T) {
 func setupTestResourceGroup(groupName string) {
 	log.Println("Creating resource group " + resourceGroupName)
 	ctx := context.TODO()
-	subscriptionID, err := azure.GetSubscriptionID(ctx)
+	helper := azure.NewACIResourceGroupHelper()
+	models, err := helper.GetSubscriptionIDs(ctx)
 	Expect(err).To(BeNil())
-	gc := azure.GetGroupsClient(subscriptionID)
-	_, err = gc.CreateOrUpdate(ctx, groupName, resources.Group{
+	_, err = helper.CreateOrUpdate(ctx, *models[0].SubscriptionID, groupName, resources.Group{
 		Location: to.StringPtr(location),
 	})
 	Expect(err).To(BeNil())
@@ -240,9 +244,9 @@ func setupTestResourceGroup(groupName string) {
 func deleteResourceGroup(groupName string) {
 	log.Println("Deleting resource group " + resourceGroupName)
 	ctx := context.TODO()
-	subscriptionID, err := azure.GetSubscriptionID(ctx)
+	helper := azure.NewACIResourceGroupHelper()
+	models, err := helper.GetSubscriptionIDs(ctx)
 	Expect(err).To(BeNil())
-	gc := azure.GetGroupsClient(subscriptionID)
-	_, err = gc.Delete(ctx, groupName)
+	err = helper.Delete(ctx, *models[0].SubscriptionID, groupName)
 	Expect(err).To(BeNil())
 }

+ 2 - 1
tests/aci-e2e/storage/storage.go

@@ -2,6 +2,7 @@ package storage
 
 import (
 	"context"
+	"errors"
 
 	"github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/storage/mgmt/storage"
 	"github.com/Azure/go-autorest/autorest"
@@ -25,7 +26,7 @@ func CreateStorageAccount(ctx context.Context, aciContext store.AciContext, acco
 		return storage.Account{}, err
 	}
 	if !*result.NameAvailable {
-		return storage.Account{}, err
+		return storage.Account{}, errors.New("storage account name already exists" + accountName)
 	}
 
 	future, err := storageAccountsClient.Create(