Repository: docker/compose-cli
Pull request #886 (2020-11-06 08:54:19)

Add auto creation of fileshares
**What I did**
Add auto creation of fileshares

**Related issue**
Resolves https://github.com/docker/compose-cli/issues/830 https://github.com/docker/compose-cli/issues/841

**Tests**
/test-aci


Patch:
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 321f074e..7a24d0b3 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -4,9 +4,6 @@ aci:
 ecs:
   - ecs/**/*
 
-local:
-  - local/**/*
-
 cli:
   - cli/**/*
 
diff --git a/Dockerfile b/Dockerfile
index d174ed20..fd02c25a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -43,13 +43,11 @@ FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION} AS lint-base
 FROM base AS lint
 ENV CGO_ENABLED=0
 COPY --from=lint-base /usr/bin/golangci-lint /usr/bin/golangci-lint
-ARG BUILD_TAGS
 ARG GIT_TAG
 RUN --mount=target=. \
     --mount=type=cache,target=/go/pkg/mod \
     --mount=type=cache,target=/root/.cache/go-build \
     --mount=type=cache,target=/root/.cache/golangci-lint \
-    BUILD_TAGS=${BUILD_TAGS} \
     GIT_TAG=${GIT_TAG} \
     make -f builder.Makefile lint
 
diff --git a/Makefile b/Makefile
index 6c55be9a..10273684 100644
--- a/Makefile
+++ b/Makefile
@@ -72,7 +72,6 @@ cache-clear: ## Clear the builder cache
 
 lint: ## run linter(s)
 	@docker build . \
-	--build-arg BUILD_TAGS=example,local,e2e \
 	--build-arg GIT_TAG=$(GIT_TAG) \
 	--target lint
 
