Bläddra i källkod

Break out aci backend.go into several files for each service

Signed-off-by: Guillaume Tardif <[email protected]>
Guillaume Tardif 5 år sedan
förälder
incheckning
15addf5c22
8 ändrade filer med 544 tillägg och 441 borttagningar
  1. 28 0
      aci/aci.go
  2. 1 393
      aci/backend.go
  3. 48 0
      aci/cloud.go
  4. 128 0
      aci/compose.go
  5. 254 0
      aci/containers.go
  6. 2 2
      aci/convert/convert.go
  7. 49 0
      aci/login/storagelogin.go
  8. 34 46
      aci/volumes.go

+ 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 {

+ 1 - 393
aci/backend.go

@@ -18,18 +18,11 @@ 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"
@@ -41,7 +34,6 @@ import (
 	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 (
@@ -111,7 +103,7 @@ func getAciAPIService(aciCtx store.AciContext) *aciAPIService {
 			ctx: aciCtx,
 		},
 		aciVolumeService: &aciVolumeService{
-			ctx: aciCtx,
+			aciContext: aciCtx,
 		},
 	}
 }
@@ -138,60 +130,6 @@ func (a *aciAPIService) VolumeService() volumes.Service {
 	return a.aciVolumeService
 }
 
-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 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 getContainerID(group containerinstance.ContainerGroup, container containerinstance.Container) string {
 	containerID := *group.Name + composeContainerSeparator + *container.Name
 	if _, ok := group.Tags[singleContainerTag]; ok {
@@ -204,26 +142,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)
@@ -231,52 +149,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)
@@ -289,267 +161,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 aciVolumeService struct {
-	ctx store.AciContext
-}
-
-func (cs *aciVolumeService) List(ctx context.Context) ([]volumes.Volume, error) {
-	storageHelper := login.StorageAccountHelper{AciContext: cs.ctx}
-	return storageHelper.ListFileShare(ctx)
-}
-
-//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 LoginParams struct from generic parameter")
-	}
-	storageHelper := login.StorageAccountHelper{AciContext: cs.ctx}
-	return storageHelper.CreateFileShare(ctx, opts.Account, opts.Fileshare)
-}
-
-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)
-}

+ 48 - 0
aci/cloud.go

@@ -0,0 +1,48 @@
+/*
+   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)
+}

+ 128 - 0
aci/compose.go

@@ -0,0 +1,128 @@
+/*
+   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\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 := getACIContainerGroups(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
+}
+
+

+ 254 - 0
aci/containers.go

@@ -0,0 +1,254 @@
+/*
+   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 []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 (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 (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 (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
+}

+ 2 - 2
aci/convert/convert.go

@@ -56,7 +56,7 @@ const (
 func ToContainerGroup(ctx context.Context, aciContext store.AciContext, p types.Project) (containerinstance.ContainerGroup, error) {
 	project := projectAciHelper(p)
 	containerGroupName := strings.ToLower(project.Name)
-	storageHelper := login.StorageAccountHelper{
+	storageHelper := login.StorageLogin{
 		AciContext: aciContext,
 	}
 	volumesCache, volumesSlice, err := project.getAciFileVolumes(ctx, storageHelper)
@@ -200,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 {

+ 49 - 0
aci/login/storagelogin.go

@@ -0,0 +1,49 @@
+/*
+   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 login
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/pkg/errors"
+
+	"github.com/docker/compose-cli/context/store"
+)
+
+// 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 StorageLogin) GetAzureStorageAccountKey(ctx context.Context, accountName string) (string, error) {
+	client, err := NewStorageAccountsClient(helper.AciContext.SubscriptionID)
+	if err != nil {
+		return "", err
+	}
+	result, err := client.ListKeys(ctx, helper.AciContext.ResourceGroup, accountName, "")
+	if err != nil {
+		return "", errors.Wrap(err, fmt.Sprintf("could not access storage account acountKeys for %s, using the azure login", accountName))
+	}
+	if result.Keys != nil && len((*result.Keys)) < 1 {
+		return "", fmt.Errorf("no key could be obtained for storage account %s from your azure login", accountName)
+	}
+
+	key := (*result.Keys)[0]
+	return *key.Value, nil
+}

+ 34 - 46
aci/login/storage_helper.go → aci/volumes.go

@@ -14,11 +14,12 @@
    limitations under the License.
 */
 
