diff --git a/local/compose.go b/local/compose.go index 7bf41d31..260d2d14 100644 --- a/local/compose.go +++ b/local/compose.go @@ -28,7 +28,6 @@ import ( "sync" "github.com/opencontainers/go-digest" - "golang.org/x/sync/errgroup" "github.com/compose-spec/compose-go/types" "github.com/docker/compose-cli/api/compose" @@ -71,113 +70,115 @@ func (s *local) Up(ctx context.Context, project *types.Project, detach bool) err } } - w := progress.ContextWriter(ctx) - eg, ctx := errgroup.WithContext(ctx) for _, service := range project.Services { - service := service - eg.Go(func() error { - err := s.applyPullPolicy(ctx, service) - if err != nil { - return err - } - - actual, err := s.containerService.apiClient.ContainerList(ctx, moby.ContainerListOptions{ - Filters: filters.NewArgs( - filters.Arg("label", "com.docker.compose.project="+project.Name), - filters.Arg("label", "com.docker.compose.service="+service.Name), - ), - }) - if err != nil { - return err - } - - expected, err := jsonHash(s) - if err != nil { - return err - } - - if len(actual) == 0 { - w.Event(progress.Event{ - ID: fmt.Sprintf("Service %q", service.Name), - Status: progress.Working, - StatusText: "Create", - Done: false, - }) - name := fmt.Sprintf("%s_%s", project.Name, service.Name) - err = s.runContainer(ctx, project, service, name, nil) - if err != nil { - return err - } - w.Event(progress.Event{ - ID: fmt.Sprintf("Service %q", service.Name), - Status: progress.Done, - StatusText: "Created", - Done: true, - }) - return nil - } - - container := actual[0] - diverged := container.Labels["com.docker.compose.config-hash"] != expected - if diverged { - w.Event(progress.Event{ - ID: fmt.Sprintf("Service %q", service.Name), - Status: progress.Working, - StatusText: "Recreate", - Done: false, - }) - err := s.containerService.Stop(ctx, container.ID, nil) - if err != nil { - return err - } - name := getContainerName(container) - tmpName := fmt.Sprintf("%s_%s", container.ID[:12], name) - err = s.containerService.apiClient.ContainerRename(ctx, container.ID, tmpName) - if err != nil { - return err - } - err = s.runContainer(ctx, project, service, name, &container) - if err != nil { - return err - } - err = s.containerService.Delete(ctx, container.ID, containers.DeleteRequest{}) - if err != nil { - return err - } - w.Event(progress.Event{ - ID: fmt.Sprintf("Service %q", service.Name), - Status: progress.Done, - StatusText: "Recreated", - Done: true, - }) - return nil - } - - if container.State == "running" { - // already running, skip - return nil - } - - w.Event(progress.Event{ - ID: fmt.Sprintf("Service %q", service.Name), - Status: progress.Working, - StatusText: "Restart", - Done: false, - }) - err = s.containerService.Start(ctx, container.ID) - if err != nil { - return err - } - w.Event(progress.Event{ - ID: fmt.Sprintf("Service %q", service.Name), - Status: progress.Done, - StatusText: "Restarted", - Done: true, - }) - return nil - }) + err := s.applyPullPolicy(ctx, service) + if err != nil { + return err + } } - return eg.Wait() + + err := inDependencyOrder(ctx, project, func(service types.ServiceConfig) error { + return s.ensureService(ctx, project, service) + }) + return err +} + +func (s *local) ensureService(ctx context.Context, project *types.Project, service types.ServiceConfig) error { + actual, err := s.containerService.apiClient.ContainerList(ctx, moby.ContainerListOptions{ + Filters: filters.NewArgs( + filters.Arg("label", "com.docker.compose.project="+project.Name), + filters.Arg("label", "com.docker.compose.service="+service.Name), + ), + }) + if err != nil { + return err + } + + expected, err := jsonHash(s) + if err != nil { + return err + } + + w := progress.ContextWriter(ctx) + if len(actual) == 0 { + w.Event(progress.Event{ + ID: fmt.Sprintf("Service %q", service.Name), + Status: progress.Working, + StatusText: "Create", + Done: false, + }) + name := fmt.Sprintf("%s_%s", project.Name, service.Name) + err = s.runContainer(ctx, project, service, name, nil) + if err != nil { + return err + } + w.Event(progress.Event{ + ID: fmt.Sprintf("Service %q", service.Name), + Status: progress.Done, + StatusText: "Created", + Done: true, + }) + return nil + } + + container := actual[0] + diverged := container.Labels["com.docker.compose.config-hash"] != expected + if diverged { + w.Event(progress.Event{ + ID: fmt.Sprintf("Service %q", service.Name), + Status: progress.Working, + StatusText: "Recreate", + Done: false, + }) + err := s.containerService.Stop(ctx, container.ID, nil) + if err != nil { + return err + } + name := getContainerName(container) + tmpName := fmt.Sprintf("%s_%s", container.ID[:12], name) + err = s.containerService.apiClient.ContainerRename(ctx, container.ID, tmpName) + if err != nil { + return err + } + err = s.runContainer(ctx, project, service, name, &container) + if err != nil { + return err + } + err = s.containerService.Delete(ctx, container.ID, containers.DeleteRequest{}) + if err != nil { + return err + } + w.Event(progress.Event{ + ID: fmt.Sprintf("Service %q", service.Name), + Status: progress.Done, + StatusText: "Recreated", + Done: true, + }) + return nil + } + + if container.State == "running" { + // already running, skip + return nil + } + + w.Event(progress.Event{ + ID: fmt.Sprintf("Service %q", service.Name), + Status: progress.Working, + StatusText: "Restart", + Done: false, + }) + err = s.containerService.Start(ctx, container.ID) + if err != nil { + return err + } + w.Event(progress.Event{ + ID: fmt.Sprintf("Service %q", service.Name), + Status: progress.Done, + StatusText: "Restarted", + Done: true, + }) + return nil } func getContainerName(c moby.Container) string { @@ -711,3 +712,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 +} \ No newline at end of file diff --git a/local/dependencies.go b/local/dependencies.go new file mode 100644 index 00000000..d26d1c3c --- /dev/null +++ b/local/dependencies.go @@ -0,0 +1,60 @@ +// +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 ( + "context" + "github.com/compose-spec/compose-go/types" + "golang.org/x/sync/errgroup" +) + +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) + 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 + }) + } + } + result, ok := <-results + if !ok { + break + } + ready = append(ready, result) + } + return eg.Wait() +} diff --git a/local/dependencies_test.go b/local/dependencies_test.go new file mode 100644 index 00000000..035eda4e --- /dev/null +++ b/local/dependencies_test.go @@ -0,0 +1,57 @@ +// +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 ( + "context" + "gotest.tools/v3/assert" + "testing" + + "github.com/compose-spec/compose-go/types" +) + +func TestInDependencyOrder(t *testing.T) { + order := make(chan string) + project := types.Project{ + Services: []types.ServiceConfig{ + { + Name: "test1", + DependsOn: map[string]types.ServiceDependency{ + "test2": {}, + }, + }, + { + Name: "test2", + DependsOn: map[string]types.ServiceDependency{ + "test3": {}, + }, + }, + { + Name: "test3", + }, + }, + } + 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") +} \ No newline at end of file