diff --git a/aci/aci.go b/aci/aci.go
index 49eaa7b0..ecfd6ed9 100644
--- a/aci/aci.go
+++ b/aci/aci.go
@@ -28,6 +28,7 @@ import (
 	"github.com/Azure/go-autorest/autorest"
 	"github.com/Azure/go-autorest/autorest/to"
 	tm "github.com/buger/goterm"
+	"github.com/compose-spec/compose-go/types"
 	"github.com/gobwas/ws"
 	"github.com/gobwas/ws/wsutil"
 	"github.com/morikuni/aec"
@@ -35,6 +36,7 @@ import (
 
 	"github.com/docker/compose-cli/aci/convert"
 	"github.com/docker/compose-cli/aci/login"
+	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/containers"
 	"github.com/docker/compose-cli/context/store"
 	"github.com/docker/compose-cli/errdefs"
@@ -64,12 +66,46 @@ func createACIContainers(ctx context.Context, aciContext store.AciContext, group
 	return createOrUpdateACIContainers(ctx, aciContext, groupDefinition)
 }
 
+func autocreateFileshares(ctx context.Context, project *types.Project) error {
+	clt, err := client.New(ctx)
+	if err != nil {
+		return err
+	}
+	for _, v := range project.Volumes {
+		if v.Driver != convert.AzureFileDriverName {
+			return fmt.Errorf("cannot use ACI volume, required driver is %q, found %q", convert.AzureFileDriverName, v.Driver)
+		}
+		shareName, ok := v.DriverOpts[convert.VolumeDriveroptsShareNameKey]
+		if !ok {
+			return fmt.Errorf("cannot retrieve fileshare name for Azure file share")
+		}
+		accountName, ok := v.DriverOpts[convert.VolumeDriveroptsAccountNameKey]
+		if !ok {
+			return fmt.Errorf("cannot retrieve account name for Azure file share")
+		}
+		_, err = clt.VolumeService().Inspect(ctx, fmt.Sprintf("%s/%s", accountName, shareName))
+		if err != nil { // Not found, autocreate fileshare
+			if !errdefs.IsNotFoundError(err) {
+				return err
+			}
+			aciVolumeOpts := &VolumeCreateOptions{
+				Account: accountName,
+			}
+			if _, err = clt.VolumeService().Create(ctx, shareName, aciVolumeOpts); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
 func createOrUpdateACIContainers(ctx context.Context, aciContext store.AciContext, groupDefinition containerinstance.ContainerGroup) error {
 	w := progress.ContextWriter(ctx)
 	containerGroupsClient, err := login.NewContainerGroupsClient(aciContext.SubscriptionID)
 	if err != nil {
 		return errors.Wrapf(err, "cannot get container group client")
 	}
+
 	groupDisplay := "Group " + *groupDefinition.Name
 	w.Event(progress.Event{
 		ID:         groupDisplay,
diff --git a/aci/compose.go b/aci/compose.go
index 73828e7d..b9a6d120 100644
--- a/aci/compose.go
+++ b/aci/compose.go
@@ -46,18 +46,49 @@ func newComposeService(ctx store.AciContext) aciComposeService {
 
 func (cs *aciComposeService) Up(ctx context.Context, project *types.Project, detach bool) error {
 	logrus.Debugf("Up on project with name %q", project.Name)
+
+	if err := autocreateFileshares(ctx, project); err != nil {
+		return err
+	}
+
 	groupDefinition, err := convert.ToContainerGroup(ctx, cs.ctx, *project, cs.storageLogin)
+	if err != nil {
+		return err
+	}
+
 	addTag(&groupDefinition, composeContainerTag)
+	return createOrUpdateACIContainers(ctx, cs.ctx, groupDefinition)
+}
 
+func (cs aciComposeService) warnKeepVolumeOnDown(ctx context.Context, projectName string) error {
+	cgClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
 	if err != nil {
 		return err
 	}
-	return createOrUpdateACIContainers(ctx, cs.ctx, groupDefinition)
+	cg, err := cgClient.Get(ctx, cs.ctx.ResourceGroup, projectName)
+	if err != nil {
+		return err
+	}
+	if cg.Volumes == nil {
+		return nil
+	}
+	for _, v := range *cg.Volumes {
+		if v.AzureFile == nil || v.AzureFile.StorageAccountName == nil || v.AzureFile.ShareName == nil {
+			continue
+		}
+		fmt.Printf("WARNING: fileshare \"%s/%s\" will NOT be deleted. Use 'docker volume rm' if you want to delete this volume\n",
+			*v.AzureFile.StorageAccountName, *v.AzureFile.ShareName)
+	}
+	return nil
 }
 
 func (cs *aciComposeService) Down(ctx context.Context, project string) error {
 	logrus.Debugf("Down on project with name %q", project)
 
+	if err := cs.warnKeepVolumeOnDown(ctx, project); err != nil {
+		return err
+	}
+
 	cg, err := deleteACIContainerGroup(ctx, cs.ctx, project)
 	if err != nil {
 		return err
diff --git a/aci/convert/volume.go b/aci/convert/volume.go
index 51b6fd86..8bb2e9a3 100644
--- a/aci/convert/volume.go
+++ b/aci/convert/volume.go
@@ -32,21 +32,24 @@ import (
 )
 
 const (
-	azureFileDriverName            = "azure_file"
-	volumeDriveroptsShareNameKey   = "share_name"
-	volumeDriveroptsAccountNameKey = "storage_account_name"
+	// AzureFileDriverName driver name for azure file share
+	AzureFileDriverName = "azure_file"
+	// VolumeDriveroptsShareNameKey driver opt for fileshare name
+	VolumeDriveroptsShareNameKey = "share_name"
+	// VolumeDriveroptsAccountNameKey driver opt for storage account
+	VolumeDriveroptsAccountNameKey = "storage_account_name"
 	volumeReadOnly                 = "read_only"
 )
 
 func (p projectAciHelper) getAciFileVolumes(ctx context.Context, helper login.StorageLogin) ([]containerinstance.Volume, error) {
 	var azureFileVolumesSlice []containerinstance.Volume
 	for name, v := range p.Volumes {
-		if v.Driver == azureFileDriverName {
-			shareName, ok := v.DriverOpts[volumeDriveroptsShareNameKey]
+		if v.Driver == AzureFileDriverName {
+			shareName, ok := v.DriverOpts[VolumeDriveroptsShareNameKey]
 			if !ok {
 				return nil, fmt.Errorf("cannot retrieve fileshare name for Azurefile")
 			}
-			accountName, ok := v.DriverOpts[volumeDriveroptsAccountNameKey]
+			accountName, ok := v.DriverOpts[VolumeDriveroptsAccountNameKey]
 			if !ok {
 				return nil, fmt.Errorf("cannot retrieve account name for Azurefile")
 			}
@@ -105,15 +108,15 @@ func GetRunVolumes(volumes []string) (map[string]types.VolumeConfig, []types.Ser
 		readOnly := strconv.FormatBool(vi.readonly)
 		projectVolumes[vi.name] = types.VolumeConfig{
 			Name:   vi.name,
-			Driver: azureFileDriverName,
+			Driver: AzureFileDriverName,
 			DriverOpts: map[string]string{
-				volumeDriveroptsAccountNameKey: vi.storageAccount,
-				volumeDriveroptsShareNameKey:   vi.fileshare,
+				VolumeDriveroptsAccountNameKey: vi.storageAccount,
+				VolumeDriveroptsShareNameKey:   vi.fileshare,
 				volumeReadOnly:                 readOnly,
 			},
 		}
 		sv := types.ServiceVolumeConfig{
-			Type:     azureFileDriverName,
+			Type:     AzureFileDriverName,
 			Source:   vi.name,
 			Target:   vi.target,
 			ReadOnly: vi.readonly,
diff --git a/aci/volumes.go b/aci/volumes.go
index e7e3e0ad..242e3865 100644
--- a/aci/volumes.go
+++ b/aci/volumes.go
@@ -232,7 +232,7 @@ func (cs *aciVolumeService) Delete(ctx context.Context, id string, options inter
 	}
 
 	result, err := fileShareClient.Delete(ctx, cs.aciContext.ResourceGroup, storageAccount, fileshare)
-	if result.StatusCode == 204 {
+	if result.HasHTTPStatus(http.StatusNoContent) {
 		return errors.Wrapf(errdefs.ErrNotFound, "fileshare %q", fileshare)
 	}
 	return err
@@ -247,8 +247,11 @@ func (cs *aciVolumeService) Inspect(ctx context.Context, id string) (volumes.Vol
 	if err != nil {
 		return volumes.Volume{}, err
 	}
-	_, err = fileShareClient.Get(ctx, cs.aciContext.ResourceGroup, storageAccount, fileshareName, "")
+	res, err := fileShareClient.Get(ctx, cs.aciContext.ResourceGroup, storageAccount, fileshareName, "")
 	if err != nil { // Just checks if it exists
+		if res.HasHTTPStatus(http.StatusNotFound) {
+			return volumes.Volume{}, errors.Wrapf(errdefs.ErrNotFound, "account %q, file share %q. Original message %s", storageAccount, fileshareName, err.Error())
+		}
 		return volumes.Volume{}, err
 	}
 	return toVolume(storageAccount, fileshareName), nil
diff --git a/builder.Makefile b/builder.Makefile
index 179ea271..c89b2382 100644
--- a/builder.Makefile
+++ b/builder.Makefile
@@ -39,7 +39,6 @@ WORK_DIR:=$(shell mktemp -d)
 TAGS:=
 ifdef BUILD_TAGS
   TAGS=-tags $(BUILD_TAGS)
-  LINT_TAGS=--build-tags $(BUILD_TAGS)
 endif
 
 TAR_TRANSFORM:=--transform s/packaging/docker/ --transform s/bin/docker/ --transform s/docker-linux-amd64/docker/ --transform s/docker-darwin-amd64/docker/
@@ -69,7 +68,7 @@ test:
 
 .PHONY: lint
 lint:
-	golangci-lint run $(LINT_TAGS) --timeout 10m0s ./...
+	golangci-lint run --timeout 10m0s ./...
 
 .PHONY: import-restrictions
 import-restrictions:
diff --git a/local/backend.go b/local/backend.go
index 35c90198..78d1195c 100644
--- a/local/backend.go
+++ b/local/backend.go
@@ -20,7 +20,6 @@ package local
 
 import (
 	"context"
-
 	"github.com/docker/docker/client"
 
 	"github.com/docker/compose-cli/api/compose"
@@ -72,3 +71,6 @@ func (s *local) VolumeService() volumes.Service {
 func (s *local) ResourceService() resources.Service {
 	return nil
 }
+
+
+
diff --git a/local/compose.go b/local/compose.go
index 232a8f4b..14dc3685 100644
--- a/local/compose.go
+++ b/local/compose.go
@@ -22,14 +22,18 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"golang.org/x/sync/errgroup"
 	"io"
 	"path/filepath"
-	"sort"
 	"strconv"
 	"strings"
 	"sync"
 
 	"github.com/compose-spec/compose-go/types"
+	"github.com/docker/compose-cli/api/compose"
+	"github.com/docker/compose-cli/api/containers"
+	"github.com/docker/compose-cli/formatter"
+	"github.com/docker/compose-cli/progress"
 	moby "github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/filters"
@@ -41,12 +45,6 @@ import (
 	"github.com/docker/go-connections/nat"
 	"github.com/pkg/errors"
 	"github.com/sanathkr/go-yaml"
-	"golang.org/x/sync/errgroup"
-
-	"github.com/docker/compose-cli/api/compose"
-	"github.com/docker/compose-cli/api/containers"
-	"github.com/docker/compose-cli/formatter"
-	"github.com/docker/compose-cli/progress"
 )
 
 func (s *local) Up(ctx context.Context, project *types.Project, detach bool) error {
@@ -79,8 +77,8 @@ func (s *local) Up(ctx context.Context, project *types.Project, detach bool) err
 		}
 	}
 
-	err := inDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
-		return s.ensureService(c, project, service)
+	err := inDependencyOrder(ctx, project, func(service types.ServiceConfig) error {
+		return s.ensureService(ctx, project, service)
 	})
 	return err
 }
@@ -224,11 +222,11 @@ func (s *local) Logs(ctx context.Context, projectName string, w io.Writer) error
 	consumer := formatter.NewLogConsumer(w)
 	for _, c := range list {
 		service := c.Labels[serviceLabel]
-		containerID := c.ID
+		containerId := c.ID
 		go func() {
-			_ = s.containerService.Logs(ctx, containerID, containers.LogsRequest{
+			s.containerService.Logs(ctx,containerId, containers.LogsRequest{
 				Follow: true,
-				Writer: consumer.GetWriter(service, containerID),
+				Writer: consumer.GetWriter(service, containerId),
 			})
 			wg.Done()
 		}()
@@ -247,109 +245,30 @@ func (s *local) Ps(ctx context.Context, projectName string) ([]compose.ServiceSt
 	if err != nil {
 		return nil, err
 	}
-	return containersToServiceStatus(list)
-}
-
-func containersToServiceStatus(containers []moby.Container) ([]compose.ServiceStatus, error) {
-	containersByLabel, keys, err := groupContainerByLabel(containers, serviceLabel)
-	if err != nil {
-		return nil, err
-	}
-	var services []compose.ServiceStatus
-	for _, service := range keys {
-		containers := containersByLabel[service]
-		runnningContainers := []moby.Container{}
-		for _, container := range containers {
-			if container.State == "running" {
-				runnningContainers = append(runnningContainers, container)
-			}
-		}
-		services = append(services, compose.ServiceStatus{
-			ID:       service,
-			Name:     service,
-			Desired:  len(containers),
-			Replicas: len(runnningContainers),
+	var status []compose.ServiceStatus
+	for _, c := range list {
+		// TODO group by service
+		status = append(status, compose.ServiceStatus{
+			ID:         c.ID,
+			Name:       c.Labels[serviceLabel],
+			Replicas:   0,
+			Desired:    0,
+			Ports:      nil,
+			Publishers: nil,
 		})
 	}
-	return services, nil
+	return status, nil
 }
 
-func groupContainerByLabel(containers []moby.Container, labelName string) (map[string][]moby.Container, []string, error) {
-	containersByLabel := map[string][]moby.Container{}
-	keys := []string{}
-	for _, c := range containers {
-		label, ok := c.Labels[labelName]
-		if !ok {
-			return nil, nil, fmt.Errorf("No label %q set on container %q of compose project", labelName, c.ID)
-		}
-		labelContainers, ok := containersByLabel[label]
-		if !ok {
-			labelContainers = []moby.Container{}
-			keys = append(keys, label)
-		}
-		labelContainers = append(labelContainers, c)
-		containersByLabel[label] = labelContainers
-	}
-	sort.Strings(keys)
-	return containersByLabel, keys, nil
-}
 
 func (s *local) List(ctx context.Context, projectName string) ([]compose.Stack, error) {
-	list, err := s.containerService.apiClient.ContainerList(ctx, moby.ContainerListOptions{
-		Filters: filters.NewArgs(hasProjectLabelFilter()),
-	})
+	_, err := s.containerService.apiClient.ContainerList(ctx, moby.ContainerListOptions{All: true})
 	if err != nil {
 		return nil, err
 	}
-
-	return containersToStacks(list)
-}
-
-func containersToStacks(containers []moby.Container) ([]compose.Stack, error) {
-	containersByLabel, keys, err := groupContainerByLabel(containers, projectLabel)
-	if err != nil {
-		return nil, err
-	}
-	var projects []compose.Stack
-	for _, project := range keys {
-		projects = append(projects, compose.Stack{
-			ID:     project,
-			Name:   project,
-			Status: combinedStatus(containerToState(containersByLabel[project])),
-		})
-	}
-	return projects, nil
-}
-
-func containerToState(containers []moby.Container) []string {
-	statuses := []string{}
-	for _, c := range containers {
-		statuses = append(statuses, c.State)
-	}
-	return statuses
-}
-
-func combinedStatus(statuses []string) string {
-	nbByStatus := map[string]int{}
-	keys := []string{}
-	for _, status := range statuses {
-		nb, ok := nbByStatus[status]
-		if !ok {
-			nb = 0
-			keys = append(keys, status)
-		}
-		nbByStatus[status] = nb + 1
-	}
-	sort.Strings(keys)
-	result := ""
-	for _, status := range keys {
-		nb := nbByStatus[status]
-		if result != "" {
-			result = result + ", "
-		}
-		result = result + fmt.Sprintf("%s(%d)", status, nb)
-	}
-	return result
+	var stacks []compose.Stack
+	// TODO rebuild stacks based on containers
+	return stacks, nil
 }
 
 func (s *local) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) {
@@ -369,9 +288,9 @@ func getContainerCreateOptions(p *types.Project, s types.ServiceConfig, number i
 		return nil, nil, nil, err
 	}
 	labels := map[string]string{
-		projectLabel:         p.Name,
-		serviceLabel:         s.Name,
-		configHashLabel:      hash,
+		projectLabel:     p.Name,
+		serviceLabel:     s.Name,
+		configHashLabel: hash,
 		containerNumberLabel: strconv.Itoa(number),
 	}
 
@@ -415,14 +334,21 @@ func getContainerCreateOptions(p *types.Project, s types.ServiceConfig, number i
 		MacAddress:      s.MacAddress,
 		Labels:          labels,
 		StopSignal:      s.StopSignal,
-		Env:             toMobyEnv(s.Environment),
-		Healthcheck:     toMobyHealthCheck(s.HealthCheck),
+		//		Env:             s.Environment, FIXME conversion
+		//		Healthcheck:     s.HealthCheck, FIXME conversion
 		// Volumes:         // FIXME unclear to me the overlap with HostConfig.Mounts
-		StopTimeout: toSeconds(s.StopGracePeriod),
+		// StopTimeout: 	 s.StopGracePeriod FIXME conversion
+	}
+
+	mountOptions, err := buildContainerMountOptions(p, s, inherit)
+	if err != nil {
+		return nil, nil, nil, err
 	}
 
-	mountOptions := buildContainerMountOptions(p, s, inherit)
-	bindings := buildContainerBindingOptions(s)
+	bindings, err := buildContainerBindingOptions(s)
+	if err != nil {
+		return nil, nil, nil, err
+	}
 
 	networkMode := getNetworkMode(p, s)
 	hostConfig := container.HostConfig{
@@ -450,7 +376,7 @@ func buildContainerPorts(s types.ServiceConfig) nat.PortSet {
 	return ports
 }
 
-func buildContainerBindingOptions(s types.ServiceConfig) nat.PortMap {
+func buildContainerBindingOptions(s types.ServiceConfig) (nat.PortMap, error) {
 	bindings := nat.PortMap{}
 	for _, port := range s.Ports {
 		p := nat.Port(fmt.Sprintf("%d/%s", port.Target, port.Protocol))
@@ -462,10 +388,10 @@ func buildContainerBindingOptions(s types.ServiceConfig) nat.PortMap {
 		bind = append(bind, binding)
 		bindings[p] = bind
 	}
-	return bindings
+	return bindings, nil
 }
 
-func buildContainerMountOptions(p *types.Project, s types.ServiceConfig, inherit *moby.Container) []mount.Mount {
+func buildContainerMountOptions(p *types.Project, s types.ServiceConfig, inherit *moby.Container) ([]mount.Mount, error) {
 	mounts := []mount.Mount{}
 	var inherited []string
 	if inherit != nil {
@@ -508,7 +434,7 @@ func buildContainerMountOptions(p *types.Project, s types.ServiceConfig, inherit
 			TmpfsOptions:  buildTmpfsOptions(v.Tmpfs),
 		})
 	}
-	return mounts
+	return mounts, nil
 }
 
 func buildBindOption(bind *types.ServiceVolumeBind) *mount.BindOptions {
@@ -635,8 +561,9 @@ func (s *local) ensureNetwork(ctx context.Context, n types.NetworkConfig) error
 				Done:       true,
 			})
 			return nil
+		} else {
+			return err
 		}
-		return err
 	}
 	return nil
 }
diff --git a/local/convergence.go b/local/convergence.go
index 72689d77..cca0f966 100644
--- a/local/convergence.go
+++ b/local/convergence.go
@@ -21,30 +21,17 @@ package local
 import (
 	"context"
 	"fmt"
-	"strconv"
-	"time"
-
 	"github.com/compose-spec/compose-go/types"
+	"github.com/docker/compose-cli/api/containers"
+	"github.com/docker/compose-cli/progress"
 	moby "github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/filters"
 	"github.com/docker/docker/api/types/network"
 	"golang.org/x/sync/errgroup"
-
-	"github.com/docker/compose-cli/api/containers"
-	"github.com/docker/compose-cli/progress"
-)
-
-const (
-	extLifecycle  = "x-lifecycle"
-	forceRecreate = "force_recreate"
+	"strconv"
 )
 
 func (s *local) ensureService(ctx context.Context, project *types.Project, service types.ServiceConfig) error {
-	err := s.waitDependencies(ctx, project, service)
-	if err != nil {
-		return err
-	}
-
 	actual, err := s.containerService.apiClient.ContainerList(ctx, moby.ContainerListOptions{
 		Filters: filters.NewArgs(
 			filters.Arg("label", fmt.Sprintf("%s=%s", projectLabel, project.Name)),
@@ -91,11 +78,10 @@ func (s *local) ensureService(ctx context.Context, project *types.Project, servi
 	if err != nil {
 		return err
 	}
-
 	for _, container := range actual {
 		container := container
 		diverged := container.Labels[configHashLabel] != expected
-		if diverged || service.Extensions[extLifecycle] == forceRecreate {
+		if diverged {
 			eg.Go(func() error {
 				return s.recreateContainer(ctx, project, service, container)
 			})
@@ -114,30 +100,6 @@ func (s *local) ensureService(ctx context.Context, project *types.Project, servi
 	return eg.Wait()
 }
 
-func (s *local) waitDependencies(ctx context.Context, project *types.Project, service types.ServiceConfig) error {
-	eg, ctx := errgroup.WithContext(ctx)
-	for dep, config := range service.DependsOn {
-		switch config.Condition {
-		case "service_healthy":
-			eg.Go(func() error {
-				ticker := time.NewTicker(500 * time.Millisecond)
-				defer ticker.Stop()
-				for {
-					<-ticker.C
-					healthy, err := s.isServiceHealthy(ctx, project, dep)
-					if err != nil {
-						return err
-					}
-					if healthy {
-						return nil
-					}
-				}
-			})
-		}
-	}
-	return eg.Wait()
-}
-
 func nextContainerNumber(containers []moby.Container) (int, error) {
 	max := 0
 	for _, c := range containers {
@@ -220,23 +182,9 @@ func (s *local) recreateContainer(ctx context.Context, project *types.Project, s
 		StatusText: "Recreated",
 		Done:       true,
 	})
-	setDependentLifecycle(project, service.Name, forceRecreate)
 	return nil
 }
 
-// setDependentLifecycle define the Lifecycle strategy for all services to depend on specified service
-func setDependentLifecycle(project *types.Project, service string, strategy string) {
-	for i, s := range project.Services {
-		if contains(s.GetDependencies(), service) {
-			if s.Extensions == nil {
-				s.Extensions = map[string]interface{}{}
-			}
-			s.Extensions[extLifecycle] = strategy
-			project.Services[i] = s
-		}
-	}
-}
-
 func (s *local) restartContainer(ctx context.Context, service types.ServiceConfig, container moby.Container) error {
 	w := progress.ContextWriter(ctx)
 	w.Event(progress.Event{
@@ -281,6 +229,7 @@ func (s *local) runContainer(ctx context.Context, project *types.Project, servic
 	return nil
 }
 
+
 func (s *local) connectContainerToNetwork(ctx context.Context, id string, service string, n string) error {
 	err := s.containerService.apiClient.NetworkConnect(ctx, n, id, &network.EndpointSettings{
 		Aliases: []string{service},
@@ -290,33 +239,3 @@ func (s *local) connectContainerToNetwork(ctx context.Context, id string, servic
 	}
 	return nil
 }
-
-func (s *local) isServiceHealthy(ctx context.Context, project *types.Project, service string) (bool, error) {
-	containers, err := s.containerService.apiClient.ContainerList(ctx, moby.ContainerListOptions{
-		Filters: filters.NewArgs(
-			filters.Arg("label", fmt.Sprintf("%s=%s", projectLabel, project.Name)),
-			filters.Arg("label", fmt.Sprintf("%s=%s", serviceLabel, service)),
-		),
-	})
-	if err != nil {
-		return false, err
-	}
-
-	for _, c := range containers {
-		container, err := s.containerService.apiClient.ContainerInspect(ctx, c.ID)
-		if err != nil {
-			return false, err
-		}
-		if container.State == nil || container.State.Health == nil {
-			return false, fmt.Errorf("container for service %q has no healthcheck configured", service)
-		}
-		switch container.State.Health.Status {
-		case "starting":
-			return false, nil
-		case "unhealthy":
-			return false, nil
-		}
-	}
-	return true, nil
-
-}
diff --git a/local/convert.go b/local/convert.go
index f0b834a7..05931a36 100644
--- a/local/convert.go
+++ b/local/convert.go
@@ -23,9 +23,7 @@ import (
 	"sort"
 	"strconv"
 	"strings"
-	"time"
 
-	compose "github.com/compose-spec/compose-go/types"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/container"
 	"github.com/docker/go-connections/nat"
@@ -95,57 +93,6 @@ func toPorts(ports []types.Port) []containers.Port {
 	return result
 }
 
-func toMobyEnv(environment compose.MappingWithEquals) []string {
-	var env []string
-	for k, v := range environment {
-		if v == nil {
-			env = append(env, k)
-		} else {
-			env = append(env, fmt.Sprintf("%s=%s", k, *v))
-		}
-	}
-	return env
-}
-
-func toMobyHealthCheck(check *compose.HealthCheckConfig) *container.HealthConfig {
-	if check == nil {
-		return nil
-	}
-	var (
-		interval time.Duration
-		timeout  time.Duration
-		period   time.Duration
-		retries  int
-	)
-	if check.Interval != nil {
-		interval = time.Duration(*check.Interval)
-	}
-	if check.Timeout != nil {
-		timeout = time.Duration(*check.Timeout)
-	}
-	if check.StartPeriod != nil {
-		period = time.Duration(*check.StartPeriod)
-	}
-	if check.Retries != nil {
-		retries = int(*check.Retries)
-	}
-	return &container.HealthConfig{
-		Test:        check.Test,
-		Interval:    interval,
-		Timeout:     timeout,
-		StartPeriod: period,
-		Retries:     retries,
-	}
-}
-
-func toSeconds(d *compose.Duration) *int {
-	if d == nil {
-		return nil
-	}
-	s := int(time.Duration(*d).Seconds())
-	return &s
-}
-
 func fromPorts(ports []containers.Port) (map[nat.Port]struct{}, map[nat.Port][]nat.PortBinding, error) {
 	var (
 		exposedPorts = make(map[nat.Port]struct{}, len(ports))
diff --git a/local/dependencies.go b/local/dependencies.go
index e5ac9a77..d26d1c3c 100644
--- a/local/dependencies.go
+++ b/local/dependencies.go
@@ -20,94 +20,41 @@ package local
 
 import (
 	"context"
-
 	"github.com/compose-spec/compose-go/types"
 	"golang.org/x/sync/errgroup"
 )
 
-func inDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, types.ServiceConfig) error) error {
-	graph := buildDependencyGraph(project.Services)
-
+func inDependencyOrder(ctx context.Context, project *types.Project, fn func(types.ServiceConfig) error) error {
 	eg, ctx := errgroup.WithContext(ctx)
+	var (
+		scheduled []string
+		ready []string
+	)
 	results := make(chan string)
-	errors := make(chan error)
-	for len(graph) > 0 {
-		for _, n := range graph.independents() {
-			service := n.service
-			eg.Go(func() error {
-				err := fn(ctx, service)
-				if err != nil {
-					errors <- err
-					return err
-				}
-				results <- service.Name
-				return nil
-			})
+	for len(ready) < len(project.Services) {
+		for _, service := range project.Services {
+			if contains(scheduled, service.Name) {
+				continue
+			}
+			if containsAll(ready, service.GetDependencies()) {
+				service := service
+				scheduled = append(scheduled, service.Name)
+				eg.Go(func() error {
+					err := fn(service)
+					if err != nil {
+						close(results)
+						return err
+					}
+					results <- service.Name
+					return nil
+				})
+			}
 		}
-		select {
-		case result := <-results:
-			graph.resolved(result)
-		case err := <-errors:
-			return err
+		result, ok := <-results
+		if !ok {
+			break
 		}
+		ready = append(ready, result)
 	}
 	return eg.Wait()
 }
-
-type dependencyGraph map[string]node
-
-type node struct {
-	service      types.ServiceConfig
-	dependencies []string
-	dependent    []string
-}
-
-func (graph dependencyGraph) independents() []node {
-	var nodes []node
-	for _, node := range graph {
-		if len(node.dependencies) == 0 {
-			nodes = append(nodes, node)
-		}
-	}
-	return nodes
-}
-
-func (graph dependencyGraph) resolved(result string) {
-	for _, parent := range graph[result].dependent {
-		node := graph[parent]
-		node.dependencies = remove(node.dependencies, result)
-		graph[parent] = node
-	}
-	delete(graph, result)
-}
-
-func buildDependencyGraph(services types.Services) dependencyGraph {
-	graph := dependencyGraph{}
-	for _, s := range services {
-		graph[s.Name] = node{
-			service: s,
-		}
-	}
-
-	for _, s := range services {
-		node := graph[s.Name]
-		for _, name := range s.GetDependencies() {
-			dependency := graph[name]
-			node.dependencies = append(node.dependencies, name)
-			dependency.dependent = append(dependency.dependent, s.Name)
-			graph[name] = dependency
-		}
-		graph[s.Name] = node
-	}
-	return graph
-}
-
-func remove(slice []string, item string) []string {
-	var s []string
-	for _, i := range slice {
-		if i != item {
-			s = append(s, i)
-		}
-	}
-	return s
-}
diff --git a/local/labels.go b/local/labels.go
index 24dee33f..29966b4d 100644
--- a/local/labels.go
+++ b/local/labels.go
@@ -20,21 +20,16 @@ package local
 
 import (
 	"fmt"
-
 	"github.com/docker/docker/api/types/filters"
 )
 
 const (
-	projectLabel         = "com.docker.compose.project"
-	serviceLabel         = "com.docker.compose.service"
-	configHashLabel      = "com.docker.compose.config-hash"
+	projectLabel = "com.docker.compose.project"
+	serviceLabel = "com.docker.compose.service"
+	configHashLabel = "com.docker.compose.config-hash"
 	containerNumberLabel = "com.docker.compose.container-number"
 )
 
 func projectFilter(projectName string) filters.KeyValuePair {
 	return filters.Arg("label", fmt.Sprintf("%s=%s", projectLabel, projectName))
-}
-
-func hasProjectLabelFilter() filters.KeyValuePair {
-	return filters.Arg("label", projectLabel)
-}
+}
\ No newline at end of file
diff --git a/local/util.go b/local/util.go
index c49af75d..cabd257c 100644
--- a/local/util.go
+++ b/local/util.go
@@ -20,14 +20,13 @@ package local
 
 import (
 	"encoding/json"
-
 	"github.com/opencontainers/go-digest"
 )
 
 func jsonHash(o interface{}) (string, error) {
 	bytes, err := json.Marshal(o)
 	if err != nil {
-		return "", err
+		return "", nil
 	}
 	return digest.SHA256.FromBytes(bytes).String(), nil
 }
@@ -40,3 +39,12 @@ func contains(slice []string, item string) bool {
 	}
 	return false
 }
+
+func containsAll(slice []string, items []string) bool {
+	for _, i := range items {
+		if !contains(slice, i) {
+			return false
+		}
+	}
+	return true
+}


Test patch:
diff --git a/aci/convert/volume_test.go b/aci/convert/volume_test.go
index 36062bf7..1a8fea97 100644
--- a/aci/convert/volume_test.go
+++ b/aci/convert/volume_test.go
@@ -207,8 +207,8 @@ func getAzurefileVolumeConfig(name string, accountNameKey string, shareNameKey s
 		Name:   name,
 		Driver: "azure_file",
 		DriverOpts: map[string]string{
-			volumeDriveroptsAccountNameKey: accountNameKey,
-			volumeDriveroptsShareNameKey:   shareNameKey,
+			VolumeDriveroptsAccountNameKey: accountNameKey,
+			VolumeDriveroptsShareNameKey:   shareNameKey,
 			volumeReadOnly:                 strconv.FormatBool(readOnly),
 		},
 	}
diff --git a/local/compose_test.go b/local/compose_test.go
deleted file mode 100644
index 74de0745..00000000
--- a/local/compose_test.go
+++ /dev/null
@@ -1,109 +0,0 @@
-// +build local
-
-/*
-   Copyright 2020 Docker Compose CLI authors
-
-   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 local
-
-import (
-	"testing"
-
-	"github.com/docker/docker/api/types"
-	"gotest.tools/v3/assert"
-
-	"github.com/docker/compose-cli/api/compose"
-)
-
-func TestContainersToStacks(t *testing.T) {
-	containers := []types.Container{
-		{
-			ID:     "service1",
-			State:  "running",
-			Labels: map[string]string{projectLabel: "project1"},
-		},
-		{
-			ID:     "service2",
-			State:  "running",
-			Labels: map[string]string{projectLabel: "project1"},
-		},
-		{
-			ID:     "service3",
-			State:  "running",
-			Labels: map[string]string{projectLabel: "project2"},
-		},
-	}
-	stacks, err := containersToStacks(containers)
-	assert.NilError(t, err)
-	assert.DeepEqual(t, stacks, []compose.Stack{
-		{
-			ID:     "project1",
-			Name:   "project1",
-			Status: "running(2)",
-		},
-		{
-			ID:     "project2",
-			Name:   "project2",
-			Status: "running(1)",
-		},
-	})
-}
-
-func TestContainersToServiceStatus(t *testing.T) {
-	containers := []types.Container{
-		{
-			ID:     "c1",
-			State:  "running",
-			Labels: map[string]string{serviceLabel: "service1"},
-		},
-		{
-			ID:     "c2",
-			State:  "exited",
-			Labels: map[string]string{serviceLabel: "service1"},
-		},
-		{
-			ID:     "c3",
-			State:  "running",
-			Labels: map[string]string{serviceLabel: "service1"},
-		},
-		{
-			ID:     "c4",
-			State:  "running",
-			Labels: map[string]string{serviceLabel: "service2"},
-		},
-	}
-	services, err := containersToServiceStatus(containers)
-	assert.NilError(t, err)
-	assert.DeepEqual(t, services, []compose.ServiceStatus{
-		{
-			ID:       "service1",
-			Name:     "service1",
-			Replicas: 2,
-			Desired:  3,
-		},
-		{
-			ID:       "service2",
-			Name:     "service2",
-			Replicas: 1,
-			Desired:  1,
-		},
-	})
-}
-
-func TestStacksMixedStatus(t *testing.T) {
-	assert.Equal(t, combinedStatus([]string{"running"}), "running(1)")
-	assert.Equal(t, combinedStatus([]string{"running", "running", "running"}), "running(3)")
-	assert.Equal(t, combinedStatus([]string{"running", "exited", "running"}), "exited(1), running(2)")
-}
diff --git a/local/dependencies_test.go b/local/dependencies_test.go
index 3dc62f69..035eda4e 100644
--- a/local/dependencies_test.go
+++ b/local/dependencies_test.go
@@ -20,9 +20,8 @@ package local
 
 import (
 	"context"
-	"testing"
-
 	"gotest.tools/v3/assert"
+	"testing"
 
 	"github.com/compose-spec/compose-go/types"
 )
@@ -32,28 +31,27 @@ func TestInDependencyOrder(t *testing.T) {
 	project := types.Project{
 		Services: []types.ServiceConfig{
 			{
-				Name: "test1",
+				Name:            "test1",
 				DependsOn: map[string]types.ServiceDependency{
 					"test2": {},
 				},
 			},
 			{
-				Name: "test2",
+				Name:            "test2",
 				DependsOn: map[string]types.ServiceDependency{
 					"test3": {},
 				},
 			},
 			{
-				Name: "test3",
+				Name:            "test3",
 			},
 		},
 	}
-	//nolint:errcheck, unparam
-	go inDependencyOrder(context.TODO(), &project, func(ctx context.Context, config types.ServiceConfig) error {
+	go inDependencyOrder(context.TODO(), &project, func(config types.ServiceConfig) error {
 		order <- config.Name
 		return nil
 	})
-	assert.Equal(t, <-order, "test3")
-	assert.Equal(t, <-order, "test2")
-	assert.Equal(t, <-order, "test1")
-}
+	assert.Equal(t, <- order, "test3")
+	assert.Equal(t, <- order, "test2")
+	assert.Equal(t, <- order, "test1")
+}
\ No newline at end of file
diff --git a/metrics/conn_e2e.go b/metrics/conn_e2e.go
index 53e87c9e..86d83715 100644
--- a/metrics/conn_e2e.go
+++ b/metrics/conn_e2e.go
@@ -22,6 +22,7 @@ import (
 	"os"
 )
 
+
 func init() {
 	testSocket, defined := os.LookupEnv("TEST_METRICS_SOCKET")
 	if defined {
diff --git a/tests/aci-e2e/e2e-aci_test.go b/tests/aci-e2e/e2e-aci_test.go
index d3820dcd..8c860a55 100644
--- a/tests/aci-e2e/e2e-aci_test.go
+++ b/tests/aci-e2e/e2e-aci_test.go
@@ -644,9 +644,7 @@ func TestUpUpdate(t *testing.T) {
 	)
 	c := NewParallelE2eCLI(t, binDir)
 	sID, groupID, location := setupTestResourceGroup(t, c)
-	composeAccountName := groupID + "-sa"
-	composeAccountName = strings.ReplaceAll(composeAccountName, "-", "")
-	composeAccountName = strings.ToLower(composeAccountName)
+	composeAccountName := strings.ToLower(strings.ReplaceAll(groupID, "-", "") + "sa")
 
 	dstDir := filepath.Join(os.TempDir(), "e2e-aci-volume-"+composeAccountName)
 	srcDir := filepath.Join("..", "composefiles", "aci-demo")
@@ -663,28 +661,33 @@ func TestUpUpdate(t *testing.T) {
 	multiPortComposefile = filepath.Join(dstDir, multiPortComposefile)
 
 	volumeID := composeAccountName + "/" + fileshareName
-	t.Run("compose up", func(t *testing.T) {
-		const (
-			testFileName    = "msg.txt"
-			testFileContent = "VOLUME_OK"
-			projectName     = "acidemo"
-		)
-
-		c.RunDockerCmd("volume", "create", "--storage-account", composeAccountName, fileshareName)
+	const (
+		testFileName    = "msg.txt"
+		testFileContent = "VOLUME_OK"
+		projectName     = "acidemo"
+	)
+	var (
+		dnsLabelName = "nginx-" + groupID
+		fqdn         = dnsLabelName + "." + location + ".azurecontainer.io"
+	)
 
-		// Bootstrap volume
+	t.Run("compose up", func(t *testing.T) {
 		aciContext := store.AciContext{
 			SubscriptionID: sID,
 			Location:       location,
 			ResourceGroup:  groupID,
 		}
+		c.RunDockerCmd("compose", "up", "-f", singlePortVolumesComposefile, "--domainname", dnsLabelName, "--project-name", projectName)
+
+		// Volume should be autocreated by the "compose up"
 		uploadTestFile(t, aciContext, composeAccountName, fileshareName, testFileName, testFileContent)
+	})
 
-		dnsLabelName := "nginx-" + groupID
-		fqdn := dnsLabelName + "." + location + ".azurecontainer.io"
-		// Name of Compose project is taken from current folder "acie2e"
-		c.RunDockerCmd("compose", "up", "-f", singlePortVolumesComposefile, "--domainname", dnsLabelName, "--project-name", projectName)
+	t.Cleanup(func() {
+		c.RunDockerCmd("volume", "rm", volumeID)
+	})
 
+	t.Run("check deployed compose app", func(t *testing.T) {
 		res := c.RunDockerCmd("ps")
 		out := lines(res.Stdout())
 		// Check three containers are running
@@ -723,9 +726,6 @@ func TestUpUpdate(t *testing.T) {
 				composeAccountName, fileshareName, projectName),
 		})
 	})
-	t.Cleanup(func() {
-		c.RunDockerCmd("volume", "rm", volumeID)
-	})
 
 	t.Run("compose ps", func(t *testing.T) {
 		res := c.RunDockerCmd("compose", "ps", "--project-name", composeProjectName, "--quiet")