-package login
+package aci
 
 import (
 	"context"
 	"fmt"
+	"github.com/docker/compose-cli/aci/login"
 
 	"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage"
 
@@ -30,48 +31,27 @@ import (
 	"github.com/docker/compose-cli/context/store"
 )
 
-// StorageAccountHelper helper for Azure Storage Account
-type StorageAccountHelper struct {
-	AciContext store.AciContext
+type aciVolumeService 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) {
-	client, err := NewStorageAccountsClient(helper.AciContext.SubscriptionID)
-	if err != nil {
-		return "", err
-	}
-	result, err := client.ListKeys(ctx, helper.AciContext.ResourceGroup, accountName, "")
-	if err != nil {
-		return "", errors.Wrap(err, fmt.Sprintf("could not access storage account acountKeys for %s, using the azure login", accountName))
-	}
-	if result.Keys != nil && len((*result.Keys)) < 1 {
-		return "", fmt.Errorf("no key could be obtained for storage account %s from your azure login", accountName)
-	}
-
-	key := (*result.Keys)[0]
-	return *key.Value, nil
-}
-
-// ListFileShare list file shares in all visible storage accounts
-func (helper StorageAccountHelper) ListFileShare(ctx context.Context) ([]volumes.Volume, error) {
-	aciContext := helper.AciContext
-	accountClient, err := NewStorageAccountsClient(aciContext.SubscriptionID)
+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, aciContext.ResourceGroup)
+	result, err := accountClient.ListByResourceGroup(ctx, cs.aciContext.ResourceGroup)
 	if err != nil {
 		return nil, err
 	}
 	accounts := result.Value
-	fileShareClient, err := NewFileShareClient(aciContext.SubscriptionID)
+	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, aciContext.ResourceGroup, *account.Name, "", "", "")
+		fileSharePage, err := fileShareClient.List(ctx, cs.aciContext.ResourceGroup, *account.Name, "", "", "")
 		if err != nil {
 			return nil, err
 		}
@@ -89,30 +69,30 @@ func (helper StorageAccountHelper) ListFileShare(ctx context.Context) ([]volumes
 	return fileShares, nil
 }
 
-func toVolume(account storage.Account, fileShareName string) volumes.Volume {
-	return volumes.Volume{
-		ID:          fmt.Sprintf("%s@%s", *account.Name, fileShareName),
-		Name:        fileShareName,
-		Description: fmt.Sprintf("Fileshare %s in %s storage account", fileShareName, *account.Name),
-	}
+//VolumeCreateOptions options to create a new ACI volume
+type VolumeCreateOptions struct {
+	Account   string
+	Fileshare string
 }
 
-// CreateFileShare create a new fileshare
-func (helper StorageAccountHelper) CreateFileShare(ctx context.Context, accountName string, fileShareName string) (volumes.Volume, error) {
-	aciContext := helper.AciContext
-	accountClient, err := NewStorageAccountsClient(aciContext.SubscriptionID)
+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 LoginParams struct from generic parameter")
+	}
+	accountClient, err := login.NewStorageAccountsClient(cs.aciContext.SubscriptionID)
 	if err != nil {
 		return volumes.Volume{}, err
 	}
-	account, err := accountClient.GetProperties(ctx, aciContext.ResourceGroup, accountName, "")
+	account, err := accountClient.GetProperties(ctx, cs.aciContext.ResourceGroup, opts.Account, "")
 	if err != nil {
 		if account.StatusCode != 404 {
 			return volumes.Volume{}, err
 		}
 		//TODO confirm storage account creation
-		parameters := defaultStorageAccountParams(aciContext)
+		parameters := defaultStorageAccountParams(cs.aciContext)
 		// TODO progress account creation
-		future, err := accountClient.Create(ctx, aciContext.ResourceGroup, accountName, parameters)
+		future, err := accountClient.Create(ctx, cs.aciContext.ResourceGroup, opts.Account, parameters)
 		if err != nil {
 			return volumes.Volume{}, err
 		}
@@ -121,25 +101,33 @@ func (helper StorageAccountHelper) CreateFileShare(ctx context.Context, accountN
 			return volumes.Volume{}, err
 		}
 	}
-	fileShareClient, err := NewFileShareClient(aciContext.SubscriptionID)
+	fileShareClient, err := login.NewFileShareClient(cs.aciContext.SubscriptionID)
 	if err != nil {
 		return volumes.Volume{}, err
 	}
 
-	fileShare, err := fileShareClient.Get(ctx, aciContext.ResourceGroup, *account.Name, fileShareName, "")
+	fileShare, err := fileShareClient.Get(ctx, cs.aciContext.ResourceGroup, *account.Name, opts.Fileshare, "")
 	if err == nil {
-		return volumes.Volume{}, errors.Wrapf(errdefs.ErrAlreadyExists, "Azure fileshare %q already exists", fileShareName)
+		return volumes.Volume{}, errors.Wrapf(errdefs.ErrAlreadyExists, "Azure fileshare %q already exists", opts.Fileshare)
 	}
 	if fileShare.StatusCode != 404 {
 		return volumes.Volume{}, err
 	}
-	fileShare, err = fileShareClient.Create(ctx, aciContext.ResourceGroup, *account.Name, fileShareName, storage.FileShare{})
+	fileShare, err = fileShareClient.Create(ctx, cs.aciContext.ResourceGroup, *account.Name, opts.Fileshare, storage.FileShare{})
 	if err != nil {
 		return volumes.Volume{}, err
 	}
 	return toVolume(account, *fileShare.Name), nil
 }
 
+func toVolume(account storage.Account, fileShareName string) volumes.Volume {
+	return volumes.Volume{
+		ID:          fmt.Sprintf("%s@%s", *account.Name, fileShareName),
+		Name:        fileShareName,
+		Description: fmt.Sprintf("Fileshare %s in %s storage account", fileShareName, *account.Name),
+	}
+}
+
 func defaultStorageAccountParams(aciContext store.AciContext) storage.AccountCreateParameters {
 	return storage.AccountCreateParameters{
 		Location: &aciContext.Location,