Pārlūkot izejas kodu

Merge pull request #593 from docker/volume_create

ACI Volume create
Guillaume Tardif 5 gadi atpakaļ
vecāks
revīzija
c7a456ab83

+ 28 - 0
aci/aci.go

@@ -129,6 +129,34 @@ func getACIContainerGroup(ctx context.Context, aciContext store.AciContext, cont
 	return containerGroupsClient.Get(ctx, aciContext.ResourceGroup, containerGroupName)
 }
 
+func getACIContainerGroups(ctx context.Context, subscriptionID string, resourceGroup string) ([]containerinstance.ContainerGroup, error) {
+	groupsClient, err := login.NewContainerGroupsClient(subscriptionID)
+	if err != nil {
+		return nil, err
+	}
+	var containerGroups []containerinstance.ContainerGroup
+	result, err := groupsClient.ListByResourceGroup(ctx, resourceGroup)
+	if err != nil {
+		return nil, err
+	}
+
+	for result.NotDone() {
+		containerGroups = append(containerGroups, result.Values()...)
+		if err := result.NextWithContext(ctx); err != nil {
+			return nil, err
+		}
+	}
+	var groups []containerinstance.ContainerGroup
+	for _, group := range containerGroups {
+		group, err := groupsClient.Get(ctx, resourceGroup, *group.Name)
+		if err != nil {
+			return nil, err
+		}
+		groups = append(groups, group)
+	}
+	return groups, nil
+}
+
 func deleteACIContainerGroup(ctx context.Context, aciContext store.AciContext, containerGroupName string) (containerinstance.ContainerGroup, error) {
 	containerGroupsClient, err := login.NewContainerGroupsClient(aciContext.SubscriptionID)
 	if err != nil {

+ 8 - 368
aci/backend.go

@@ -18,35 +18,29 @@ package aci
 
 import (
 	"context"
-	"fmt"
-	"io"
-	"net/http"
-	"strconv"
 	"strings"
 
 	"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"
-	"github.com/compose-spec/compose-go/types"
 	"github.com/pkg/errors"
-	"github.com/sirupsen/logrus"
 
 	"github.com/docker/compose-cli/aci/convert"
 	"github.com/docker/compose-cli/aci/login"
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/containers"
 	"github.com/docker/compose-cli/api/secrets"
+	"github.com/docker/compose-cli/api/volumes"
 	"github.com/docker/compose-cli/backend"
 	apicontext "github.com/docker/compose-cli/context"
 	"github.com/docker/compose-cli/context/cloud"
 	"github.com/docker/compose-cli/context/store"
-	"github.com/docker/compose-cli/errdefs"
 )
 
 const (
 	backendType               = store.AciContextType
 	singleContainerTag        = "docker-single-container"
 	composeContainerTag       = "docker-compose-application"
+	dockerVolumeTag           = "docker-volume"
 	composeContainerSeparator = "_"
 )
 
@@ -109,12 +103,16 @@ func getAciAPIService(aciCtx store.AciContext) *aciAPIService {
 		aciComposeService: &aciComposeService{
 			ctx: aciCtx,
 		},
+		aciVolumeService: &aciVolumeService{
+			aciContext: aciCtx,
+		},
 	}
 }
 
 type aciAPIService struct {
 	*aciContainerService
 	*aciComposeService
+	*aciVolumeService
 }
 
 func (a *aciAPIService) ContainerService() containers.Service {
@@ -129,58 +127,8 @@ func (a *aciAPIService) SecretsService() secrets.Service {
 	return nil
 }
 
-type aciContainerService struct {
-	ctx store.AciContext
-}
-
-func (cs *aciContainerService) List(ctx context.Context, all bool) ([]containers.Container, error) {
-	containerGroups, err := getContainerGroups(ctx, cs.ctx.SubscriptionID, cs.ctx.ResourceGroup)
-	if err != nil {
-		return []containers.Container{}, err
-	}
-	var res []containers.Container
-	for _, group := range containerGroups {
-		if group.Containers == nil || len(*group.Containers) < 1 {
-			return []containers.Container{}, fmt.Errorf("no containers found in ACI container group %s", *group.Name)
-		}
-
-		for _, container := range *group.Containers {
-			if isContainerVisible(container, group, all) {
-				continue
-			}
-			c := convert.ContainerGroupToContainer(getContainerID(group, container), group, container)
-			res = append(res, c)
-		}
-	}
-	return res, nil
-}
-
-func getContainerGroups(ctx context.Context, subscriptionID string, resourceGroup string) ([]containerinstance.ContainerGroup, error) {
-	groupsClient, err := login.NewContainerGroupsClient(subscriptionID)
-	if err != nil {
-		return nil, err
-	}
-	var containerGroups []containerinstance.ContainerGroup
-	result, err := groupsClient.ListByResourceGroup(ctx, resourceGroup)
-	if err != nil {
-		return []containerinstance.ContainerGroup{}, err
-	}
-
-	for result.NotDone() {
-		containerGroups = append(containerGroups, result.Values()...)
-		if err := result.NextWithContext(ctx); err != nil {
-			return []containerinstance.ContainerGroup{}, err
-		}
-	}
-	var groups []containerinstance.ContainerGroup
-	for _, group := range containerGroups {
-		group, err := groupsClient.Get(ctx, resourceGroup, *group.Name)
-		if err != nil {
-			return []containerinstance.ContainerGroup{}, err
-		}
-		groups = append(groups, group)
-	}
-	return groups, nil
+func (a *aciAPIService) VolumeService() volumes.Service {
+	return a.aciVolumeService
 }
 
 func getContainerID(group containerinstance.ContainerGroup, container containerinstance.Container) string {
@@ -195,26 +143,6 @@ func isContainerVisible(container containerinstance.Container, group containerin
 	return *container.Name == convert.ComposeDNSSidecarName || (!showAll && convert.GetStatus(container, group) != convert.StatusRunning)
 }
 
-func (cs *aciContainerService) Run(ctx context.Context, r containers.ContainerConfig) error {
-	if strings.Contains(r.ID, composeContainerSeparator) {
-		return errors.New(fmt.Sprintf("invalid container name. ACI container name cannot include %q", composeContainerSeparator))
-	}
-
-	project, err := convert.ContainerToComposeProject(r)
-	if err != nil {
-		return err
-	}
-
-	logrus.Debugf("Running container %q with name %q\n", r.Image, r.ID)
-	groupDefinition, err := convert.ToContainerGroup(ctx, cs.ctx, project)
-	if err != nil {
-		return err
-	}
-	addTag(&groupDefinition, singleContainerTag)
-
-	return createACIContainers(ctx, cs.ctx, groupDefinition)
-}
-
 func addTag(groupDefinition *containerinstance.ContainerGroup, tagName string) {
 	if groupDefinition.Tags == nil {
 		groupDefinition.Tags = make(map[string]*string, 1)
@@ -222,53 +150,6 @@ func addTag(groupDefinition *containerinstance.ContainerGroup, tagName string) {
 	groupDefinition.Tags[tagName] = to.StringPtr(tagName)
 }
 
-func (cs *aciContainerService) Start(ctx context.Context, containerID string) error {
-	groupName, containerName := getGroupAndContainerName(containerID)
-	if groupName != containerID {
-		msg := "cannot start specified service %q from compose application %q, you can update and restart the entire compose app with docker compose up --project-name %s"
-		return errors.New(fmt.Sprintf(msg, containerName, groupName, groupName))
-	}
-
-	containerGroupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
-	if err != nil {
-		return err
-	}
-
-	future, err := containerGroupsClient.Start(ctx, cs.ctx.ResourceGroup, containerName)
-	if err != nil {
-		var aerr autorest.DetailedError
-		if ok := errors.As(err, &aerr); ok {
-			if aerr.StatusCode == http.StatusNotFound {
-				return errdefs.ErrNotFound
-			}
-		}
-		return err
-	}
-
-	return future.WaitForCompletionRef(ctx, containerGroupsClient.Client)
-}
-
-func (cs *aciContainerService) Stop(ctx context.Context, containerID string, timeout *uint32) error {
-	if timeout != nil && *timeout != uint32(0) {
-		return errors.Errorf("ACI integration does not support setting a timeout to stop a container before killing it.")
-	}
-	groupName, containerName := getGroupAndContainerName(containerID)
-	if groupName != containerID {
-		msg := "cannot stop service %q from compose application %q, you can stop the entire compose app with docker stop %s"
-		return errors.New(fmt.Sprintf(msg, containerName, groupName, groupName))
-	}
-	return stopACIContainerGroup(ctx, cs.ctx, groupName)
-}
-
-func (cs *aciContainerService) Kill(ctx context.Context, containerID string, _ string) error {
-	groupName, containerName := getGroupAndContainerName(containerID)
-	if groupName != containerID {
-		msg := "cannot kill service %q from compose application %q, you can kill the entire compose app with docker kill %s"
-		return errors.New(fmt.Sprintf(msg, containerName, groupName, groupName))
-	}
-	return stopACIContainerGroup(ctx, cs.ctx, groupName) // As ACI doesn't have a kill command, we are using the stop implementation instead
-}
-
 func getGroupAndContainerName(containerID string) (string, string) {
 	tokens := strings.Split(containerID, composeContainerSeparator)
 	groupName := tokens[0]
@@ -279,244 +160,3 @@ func getGroupAndContainerName(containerID string) (string, string) {
 	}
 	return groupName, containerName
 }
-
-func (cs *aciContainerService) Exec(ctx context.Context, name string, request containers.ExecRequest) error {
-	err := verifyExecCommand(request.Command)
-	if err != nil {
-		return err
-	}
-	groupName, containerAciName := getGroupAndContainerName(name)
-	containerExecResponse, err := execACIContainer(ctx, cs.ctx, request.Command, groupName, containerAciName)
-	if err != nil {
-		return err
-	}
-
-	return exec(
-		context.Background(),
-		*containerExecResponse.WebSocketURI,
-		*containerExecResponse.Password,
-		request,
-	)
-}
-
-func verifyExecCommand(command string) error {
-	tokens := strings.Split(command, " ")
-	if len(tokens) > 1 {
-		return errors.New("ACI exec command does not accept arguments to the command. " +
-			"Only the binary should be specified")
-	}
-	return nil
-}
-
-func (cs *aciContainerService) Logs(ctx context.Context, containerName string, req containers.LogsRequest) error {
-	groupName, containerAciName := getGroupAndContainerName(containerName)
-	var tail *int32
-
-	if req.Follow {
-		return streamLogs(ctx, cs.ctx, groupName, containerAciName, req)
-	}
-
-	if req.Tail != "all" {
-		reqTail, err := strconv.Atoi(req.Tail)
-		if err != nil {
-			return err
-		}
-		i32 := int32(reqTail)
-		tail = &i32
-	}
-
-	logs, err := getACIContainerLogs(ctx, cs.ctx, groupName, containerAciName, tail)
-	if err != nil {
-		return err
-	}
-
-	_, err = fmt.Fprint(req.Writer, logs)
-	return err
-}
-
-func (cs *aciContainerService) Delete(ctx context.Context, containerID string, request containers.DeleteRequest) error {
-	groupName, containerName := getGroupAndContainerName(containerID)
-	if groupName != containerID {
-		msg := "cannot delete service %q from compose application %q, you can delete the entire compose app with docker compose down --project-name %s"
-		return errors.New(fmt.Sprintf(msg, containerName, groupName, groupName))
-	}
-
-	if !request.Force {
-		containerGroupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
-		if err != nil {
-			return err
-		}
-
-		cg, err := containerGroupsClient.Get(ctx, cs.ctx.ResourceGroup, groupName)
-		if err != nil {
-			if cg.StatusCode == http.StatusNotFound {
-				return errdefs.ErrNotFound
-			}
-			return err
-		}
-
-		for _, container := range *cg.Containers {
-			status := convert.GetStatus(container, cg)
-
-			if status == convert.StatusRunning {
-				return errdefs.ErrForbidden
-			}
-		}
-	}
-
-	cg, err := deleteACIContainerGroup(ctx, cs.ctx, groupName)
-	// Delete returns `StatusNoContent` if the group is not found
-	if cg.StatusCode == http.StatusNoContent {
-		return errdefs.ErrNotFound
-	}
-	if err != nil {
-		return err
-	}
-
-	return err
-}
-
-func (cs *aciContainerService) Inspect(ctx context.Context, containerID string) (containers.Container, error) {
-	groupName, containerName := getGroupAndContainerName(containerID)
-
-	cg, err := getACIContainerGroup(ctx, cs.ctx, groupName)
-	if err != nil {
-		return containers.Container{}, err
-	}
-	if cg.StatusCode == http.StatusNoContent {
-		return containers.Container{}, errdefs.ErrNotFound
-	}
-
-	var cc containerinstance.Container
-	var found = false
-	for _, c := range *cg.Containers {
-		if to.String(c.Name) == containerName {
-			cc = c
-			found = true
-			break
-		}
-	}
-	if !found {
-		return containers.Container{}, errdefs.ErrNotFound
-	}
-
-	return convert.ContainerGroupToContainer(containerID, cg, cc), nil
-}
-
-type aciComposeService struct {
-	ctx store.AciContext
-}
-
-func (cs *aciComposeService) Up(ctx context.Context, project *types.Project) error {
-	logrus.Debugf("Up on project with name %q\n", project.Name)
-	groupDefinition, err := convert.ToContainerGroup(ctx, cs.ctx, *project)
-	addTag(&groupDefinition, composeContainerTag)
-
-	if err != nil {
-		return err
-	}
-	return createOrUpdateACIContainers(ctx, cs.ctx, groupDefinition)
-}
-
-func (cs *aciComposeService) Down(ctx context.Context, project string) error {
-	logrus.Debugf("Down on project with name %q\n", project)
-
-	cg, err := deleteACIContainerGroup(ctx, cs.ctx, project)
-	if err != nil {
-		return err
-	}
-	if cg.StatusCode == http.StatusNoContent {
-		return errdefs.ErrNotFound
-	}
-
-	return err
-}
-
-func (cs *aciComposeService) Ps(ctx context.Context, project string) ([]compose.ServiceStatus, error) {
-	groupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
-	if err != nil {
-		return nil, err
-	}
-
-	group, err := groupsClient.Get(ctx, cs.ctx.ResourceGroup, project)
-	if err != nil {
-		return []compose.ServiceStatus{}, err
-	}
-
-	if group.Containers == nil || len(*group.Containers) < 1 {
-		return []compose.ServiceStatus{}, fmt.Errorf("no containers found in ACI container group %s", project)
-	}
-
-	res := []compose.ServiceStatus{}
-	for _, container := range *group.Containers {
-		if isContainerVisible(container, group, false) {
-			continue
-		}
-		res = append(res, convert.ContainerGroupToServiceStatus(getContainerID(group, container), group, container))
-	}
-	return res, nil
-}
-
-func (cs *aciComposeService) List(ctx context.Context, project string) ([]compose.Stack, error) {
-	containerGroups, err := getContainerGroups(ctx, cs.ctx.SubscriptionID, cs.ctx.ResourceGroup)
-	if err != nil {
-		return []compose.Stack{}, err
-	}
-
-	stacks := []compose.Stack{}
-	for _, group := range containerGroups {
-		if _, found := group.Tags[composeContainerTag]; !found {
-			continue
-		}
-		if project != "" && *group.Name != project {
-			continue
-		}
-		state := compose.RUNNING
-		for _, container := range *group.ContainerGroupProperties.Containers {
-			containerState := convert.GetStatus(container, group)
-			if containerState != compose.RUNNING {
-				state = containerState
-				break
-			}
-		}
-		stacks = append(stacks, compose.Stack{
-			ID:     *group.ID,
-			Name:   *group.Name,
-			Status: state,
-		})
-	}
-	return stacks, nil
-}
-
-func (cs *aciComposeService) Logs(ctx context.Context, project string, w io.Writer) error {
-	return errdefs.ErrNotImplemented
-}
-
-func (cs *aciComposeService) Convert(ctx context.Context, project *types.Project) ([]byte, error) {
-	return nil, errdefs.ErrNotImplemented
-}
-
-type aciCloudService struct {
-	loginService login.AzureLoginServiceAPI
-}
-
-func (cs *aciCloudService) Login(ctx context.Context, params interface{}) error {
-	opts, ok := params.(LoginParams)
-	if !ok {
-		return errors.New("Could not read azure LoginParams struct from generic parameter")
-	}
-	if opts.ClientID != "" {
-		return cs.loginService.LoginServicePrincipal(opts.ClientID, opts.ClientSecret, opts.TenantID)
-	}
-	return cs.loginService.Login(ctx, opts.TenantID)
-}
-
-func (cs *aciCloudService) Logout(ctx context.Context) error {
-	return cs.loginService.Logout(ctx)
-}
-
-func (cs *aciCloudService) CreateContextData(ctx context.Context, params interface{}) (interface{}, string, error) {
-	contextHelper := newContextCreateHelper()
-	createOpts := params.(ContextParams)
-	return contextHelper.createContextData(ctx, createOpts)
-}

+ 50 - 0
aci/cloud.go

@@ -0,0 +1,50 @@
+/*
+   Copyright 2020 Docker, Inc.
+
+   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 aci
+
+import (
+	"context"
+
+	"github.com/pkg/errors"
+
+	"github.com/docker/compose-cli/aci/login"
+)
+
+type aciCloudService struct {
+	loginService login.AzureLoginServiceAPI
+}
+
+func (cs *aciCloudService) Login(ctx context.Context, params interface{}) error {
+	opts, ok := params.(LoginParams)
+	if !ok {
+		return errors.New("could not read Azure LoginParams struct from generic parameter")
+	}
+	if opts.ClientID != "" {
+		return cs.loginService.LoginServicePrincipal(opts.ClientID, opts.ClientSecret, opts.TenantID)
+	}
+	return cs.loginService.Login(ctx, opts.TenantID)
+}
+
+func (cs *aciCloudService) Logout(ctx context.Context) error {
+	return cs.loginService.Logout(ctx)
+}
+
+func (cs *aciCloudService) CreateContextData(ctx context.Context, params interface{}) (interface{}, string, error) {
+	contextHelper := newContextCreateHelper()
+	createOpts := params.(ContextParams)
+	return contextHelper.createContextData(ctx, createOpts)
+}

+ 126 - 0
aci/compose.go

@@ -0,0 +1,126 @@
+/*
+   Copyright 2020 Docker, Inc.
+
+   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 aci
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"net/http"
+
+	"github.com/compose-spec/compose-go/types"
+	"github.com/sirupsen/logrus"
+
+	"github.com/docker/compose-cli/aci/convert"
+	"github.com/docker/compose-cli/aci/login"
+	"github.com/docker/compose-cli/api/compose"
+	"github.com/docker/compose-cli/context/store"
+	"github.com/docker/compose-cli/errdefs"
+)
+
+type aciComposeService struct {
+	ctx store.AciContext
+}
+
+func (cs *aciComposeService) Up(ctx context.Context, project *types.Project) error {
+	logrus.Debugf("Up on project with name %q", project.Name)
+	groupDefinition, err := convert.ToContainerGroup(ctx, cs.ctx, *project)
+	addTag(&groupDefinition, composeContainerTag)
+
+	if err != nil {
+		return err
+	}
+	return createOrUpdateACIContainers(ctx, cs.ctx, groupDefinition)
+}
+
+func (cs *aciComposeService) Down(ctx context.Context, project string) error {
+	logrus.Debugf("Down on project with name %q", project)
+
+	cg, err := deleteACIContainerGroup(ctx, cs.ctx, project)
+	if err != nil {
+		return err
+	}
+	if cg.StatusCode == http.StatusNoContent {
+		return errdefs.ErrNotFound
+	}
+
+	return err
+}
+
+func (cs *aciComposeService) Ps(ctx context.Context, project string) ([]compose.ServiceStatus, error) {
+	groupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
+	if err != nil {
+		return nil, err
+	}
+
+	group, err := groupsClient.Get(ctx, cs.ctx.ResourceGroup, project)
+	if err != nil {
+		return nil, err
+	}
+
+	if group.Containers == nil || len(*group.Containers) == 0 {
+		return nil, fmt.Errorf("no containers found in ACI container group %s", project)
+	}
+
+	res := []compose.ServiceStatus{}
+	for _, container := range *group.Containers {
+		if isContainerVisible(container, group, false) {
+			continue
+		}
+		res = append(res, convert.ContainerGroupToServiceStatus(getContainerID(group, container), group, container))
+	}
+	return res, nil
+}
+
+func (cs *aciComposeService) List(ctx context.Context, project string) ([]compose.Stack, error) {
+	containerGroups, err := getACIContainerGroups(ctx, cs.ctx.SubscriptionID, cs.ctx.ResourceGroup)
+	if err != nil {
+		return nil, err
+	}
+
+	stacks := []compose.Stack{}
+	for _, group := range containerGroups {
+		if _, found := group.Tags[composeContainerTag]; !found {
+			continue
+		}
+		if project != "" && *group.Name != project {
+			continue
+		}
+		state := compose.RUNNING
+		for _, container := range *group.ContainerGroupProperties.Containers {
+			containerState := convert.GetStatus(container, group)
+			if containerState != compose.RUNNING {
+				state = containerState
+				break
+			}
+		}
+		stacks = append(stacks, compose.Stack{
+			ID:     *group.ID,
+			Name:   *group.Name,
+			Status: state,
+		})
+	}
+	return stacks, nil
+}
+
+func (cs *aciComposeService) Logs(ctx context.Context, project string, w io.Writer) error {
+	return errdefs.ErrNotImplemented
+}
+
+func (cs *aciComposeService) Convert(ctx context.Context, project *types.Project) ([]byte, error) {
+	return nil, errdefs.ErrNotImplemented
+}

+ 253 - 0
aci/containers.go

@@ -0,0 +1,253 @@
+/*
+   Copyright 2020 Docker, Inc.
+
+   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 aci
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"strconv"
+	"strings"
+
+	"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"
+	"github.com/pkg/errors"
+	"github.com/sirupsen/logrus"
+
+	"github.com/docker/compose-cli/aci/convert"
+	"github.com/docker/compose-cli/aci/login"
+	"github.com/docker/compose-cli/api/containers"
+	"github.com/docker/compose-cli/context/store"
+	"github.com/docker/compose-cli/errdefs"
+)
+
+type aciContainerService struct {
+	ctx store.AciContext
+}
+
+func (cs *aciContainerService) List(ctx context.Context, all bool) ([]containers.Container, error) {
+	containerGroups, err := getACIContainerGroups(ctx, cs.ctx.SubscriptionID, cs.ctx.ResourceGroup)
+	if err != nil {
+		return nil, err
+	}
+	var res []containers.Container
+	for _, group := range containerGroups {
+		if group.Containers == nil || len(*group.Containers) == 0 {
+			return nil, fmt.Errorf("no containers found in ACI container group %s", *group.Name)
+		}
+
+		for _, container := range *group.Containers {
+			if isContainerVisible(container, group, all) {
+				continue
+			}
+			c := convert.ContainerGroupToContainer(getContainerID(group, container), group, container)
+			res = append(res, c)
+		}
+	}
+	return res, nil
+}
+
+func (cs *aciContainerService) Run(ctx context.Context, r containers.ContainerConfig) error {
+	if strings.Contains(r.ID, composeContainerSeparator) {
+		return fmt.Errorf("invalid container name. ACI container name cannot include %q", composeContainerSeparator)
+	}
+
+	project, err := convert.ContainerToComposeProject(r)
+	if err != nil {
+		return err
+	}
+
+	logrus.Debugf("Running container %q with name %q", r.Image, r.ID)
+	groupDefinition, err := convert.ToContainerGroup(ctx, cs.ctx, project)
+	if err != nil {
+		return err
+	}
+	addTag(&groupDefinition, singleContainerTag)
+
+	return createACIContainers(ctx, cs.ctx, groupDefinition)
+}
+
+func (cs *aciContainerService) Start(ctx context.Context, containerID string) error {
+	groupName, containerName := getGroupAndContainerName(containerID)
+	if groupName != containerID {
+		msg := "cannot start specified service %q from compose application %q, you can update and restart the entire compose app with docker compose up --project-name %s"
+		return fmt.Errorf(msg, containerName, groupName, groupName)
+	}
+
+	containerGroupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
+	if err != nil {
+		return err
+	}
+
+	future, err := containerGroupsClient.Start(ctx, cs.ctx.ResourceGroup, containerName)
+	if err != nil {
+		var aerr autorest.DetailedError
+		if ok := errors.As(err, &aerr); ok {
+			if aerr.StatusCode == http.StatusNotFound {
+				return errdefs.ErrNotFound
+			}
+		}
+		return err
+	}
+
+	return future.WaitForCompletionRef(ctx, containerGroupsClient.Client)
+}
+
+func (cs *aciContainerService) Stop(ctx context.Context, containerID string, timeout *uint32) error {
+	if timeout != nil && *timeout != uint32(0) {
+		return fmt.Errorf("the ACI integration does not support setting a timeout to stop a container before killing it")
+	}
+	groupName, containerName := getGroupAndContainerName(containerID)
+	if groupName != containerID {
+		msg := "cannot stop service %q from compose application %q, you can stop the entire compose app with docker stop %s"
+		return fmt.Errorf(msg, containerName, groupName, groupName)
+	}
+	return stopACIContainerGroup(ctx, cs.ctx, groupName)
+}
+
+func (cs *aciContainerService) Kill(ctx context.Context, containerID string, _ string) error {
+	groupName, containerName := getGroupAndContainerName(containerID)
+	if groupName != containerID {
+		msg := "cannot kill service %q from compose application %q, you can kill the entire compose app with docker kill %s"
+		return fmt.Errorf(msg, containerName, groupName, groupName)
+	}
+	return stopACIContainerGroup(ctx, cs.ctx, groupName) // As ACI doesn't have a kill command, we are using the stop implementation instead
+}
+
+func (cs *aciContainerService) Exec(ctx context.Context, name string, request containers.ExecRequest) error {
+	err := verifyExecCommand(request.Command)
+	if err != nil {
+		return err
+	}
+	groupName, containerAciName := getGroupAndContainerName(name)
+	containerExecResponse, err := execACIContainer(ctx, cs.ctx, request.Command, groupName, containerAciName)
+	if err != nil {
+		return err
+	}
+
+	return exec(
+		context.Background(),
+		*containerExecResponse.WebSocketURI,
+		*containerExecResponse.Password,
+		request,
+	)
+}
+
+func verifyExecCommand(command string) error {
+	tokens := strings.Split(command, " ")
+	if len(tokens) > 1 {
+		return errors.New("ACI exec command does not accept arguments to the command. " +
+			"Only the binary should be specified")
+	}
+	return nil
+}
+
+func (cs *aciContainerService) Logs(ctx context.Context, containerName string, req containers.LogsRequest) error {
+	groupName, containerAciName := getGroupAndContainerName(containerName)
+	var tail *int32
+
+	if req.Follow {
+		return streamLogs(ctx, cs.ctx, groupName, containerAciName, req)
+	}
+
+	if req.Tail != "all" {
+		reqTail, err := strconv.Atoi(req.Tail)
+		if err != nil {
+			return err
+		}
+		i32 := int32(reqTail)
+		tail = &i32
+	}
+
+	logs, err := getACIContainerLogs(ctx, cs.ctx, groupName, containerAciName, tail)
+	if err != nil {
+		return err
+	}
+
+	_, err = fmt.Fprint(req.Writer, logs)
+	return err
+}
+
+func (cs *aciContainerService) Delete(ctx context.Context, containerID string, request containers.DeleteRequest) error {
+	groupName, containerName := getGroupAndContainerName(containerID)
+	if groupName != containerID {
+		msg := "cannot delete service %q from compose application %q, you can delete the entire compose app with docker compose down --project-name %s"
+		return fmt.Errorf(msg, containerName, groupName, groupName)
+	}
+
+	if !request.Force {
+		containerGroupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
+		if err != nil {
+			return err
+		}
+
+		cg, err := containerGroupsClient.Get(ctx, cs.ctx.ResourceGroup, groupName)
+		if err != nil {
+			if cg.StatusCode == http.StatusNotFound {
+				return errdefs.ErrNotFound
+			}
+			return err
+		}
+
+		for _, container := range *cg.Containers {
+			status := convert.GetStatus(container, cg)
+
+			if status == convert.StatusRunning {
+				return errdefs.ErrForbidden
+			}
+		}
+	}
+
+	cg, err := deleteACIContainerGroup(ctx, cs.ctx, groupName)
+	// Delete returns `StatusNoContent` if the group is not found
+	if cg.StatusCode == http.StatusNoContent {
+		return errdefs.ErrNotFound
+	}
+	if err != nil {
+		return err
+	}
+
+	return err
+}
+
+func (cs *aciContainerService) Inspect(ctx context.Context, containerID string) (containers.Container, error) {
+	groupName, containerName := getGroupAndContainerName(containerID)
+
+	cg, err := getACIContainerGroup(ctx, cs.ctx, groupName)
+	if err != nil {
+		return containers.Container{}, err
+	}
+	if cg.StatusCode == http.StatusNoContent {
+		return containers.Container{}, errdefs.ErrNotFound
+	}
+
+	var cc containerinstance.Container
+	var found = false
+	for _, c := range *cg.Containers {
+		if to.String(c.Name) == containerName {
+			cc = c
+			found = true
+			break
+		}
+	}
+	if !found {
+		return containers.Container{}, errdefs.ErrNotFound
+	}
+
+	return convert.ContainerGroupToContainer(containerID, cg, cc), nil
+}

+ 3 - 8
aci/convert/convert.go

@@ -56,13 +56,8 @@ const (
 func ToContainerGroup(ctx context.Context, aciContext store.AciContext, p types.Project) (containerinstance.ContainerGroup, error) {
 	project := projectAciHelper(p)
 	containerGroupName := strings.ToLower(project.Name)
-	loginService, err := login.NewAzureLoginService()
-	if err != nil {
-		return containerinstance.ContainerGroup{}, err
-	}
-	storageHelper := login.StorageAccountHelper{
-		LoginService: *loginService,
-		AciContext:   aciContext,
+	storageHelper := login.StorageLogin{
+		AciContext: aciContext,
 	}
 	volumesCache, volumesSlice, err := project.getAciFileVolumes(ctx, storageHelper)
 	if err != nil {
@@ -205,7 +200,7 @@ func (p projectAciHelper) getAciSecretVolumes() ([]containerinstance.Volume, err
 	return secretVolumes, nil
 }
 
-func (p projectAciHelper) getAciFileVolumes(ctx context.Context, helper login.StorageAccountHelper) (map[string]bool, []containerinstance.Volume, error) {
+func (p projectAciHelper) getAciFileVolumes(ctx context.Context, helper login.StorageLogin) (map[string]bool, []containerinstance.Volume, error) {
 	azureFileVolumesMap := make(map[string]bool, len(p.Volumes))
 	var azureFileVolumesSlice []containerinstance.Volume
 	for name, v := range p.Volumes {

+ 13 - 0
aci/login/client.go

@@ -67,6 +67,19 @@ func NewStorageAccountsClient(subscriptionID string) (storage.AccountsClient, er
 	return containerGroupsClient, nil
 }
 
+// NewFileShareClient get client to manipulate file shares
+func NewFileShareClient(subscriptionID string) (storage.FileSharesClient, error) {
+	containerGroupsClient := storage.NewFileSharesClient(subscriptionID)
+	err := setupClient(&containerGroupsClient.Client)
+	if err != nil {
+		return storage.FileSharesClient{}, err
+	}
+	containerGroupsClient.PollingDelay = 5 * time.Second
+	containerGroupsClient.RetryAttempts = 30
+	containerGroupsClient.RetryDuration = 1 * time.Second
+	return containerGroupsClient, nil
+}
+
 // NewSubscriptionsClient get subscription client
 func NewSubscriptionsClient() (subscription.SubscriptionsClient, error) {
 	subc := subscription.NewSubscriptionsClient()

+ 4 - 5
aci/login/storage_helper.go → aci/login/storagelogin.go

@@ -25,14 +25,13 @@ import (
 	"github.com/docker/compose-cli/context/store"
 )
 
-// StorageAccountHelper helper for Azure Storage Account
-type StorageAccountHelper struct {
-	LoginService AzureLoginService
-	AciContext   store.AciContext
+// StorageLogin helper for Azure Storage Login
+type StorageLogin struct {
+	AciContext store.AciContext
 }
 
 // GetAzureStorageAccountKey retrieves the storage account ket from the current azure login
-func (helper StorageAccountHelper) GetAzureStorageAccountKey(ctx context.Context, accountName string) (string, error) {
+func (helper StorageLogin) GetAzureStorageAccountKey(ctx context.Context, accountName string) (string, error) {
 	client, err := NewStorageAccountsClient(helper.AciContext.SubscriptionID)
 	if err != nil {
 		return "", err

+ 234 - 0
aci/volumes.go

@@ -0,0 +1,234 @@
+/*
+   Copyright 2020 Docker, Inc.
+
+   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 aci
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/pkg/errors"
+
+	"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage"
+	"github.com/Azure/go-autorest/autorest/to"
+
+	"github.com/docker/compose-cli/aci/login"
+	"github.com/docker/compose-cli/api/volumes"
+	"github.com/docker/compose-cli/context/store"
+	"github.com/docker/compose-cli/errdefs"
+	"github.com/docker/compose-cli/progress"
+)
+
+type aciVolumeService struct {
+	aciContext store.AciContext
+}
+
+func (cs *aciVolumeService) List(ctx context.Context) ([]volumes.Volume, error) {
+	accountClient, err := login.NewStorageAccountsClient(cs.aciContext.SubscriptionID)
+	if err != nil {
+		return nil, err
+	}
+	result, err := accountClient.ListByResourceGroup(ctx, cs.aciContext.ResourceGroup)
+	if err != nil {
+		return nil, err
+	}
+	accounts := result.Value
+	fileShareClient, err := login.NewFileShareClient(cs.aciContext.SubscriptionID)
+	if err != nil {
+		return nil, err
+	}
+	fileShares := []volumes.Volume{}
+	for _, account := range *accounts {
+		fileSharePage, err := fileShareClient.List(ctx, cs.aciContext.ResourceGroup, *account.Name, "", "", "")
+		if err != nil {
+			return nil, err
+		}
+
+		for fileSharePage.NotDone() {
+			values := fileSharePage.Values()
+			for _, fileShare := range values {
+				fileShares = append(fileShares, toVolume(account, *fileShare.Name))
+			}
+			if err := fileSharePage.NextWithContext(ctx); err != nil {
+				return nil, err
+			}
+		}
+	}
+	return fileShares, nil
+}
+
+// VolumeCreateOptions options to create a new ACI volume
+type VolumeCreateOptions struct {
+	Account   string
+	Fileshare string
+}
+
+func (cs *aciVolumeService) Create(ctx context.Context, options interface{}) (volumes.Volume, error) {
+	opts, ok := options.(VolumeCreateOptions)
+	if !ok {
+		return volumes.Volume{}, errors.New("could not read Azure VolumeCreateOptions struct from generic parameter")
+	}
+	w := progress.ContextWriter(ctx)
+	w.Event(event(opts.Account, progress.Working, "Validating"))
+	accountClient, err := login.NewStorageAccountsClient(cs.aciContext.SubscriptionID)
+	if err != nil {
+		return volumes.Volume{}, err
+	}
+	account, err := accountClient.GetProperties(ctx, cs.aciContext.ResourceGroup, opts.Account, "")
+	if err == nil {
+		w.Event(event(opts.Account, progress.Done, "Use existing"))
+	} else {
+		if account.StatusCode != http.StatusNotFound {
+			return volumes.Volume{}, err
+		}
+		result, err := accountClient.CheckNameAvailability(ctx, storage.AccountCheckNameAvailabilityParameters{
+			Name: to.StringPtr(opts.Account),
+			Type: to.StringPtr("Microsoft.Storage/storageAccounts"),
+		})
+		if err != nil {
+			return volumes.Volume{}, err
+		}
+		if !*result.NameAvailable {
+			return volumes.Volume{}, errors.New("error: " + *result.Message)
+		}
+		parameters := defaultStorageAccountParams(cs.aciContext)
+
+		w.Event(event(opts.Account, progress.Working, "Creating"))
+
+		future, err := accountClient.Create(ctx, cs.aciContext.ResourceGroup, opts.Account, parameters)
+		if err != nil {
+			w.Event(errorEvent(opts.Account))
+			return volumes.Volume{}, err
+		}
+		if err := future.WaitForCompletionRef(ctx, accountClient.Client); err != nil {
+			w.Event(errorEvent(opts.Account))
+			return volumes.Volume{}, err
+		}
+		account, err = future.Result(accountClient)
+		if err != nil {
+			w.Event(errorEvent(opts.Account))
+			return volumes.Volume{}, err
+		}
+		w.Event(event(opts.Account, progress.Done, "Created"))
+	}
+	w.Event(event(opts.Fileshare, progress.Working, "Creating"))
+	fileShareClient, err := login.NewFileShareClient(cs.aciContext.SubscriptionID)
+	if err != nil {
+		return volumes.Volume{}, err
+	}
+
+	fileShare, err := fileShareClient.Get(ctx, cs.aciContext.ResourceGroup, *account.Name, opts.Fileshare, "")
+	if err == nil {
+		w.Event(errorEvent(opts.Fileshare))
+		return volumes.Volume{}, errors.Wrapf(errdefs.ErrAlreadyExists, "Azure fileshare %q already exists", opts.Fileshare)
+	}
+	if fileShare.StatusCode != http.StatusNotFound {
+		w.Event(errorEvent(opts.Fileshare))
+		return volumes.Volume{}, err
+	}
+	fileShare, err = fileShareClient.Create(ctx, cs.aciContext.ResourceGroup, *account.Name, opts.Fileshare, storage.FileShare{})
+	if err != nil {
+		w.Event(errorEvent(opts.Fileshare))
+		return volumes.Volume{}, err
+	}
+	w.Event(event(opts.Fileshare, progress.Done, "Created"))
+	return toVolume(account, *fileShare.Name), nil
+}
+
+func event(resource string, status progress.EventStatus, text string) progress.Event {
+	return progress.Event{
+		ID:         resource,
+		Status:     status,
+		StatusText: text,
+	}
+}
+
+func errorEvent(resource string) progress.Event {
+	return progress.Event{
+		ID:         resource,
+		Status:     progress.Error,
+		StatusText: "Error",
+	}
+}
+
+func (cs *aciVolumeService) Delete(ctx context.Context, id string, options interface{}) error {
+	tokens := strings.Split(id, "@")
+	if len(tokens) != 2 {
+		return errors.New("wrong format for volume ID : should be storageaccount@fileshare")
+	}
+	storageAccount := tokens[0]
+	fileshare := tokens[1]
+
+	fileShareClient, err := login.NewFileShareClient(cs.aciContext.SubscriptionID)
+	if err != nil {
+		return err
+	}
+	fileShareItemsPage, err := fileShareClient.List(ctx, cs.aciContext.ResourceGroup, storageAccount, "", "", "")
+	if err != nil {
+		return err
+	}
+	fileshares := fileShareItemsPage.Values()
+	if len(fileshares) == 1 && *fileshares[0].Name == fileshare {
+		storageAccountsClient, err := login.NewStorageAccountsClient(cs.aciContext.SubscriptionID)
+		if err != nil {
+			return err
+		}
+		account, err := storageAccountsClient.GetProperties(ctx, cs.aciContext.ResourceGroup, storageAccount, "")
+		if err != nil {
+			return err
+		}
+		if err == nil {
+			if _, ok := account.Tags[dockerVolumeTag]; ok {
+				result, err := storageAccountsClient.Delete(ctx, cs.aciContext.ResourceGroup, storageAccount)
+				if result.StatusCode == http.StatusNoContent {
+					return errors.Wrapf(errdefs.ErrNotFound, "storage account %s does not exist", storageAccount)
+				}
+				return err
+			}
+		}
+	}
+
+	result, err := fileShareClient.Delete(ctx, cs.aciContext.ResourceGroup, storageAccount, fileshare)
+	if result.StatusCode == 204 {
+		return errors.Wrapf(errdefs.ErrNotFound, "fileshare %q", fileshare)
+	}
+	return err
+}
+
+func toVolume(account storage.Account, fileShareName string) volumes.Volume {
+	return volumes.Volume{
+		ID:          VolumeID(*account.Name, fileShareName),
+		Description: fmt.Sprintf("Fileshare %s in %s storage account", fileShareName, *account.Name),
+	}
+}
+
+// VolumeID generate volume ID from azure storage accoun & fileshare
+func VolumeID(storageAccount string, fileShareName string) string {
+	return fmt.Sprintf("%s@%s", storageAccount, fileShareName)
+}
+
+func defaultStorageAccountParams(aciContext store.AciContext) storage.AccountCreateParameters {
+	tags := map[string]*string{dockerVolumeTag: to.StringPtr(dockerVolumeTag)}
+	return storage.AccountCreateParameters{
+		Location: to.StringPtr(aciContext.Location),
+		Sku: &storage.Sku{
+			Name: storage.StandardLRS,
+		},
+		Tags: tags,
+	}
+}

+ 10 - 0
api/client/client.go

@@ -22,6 +22,7 @@ import (
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/containers"
 	"github.com/docker/compose-cli/api/secrets"
+	"github.com/docker/compose-cli/api/volumes"
 	"github.com/docker/compose-cli/backend"
 	apicontext "github.com/docker/compose-cli/context"
 	"github.com/docker/compose-cli/context/cloud"
@@ -86,3 +87,12 @@ func (c *Client) SecretsService() secrets.Service {
 
 	return &secretsService{}
 }
+
+// VolumeService returns the backend service for the current context
+func (c *Client) VolumeService() volumes.Service {
+	if vs := c.bs.VolumeService(); vs != nil {
+		return vs
+	}
+
+	return &volumeService{}
+}

+ 39 - 0
api/client/volume.go

@@ -0,0 +1,39 @@
+/*
+   Copyright 2020 Docker, Inc.
+
+   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 client
+
+import (
+	"context"
+
+	"github.com/docker/compose-cli/api/volumes"
+	"github.com/docker/compose-cli/errdefs"
+)
+
+type volumeService struct {
+}
+
+func (c *volumeService) List(ctx context.Context) ([]volumes.Volume, error) {
+	return nil, errdefs.ErrNotImplemented
+}
+
+func (c *volumeService) Create(ctx context.Context, options interface{}) (volumes.Volume, error) {
+	return volumes.Volume{}, errdefs.ErrNotImplemented
+}
+
+func (c *volumeService) Delete(ctx context.Context, id string, options interface{}) error {
+	return errdefs.ErrNotImplemented
+}

+ 37 - 0
api/volumes/api.go

@@ -0,0 +1,37 @@
+/*
+   Copyright 2020 Docker, Inc.
+
+   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 volumes
+
+import (
+	"context"
+)
+
+// Volume volume info
+type Volume struct {
+	ID          string
+	Description string
+}
+
+// Service interacts with the underlying container backend
+type Service interface {
+	// List returns all available volumes
+	List(ctx context.Context) ([]Volume, error)
+	// Create creates a new volume
+	Create(ctx context.Context, options interface{}) (Volume, error)
+	// Delete deletes an existing volume
+	Delete(ctx context.Context, volumeID string, options interface{}) error
+}

+ 3 - 1
backend/backend.go

@@ -26,6 +26,7 @@ import (
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/containers"
 	"github.com/docker/compose-cli/api/secrets"
+	"github.com/docker/compose-cli/api/volumes"
 	"github.com/docker/compose-cli/context/cloud"
 	"github.com/docker/compose-cli/errdefs"
 )
@@ -53,8 +54,9 @@ var backends = struct {
 // Service aggregates the service interfaces
 type Service interface {
 	ContainerService() containers.Service
-	SecretsService() secrets.Service
 	ComposeService() compose.Service
+	SecretsService() secrets.Service
+	VolumeService() volumes.Service
 }
 
 // Register adds a typed backend to the registry

+ 115 - 0
cli/cmd/volume/acivolume.go

@@ -0,0 +1,115 @@
+/*
+   Copyright 2020 Docker, Inc.
+
+   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 volume
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/hashicorp/go-multierror"
+
+	"github.com/spf13/cobra"
+
+	"github.com/docker/compose-cli/aci"
+	"github.com/docker/compose-cli/api/client"
+	"github.com/docker/compose-cli/progress"
+)
+
+// ACICommand manage volumes
+func ACICommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "volume",
+		Short: "Manages volumes",
+	}
+
+	cmd.AddCommand(
+		createVolume(),
+		listVolume(),
+		rmVolume(),
+	)
+	return cmd
+}
+
+func createVolume() *cobra.Command {
+	aciOpts := aci.VolumeCreateOptions{}
+	cmd := &cobra.Command{
+		Use:   "create --storage-account ACCOUNT --fileshare FILESHARE",
+		Short: "Creates an Azure file share to use as ACI volume.",
+		Args:  cobra.ExactArgs(0),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			ctx := cmd.Context()
+			c, err := client.New(ctx)
+			if err != nil {
+				return err
+			}
+			err = progress.Run(ctx, func(ctx context.Context) error {
+				if _, err := c.VolumeService().Create(ctx, aciOpts); err != nil {
+					return err
+				}
+				return nil
+			})
+			if err != nil {
+				return err
+			}
+			fmt.Println(aci.VolumeID(aciOpts.Account, aciOpts.Fileshare))
+			return nil
+		},
+	}
+
+	cmd.Flags().StringVar(&aciOpts.Account, "storage-account", "", "Storage account name")
+	cmd.Flags().StringVar(&aciOpts.Fileshare, "fileshare", "", "Fileshare name")
+	_ = cmd.MarkFlagRequired("fileshare")
+	_ = cmd.MarkFlagRequired("storage-account")
+	return cmd
+}
+
+func rmVolume() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "rm [OPTIONS] VOLUME [VOLUME...]",
+		Short: "Remove one or more volumes.",
+		Args:  cobra.MinimumNArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			c, err := client.New(cmd.Context())
+			if err != nil {
+				return err
+			}
+			var errs *multierror.Error
+			for _, id := range args {
+				err = c.VolumeService().Delete(cmd.Context(), id, nil)
+				if err != nil {
+					errs = multierror.Append(errs, err)
+					continue
+				}
+				fmt.Println(id)
+			}
+			if errs != nil {
+				errs.ErrorFormat = formatErrors
+			}
+			return errs.ErrorOrNil()
+		},
+	}
+	return cmd
+}
+
+func formatErrors(errs []error) string {
+	messages := make([]string, len(errs))
+	for i, err := range errs {
+		messages[i] = "Error: " + err.Error()
+	}
+	return strings.Join(messages, "\n")
+}

+ 66 - 0
cli/cmd/volume/list.go

@@ -0,0 +1,66 @@
+/*
+   Copyright 2020 Docker, Inc.
+
+   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 volume
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"strings"
+	"text/tabwriter"
+
+	"github.com/spf13/cobra"
+
+	"github.com/docker/compose-cli/api/client"
+	"github.com/docker/compose-cli/api/volumes"
+)
+
+func listVolume() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "ls",
+		Short: "list available volumes in context.",
+		Args:  cobra.ExactArgs(0),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			c, err := client.New(cmd.Context())
+			if err != nil {
+				return err
+			}
+			vols, err := c.VolumeService().List(cmd.Context())
+			if err != nil {
+				return err
+			}
+			printList(os.Stdout, vols)
+			return nil
+		},
+	}
+	return cmd
+}
+
+func printList(out io.Writer, volumes []volumes.Volume) {
+	printSection(out, func(w io.Writer) {
+		for _, vol := range volumes {
+			_, _ = fmt.Fprintf(w, "%s\t%s\n", vol.ID, vol.Description)
+		}
+	}, "ID", "DESCRIPTION")
+}
+
+func printSection(out io.Writer, printer func(io.Writer), headers ...string) {
+	w := tabwriter.NewWriter(out, 20, 1, 3, ' ', 0)
+	_, _ = fmt.Fprintln(w, strings.Join(headers, "\t"))
+	printer(w)
+	_ = w.Flush()
+}

+ 38 - 0
cli/cmd/volume/list_test.go

@@ -0,0 +1,38 @@
+/*
+   Copyright 2020 Docker, Inc.
+
+   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 volume
+
+import (
+	"bytes"
+	"testing"
+
+	"gotest.tools/v3/golden"
+
+	"github.com/docker/compose-cli/api/volumes"
+)
+
+func TestPrintList(t *testing.T) {
+	secrets := []volumes.Volume{
+		{
+			ID:          "volume@123",
+			Description: "volume 123",
+		},
+	}
+	out := &bytes.Buffer{}
+	printList(out, secrets)
+	golden.Assert(t, out.String(), "volumes-out.golden")
+}

+ 2 - 0
cli/cmd/volume/testdata/volumes-out.golden

@@ -0,0 +1,2 @@
+ID                  DESCRIPTION
+volume@123          volume 123

+ 11 - 9
cli/main.go

@@ -28,23 +28,15 @@ import (
 	"time"
 
 	"github.com/docker/compose-cli/cli/cmd/compose"
-
 	"github.com/docker/compose-cli/cli/cmd/logout"
-
+	volume "github.com/docker/compose-cli/cli/cmd/volume"
 	"github.com/docker/compose-cli/errdefs"
-
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 
 	// Backend registrations
 	_ "github.com/docker/compose-cli/aci"
-	_ "github.com/docker/compose-cli/ecs"
-	_ "github.com/docker/compose-cli/ecs/local"
-	_ "github.com/docker/compose-cli/example"
-	_ "github.com/docker/compose-cli/local"
-	"github.com/docker/compose-cli/metrics"
-
 	"github.com/docker/compose-cli/cli/cmd"
 	contextcmd "github.com/docker/compose-cli/cli/cmd/context"
 	"github.com/docker/compose-cli/cli/cmd/login"
@@ -54,6 +46,11 @@ import (
 	"github.com/docker/compose-cli/config"
 	apicontext "github.com/docker/compose-cli/context"
 	"github.com/docker/compose-cli/context/store"
+	_ "github.com/docker/compose-cli/ecs"
+	_ "github.com/docker/compose-cli/ecs/local"
+	_ "github.com/docker/compose-cli/example"
+	_ "github.com/docker/compose-cli/local"
+	"github.com/docker/compose-cli/metrics"
 )
 
 var (
@@ -182,6 +179,11 @@ func main() {
 		ctype = cc.Type()
 	}
 
+	if ctype == store.AciContextType {
+		// we can also pass ctype as a parameter to the volume command and customize subcommands, flags, etc. when we have other backend implementations
+		root.AddCommand(volume.ACICommand())
+	}
+
 	metrics.Track(ctype, os.Args[1:], root.PersistentFlags())
 
 	ctx = apicontext.WithCurrentContext(ctx, currentContext)

+ 5 - 1
ecs/backend.go

@@ -21,10 +21,10 @@ import (
 
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/session"
-
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/containers"
 	"github.com/docker/compose-cli/api/secrets"
+	"github.com/docker/compose-cli/api/volumes"
 	"github.com/docker/compose-cli/backend"
 	apicontext "github.com/docker/compose-cli/context"
 	"github.com/docker/compose-cli/context/cloud"
@@ -97,6 +97,10 @@ func (a *ecsAPIService) SecretsService() secrets.Service {
 	return a
 }
 
+func (a *ecsAPIService) VolumeService() volumes.Service {
+	return nil
+}
+
 func getCloudService() (cloud.Service, error) {
 	return ecsCloudService{}, nil
 }

+ 6 - 2
ecs/local/backend.go

@@ -22,11 +22,11 @@ import (
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/containers"
 	"github.com/docker/compose-cli/api/secrets"
-	"github.com/docker/docker/client"
-
+	"github.com/docker/compose-cli/api/volumes"
 	"github.com/docker/compose-cli/backend"
 	"github.com/docker/compose-cli/context/cloud"
 	"github.com/docker/compose-cli/context/store"
+	"github.com/docker/docker/client"
 )
 
 const backendType = store.EcsLocalSimulationContextType
@@ -58,6 +58,10 @@ func (e ecsLocalSimulation) ContainerService() containers.Service {
 	return nil
 }
 
+func (e ecsLocalSimulation) VolumeService() volumes.Service {
+	return nil
+}
+
 func (e ecsLocalSimulation) SecretsService() secrets.Service {
 	return nil
 }

+ 5 - 0
example/backend.go

@@ -29,6 +29,7 @@ import (
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/containers"
 	"github.com/docker/compose-cli/api/secrets"
+	"github.com/docker/compose-cli/api/volumes"
 	"github.com/docker/compose-cli/backend"
 	"github.com/docker/compose-cli/context/cloud"
 	"github.com/docker/compose-cli/errdefs"
@@ -51,6 +52,10 @@ func (a *apiService) SecretsService() secrets.Service {
 	return nil
 }
 
+func (a *apiService) VolumeService() volumes.Service {
+	return nil
+}
+
 func init() {
 	backend.Register("example", "example", service, cloud.NotImplementedCloudService)
 }

+ 5 - 0
local/backend.go

@@ -39,6 +39,7 @@ import (
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/containers"
 	"github.com/docker/compose-cli/api/secrets"
+	"github.com/docker/compose-cli/api/volumes"
 	"github.com/docker/compose-cli/backend"
 	"github.com/docker/compose-cli/context/cloud"
 	"github.com/docker/compose-cli/errdefs"
@@ -75,6 +76,10 @@ func (ms *local) SecretsService() secrets.Service {
 	return nil
 }
 
+func (ms *local) VolumeService() volumes.Service {
+	return nil
+}
+
 func (ms *local) Inspect(ctx context.Context, id string) (containers.Container, error) {
 	c, err := ms.apiClient.ContainerInspect(ctx, id)
 	if err != nil {

+ 79 - 61
tests/aci-e2e/e2e-aci_test.go

@@ -39,7 +39,6 @@ import (
 	"gotest.tools/v3/poll"
 
 	"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"
 	"github.com/Azure/go-autorest/autorest/to"
 
@@ -48,7 +47,6 @@ import (
 	"github.com/docker/compose-cli/api/containers"
 	"github.com/docker/compose-cli/context/store"
 	"github.com/docker/compose-cli/errdefs"
-	"github.com/docker/compose-cli/tests/aci-e2e/storage"
 	. "github.com/docker/compose-cli/tests/framework"
 )
 
@@ -131,7 +129,7 @@ func TestContainerRunVolume(t *testing.T) {
 	sID, rg := setupTestResourceGroup(t, c)
 
 	const (
-		testShareName   = "dockertestshare"
+		fileshareName   = "dockertestshare"
 		testFileContent = "Volume mounted successfully!"
 		testFileName    = "index.html"
 	)
@@ -142,31 +140,78 @@ func TestContainerRunVolume(t *testing.T) {
 		Location:       location,
 		ResourceGroup:  rg,
 	}
-	saName := "e2e" + strconv.Itoa(int(time.Now().UnixNano()))
-	_, cleanupSa := createStorageAccount(t, aciContext, saName)
-	t.Cleanup(func() {
-		if err := cleanupSa(); err != nil {
-			t.Error(err)
-		}
-	})
-	keys := getStorageKeys(t, aciContext, saName)
-	assert.Assert(t, len(keys) > 0)
-	k := *keys[0].Value
-	cred, u := createFileShare(t, k, testShareName, saName)
-	uploadFile(t, *cred, u.String(), testFileName, testFileContent)
 
 	// Used in subtests
 	var (
-		container string
-		hostIP    string
-		endpoint  string
+		container   string
+		hostIP      string
+		endpoint    string
+		volumeID    string
+		accountName = "e2e" + strconv.Itoa(int(time.Now().UnixNano()))
 	)
 
+	t.Run("check volume name validity", func(t *testing.T) {
+		invalidName := "some-storage-123"
+		res := c.RunDockerOrExitError("volume", "create", "--storage-account", invalidName, "--fileshare", fileshareName)
+		res.Assert(t, icmd.Expected{
+			ExitCode: 1,
+			Err:      "some-storage-123 is not a valid storage account name. Storage account name must be between 3 and 24 characters in length and use numbers and lower-case letters only.",
+		})
+	})
+
+	t.Run("create volumes", func(t *testing.T) {
+		c.RunDockerCmd("volume", "create", "--storage-account", accountName, "--fileshare", fileshareName)
+	})
+	volumeID = accountName + "@" + fileshareName
+
+	t.Cleanup(func() {
+		c.RunDockerCmd("volume", "rm", volumeID)
+		res := c.RunDockerCmd("volume", "ls")
+		lines := lines(res.Stdout())
+		assert.Equal(t, len(lines), 1)
+	})
+
+	t.Run("create second fileshare", func(t *testing.T) {
+		c.RunDockerCmd("volume", "create", "--storage-account", accountName, "--fileshare", "dockertestshare2")
+	})
+	volumeID2 := accountName + "@dockertestshare2"
+
+	t.Run("list volumes", func(t *testing.T) {
+		res := c.RunDockerCmd("volume", "ls")
+		lines := lines(res.Stdout())
+		assert.Equal(t, len(lines), 3)
+		firstAccount := lines[1]
+		fields := strings.Fields(firstAccount)
+		assert.Equal(t, fields[0], volumeID)
+		secondAccount := lines[2]
+		fields = strings.Fields(secondAccount)
+		assert.Equal(t, fields[0], volumeID2)
+	})
+
+	t.Run("delete only fileshare", func(t *testing.T) {
+		c.RunDockerCmd("volume", "rm", volumeID2)
+		res := c.RunDockerCmd("volume", "ls")
+		lines := lines(res.Stdout())
+		assert.Equal(t, len(lines), 2)
+		assert.Assert(t, !strings.Contains(res.Stdout(), "dockertestshare2"), "second fileshare still visible after rm")
+	})
+
+	t.Run("upload file", func(t *testing.T) {
+		storageLogin := login.StorageLogin{AciContext: aciContext}
+
+		key, err := storageLogin.GetAzureStorageAccountKey(context.TODO(), accountName)
+		assert.NilError(t, err)
+		cred, err := azfile.NewSharedKeyCredential(accountName, key)
+		assert.NilError(t, err)
+		u, _ := url.Parse(fmt.Sprintf("https://%s.file.core.windows.net/%s", accountName, fileshareName))
+		uploadFile(t, *cred, u.String(), testFileName, testFileContent)
+	})
+
 	t.Run("run", func(t *testing.T) {
 		mountTarget := "/usr/share/nginx/html"
 		res := c.RunDockerCmd(
 			"run", "-d",
-			"-v", fmt.Sprintf("%s@%s:%s", saName, testShareName, mountTarget),
+			"-v", fmt.Sprintf("%s:%s", volumeID, mountTarget),
 			"-p", "80:80",
 			"nginx",
 		)
@@ -189,7 +234,7 @@ func TestContainerRunVolume(t *testing.T) {
 
 	t.Run("ps", func(t *testing.T) {
 		res := c.RunDockerCmd("ps")
-		out := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
+		out := lines(res.Stdout())
 		l := out[len(out)-1]
 		assert.Assert(t, strings.Contains(l, container), "Looking for %q in line: %s", container, l)
 		assert.Assert(t, strings.Contains(l, "nginx"))
@@ -284,6 +329,10 @@ func TestContainerRunVolume(t *testing.T) {
 	})
 }
 
+func lines(output string) []string {
+	return strings.Split(strings.TrimSpace(output), "\n")
+}
+
 func TestContainerRunAttached(t *testing.T) {
 	c := NewParallelE2eCLI(t, binDir)
 	_, _ = setupTestResourceGroup(t, c)
@@ -374,11 +423,11 @@ func TestContainerRunAttached(t *testing.T) {
 
 	t.Run("ps stopped container with --all", func(t *testing.T) {
 		res := c.RunDockerCmd("ps", container)
-		out := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
+		out := lines(res.Stdout())
 		assert.Assert(t, is.Len(out, 1))
 
 		res = c.RunDockerCmd("ps", "--all", container)
-		out = strings.Split(strings.TrimSpace(res.Stdout()), "\n")
+		out = lines(res.Stdout())
 		assert.Assert(t, is.Len(out, 2))
 	})
 
@@ -415,7 +464,7 @@ func TestComposeUpUpdate(t *testing.T) {
 		// Name of Compose project is taken from current folder "acie2e"
 		c.RunDockerCmd("compose", "up", "-f", composeFile)
 		res := c.RunDockerCmd("ps")
-		out := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
+		out := lines(res.Stdout())
 		// Check three containers are running
 		assert.Assert(t, is.Len(out, 4))
 		webRunning := false
@@ -444,7 +493,7 @@ func TestComposeUpUpdate(t *testing.T) {
 
 	t.Run("compose ps", func(t *testing.T) {
 		res := c.RunDockerCmd("compose", "ps", "--project-name", composeProjectName)
-		lines := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
+		lines := lines(res.Stdout())
 		assert.Assert(t, is.Len(lines, 4))
 		var wordsDisplayed, webDisplayed, dbDisplayed bool
 		for _, line := range lines {
@@ -468,7 +517,7 @@ func TestComposeUpUpdate(t *testing.T) {
 
 	t.Run("compose ls", func(t *testing.T) {
 		res := c.RunDockerCmd("compose", "ls")
-		lines := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
+		lines := lines(res.Stdout())
 
 		assert.Equal(t, 2, len(lines))
 		fields := strings.Fields(lines[1])
@@ -485,7 +534,7 @@ func TestComposeUpUpdate(t *testing.T) {
 	t.Run("update", func(t *testing.T) {
 		c.RunDockerCmd("compose", "up", "-f", composeFileMultiplePorts, "--project-name", composeProjectName)
 		res := c.RunDockerCmd("ps")
-		out := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
+		out := lines(res.Stdout())
 		// Check three containers are running
 		assert.Assert(t, is.Len(out, 4))
 
@@ -527,7 +576,7 @@ func TestComposeUpUpdate(t *testing.T) {
 	t.Run("down", func(t *testing.T) {
 		c.RunDockerCmd("compose", "down", "--project-name", composeProjectName)
 		res := c.RunDockerCmd("ps")
-		out := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
+		out := lines(res.Stdout())
 		assert.Equal(t, len(out), 1)
 	})
 }
@@ -548,7 +597,7 @@ func TestRunEnvVars(t *testing.T) {
 		cmd.Env = append(cmd.Env, "MYSQL_USER=user1")
 		res := icmd.RunCmd(cmd)
 		res.Assert(t, icmd.Success)
-		out := strings.Split(strings.TrimSpace(res.Stdout()), "\n")
+		out := lines(res.Stdout())
 		container := strings.TrimSpace(out[len(out)-1])
 
 		res = c.RunDockerCmd("inspect", container)
@@ -583,7 +632,7 @@ func setupTestResourceGroup(t *testing.T, c *E2eCLI) (string, string) {
 	createAciContextAndUseIt(t, c, sID, rg)
 	// Check nothing is running
 	res := c.RunDockerCmd("ps")
-	assert.Assert(t, is.Len(strings.Split(strings.TrimSpace(res.Stdout()), "\n"), 1))
+	assert.Assert(t, is.Len(lines(res.Stdout()), 1))
 	return sID, rg
 }
 
@@ -637,37 +686,6 @@ func createAciContextAndUseIt(t *testing.T, c *E2eCLI, sID, rgName string) {
 	res.Assert(t, icmd.Expected{Out: contextName + " *"})
 }
 
-func createStorageAccount(t *testing.T, aciContext store.AciContext, name string) (azure_storage.Account, func() error) {
-	account, err := storage.CreateStorageAccount(context.TODO(), aciContext, name)
-	assert.Check(t, is.Nil(err))
-	assert.Check(t, is.Equal(*(account.Name), name))
-	return account, func() error { return deleteStorageAccount(aciContext, name) }
-}
-
-func deleteStorageAccount(aciContext store.AciContext, name string) error {
-	_, err := storage.DeleteStorageAccount(context.TODO(), aciContext, name)
-	return err
-}
-
-func getStorageKeys(t *testing.T, aciContext store.AciContext, saName string) []azure_storage.AccountKey {
-	l, err := storage.ListKeys(context.TODO(), aciContext, saName)
-	assert.NilError(t, err)
-	assert.Assert(t, l.Keys != nil)
-	return *l.Keys
-}
-
-func createFileShare(t *testing.T, key, share, storageAccount string) (*azfile.SharedKeyCredential, *url.URL) {
-	// Create a ShareURL object that wraps a soon-to-be-created share's URL and a default pipeline.
-	u, _ := url.Parse(fmt.Sprintf("https://%s.file.core.windows.net/%s", storageAccount, share))
-	cred, err := azfile.NewSharedKeyCredential(storageAccount, key)
-	assert.NilError(t, err)
-
-	shareURL := azfile.NewShareURL(*u, azfile.NewPipeline(cred, azfile.PipelineOptions{}))
-	_, err = shareURL.Create(context.TODO(), azfile.Metadata{}, 0)
-	assert.NilError(t, err)
-	return cred, u
-}
-
 func uploadFile(t *testing.T, cred azfile.SharedKeyCredential, baseURL, fileName, content string) {
 	fURL, err := url.Parse(baseURL + "/" + fileName)
 	assert.NilError(t, err)
@@ -677,7 +695,7 @@ func uploadFile(t *testing.T, cred azfile.SharedKeyCredential, baseURL, fileName
 }
 
 func getContainerName(stdout string) string {
-	out := strings.Split(strings.TrimSpace(stdout), "\n")
+	out := lines(stdout)
 	return strings.TrimSpace(out[len(out)-1])
 }
 

+ 0 - 93
tests/aci-e2e/storage/storage.go

@@ -1,93 +0,0 @@
-/*
-   Copyright 2020 Docker, Inc.
-
-   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 storage
-
-import (
-	"context"
-	"errors"
-
-	"github.com/Azure/azure-sdk-for-go/profiles/2019-03-01/storage/mgmt/storage"
-	"github.com/Azure/go-autorest/autorest"
-	"github.com/Azure/go-autorest/autorest/to"
-
-	"github.com/docker/compose-cli/aci/login"
-	"github.com/docker/compose-cli/context/store"
-)
-
-// CreateStorageAccount creates a new storage account.
-func CreateStorageAccount(ctx context.Context, aciContext store.AciContext, accountName string) (storage.Account, error) {
-	storageAccountsClient := getStorageAccountsClient(aciContext)
-	result, err := storageAccountsClient.CheckNameAvailability(
-		ctx,
-		storage.AccountCheckNameAvailabilityParameters{
-			Name: to.StringPtr(accountName),
-			Type: to.StringPtr("Microsoft.Storage/storageAccounts"),
-		})
-
-	if err != nil {
-		return storage.Account{}, err
-	}
-	if !*result.NameAvailable {
-		return storage.Account{}, errors.New("storage account name already exists" + accountName)
-	}
-
-	future, err := storageAccountsClient.Create(
-		ctx,
-		aciContext.ResourceGroup,
-		accountName,
-		storage.AccountCreateParameters{
-			Sku: &storage.Sku{
-				Name: storage.StandardLRS,
-			},
-			Location:                          to.StringPtr(aciContext.Location),
-			AccountPropertiesCreateParameters: &storage.AccountPropertiesCreateParameters{}})
-	if err != nil {
-		return storage.Account{}, err
-	}
-	err = future.WaitForCompletionRef(ctx, storageAccountsClient.Client)
-	if err != nil {
-		return storage.Account{}, err
-	}
-	return future.Result(storageAccountsClient)
-}
-
-// DeleteStorageAccount deletes a given storage account
-func DeleteStorageAccount(ctx context.Context, aciContext store.AciContext, accountName string) (autorest.Response, error) {
-	storageAccountsClient := getStorageAccountsClient(aciContext)
-	response, err := storageAccountsClient.Delete(ctx, aciContext.ResourceGroup, accountName)
-	if err != nil {
-		return autorest.Response{}, err
-	}
-	return response, err
-}
-
-// ListKeys lists the storage account keys
-func ListKeys(ctx context.Context, aciContext store.AciContext, accountName string) (storage.AccountListKeysResult, error) {
-	storageAccountsClient := getStorageAccountsClient(aciContext)
-	keys, err := storageAccountsClient.ListKeys(ctx, aciContext.ResourceGroup, accountName)
-	if err != nil {
-		return storage.AccountListKeysResult{}, err
-	}
-	return keys, nil
-}
-
-func getStorageAccountsClient(aciContext store.AciContext) storage.AccountsClient {
-	storageAccountsClient := storage.NewAccountsClient(aciContext.SubscriptionID)
-	autho, _ := login.NewAuthorizerFromLogin()
-	storageAccountsClient.Authorizer = autho
-	return storageAccountsClient
-}

+ 2 - 1
tests/ecs-local-e2e/context_test.go

@@ -21,8 +21,9 @@ import (
 	"os"
 	"testing"
 
-	. "github.com/docker/compose-cli/tests/framework"
 	"gotest.tools/v3/icmd"
+
+	. "github.com/docker/compose-cli/tests/framework"
 )
 
 const (