From 6346db1d6f5b65f9fd6744e5de2c763068c4a425 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Tue, 17 Nov 2020 10:15:07 +0100 Subject: [PATCH] Move reconciliation logic into convergence.go Signed-off-by: Nicolas De Loof --- local/compose.go | 158 -------------------------------------- local/convergence.go | 178 +++++++++++++++++++++++++++++++++++++++++++ local/util.go | 50 ++++++++++++ 3 files changed, 228 insertions(+), 158 deletions(-) create mode 100644 local/convergence.go create mode 100644 local/util.go diff --git a/local/compose.go b/local/compose.go index 260d2d143..d63b262dc 100644 --- a/local/compose.go +++ b/local/compose.go @@ -27,8 +27,6 @@ import ( "strings" "sync" - "github.com/opencontainers/go-digest" - "github.com/compose-spec/compose-go/types" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/containers" @@ -83,104 +81,6 @@ func (s *local) Up(ctx context.Context, project *types.Project, detach bool) err 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 { // Names return container canonical name /foo + link aliases /linked_by/foo for _, name := range c.Names { @@ -191,29 +91,6 @@ func getContainerName(c moby.Container) string { return c.Names[0][1:] } -func (s *local) runContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, name string, container *moby.Container) error { - containerConfig, hostConfig, networkingConfig, err := getContainerCreateOptions(project, service, container) - if err != nil { - return err - } - id, err := s.containerService.create(ctx, containerConfig, hostConfig, networkingConfig, name) - if err != nil { - return err - } - for net := range service.Networks { - name := fmt.Sprintf("%s_%s", project.Name, net) - err = s.connectContainerToNetwork(ctx, id, service.Name, name) - if err != nil { - return err - } - } - err = s.containerService.apiClient.ContainerStart(ctx, id, moby.ContainerStartOptions{}) - if err != nil { - return err - } - return nil -} - func (s *local) applyPullPolicy(ctx context.Context, service types.ServiceConfig) error { w := progress.ContextWriter(ctx) // TODO build vs pull should be controlled by pull policy @@ -657,15 +534,6 @@ func (s *local) ensureNetwork(ctx context.Context, n types.NetworkConfig) error 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}, - }) - if err != nil { - return err - } - return nil -} func (s *local) ensureVolume(ctx context.Context, volume types.VolumeConfig) error { // TODO could identify volume by label vs name @@ -695,29 +563,3 @@ func (s *local) ensureVolume(ctx context.Context, volume types.VolumeConfig) err } return nil } - -func jsonHash(o interface{}) (string, error) { - bytes, err := json.Marshal(o) - if err != nil { - return "", nil - } - return digest.SHA256.FromBytes(bytes).String(), nil -} - -func contains(slice []string, item string) bool { - for _, v := range slice { - if v == item { - return true - } - } - 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/convergence.go b/local/convergence.go new file mode 100644 index 000000000..b7c3494a6 --- /dev/null +++ b/local/convergence.go @@ -0,0 +1,178 @@ +// +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" + "fmt" + "github.com/docker/docker/api/types/network" + + "github.com/docker/compose-cli/api/containers" + "github.com/docker/compose-cli/progress" + + "github.com/compose-spec/compose-go/types" + moby "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" +) + +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(service) + if err != nil { + return err + } + + if len(actual) == 0 { + return s.createContainer(ctx, project, service) + } + + container := actual[0] // TODO handle services with replicas + diverged := container.Labels["com.docker.compose.config-hash"] != expected + if diverged { + return s.recreateContainer(ctx, project, service, container) + } + + if container.State == "running" { + // already running, skip + return nil + } + + return s.restartContainer(ctx, service, container) +} + +func (s *local) createContainer(ctx context.Context, project *types.Project, service types.ServiceConfig) error { + w := progress.ContextWriter(ctx) + 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 +} + +func (s *local) recreateContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, container moby.Container) error { + w := progress.ContextWriter(ctx) + 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 +} + +func (s *local) restartContainer(ctx context.Context, service types.ServiceConfig, container moby.Container) error { + w := progress.ContextWriter(ctx) + 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 (s *local) runContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, name string, container *moby.Container) error { + containerConfig, hostConfig, networkingConfig, err := getContainerCreateOptions(project, service, container) + if err != nil { + return err + } + id, err := s.containerService.create(ctx, containerConfig, hostConfig, networkingConfig, name) + if err != nil { + return err + } + for net := range service.Networks { + name := fmt.Sprintf("%s_%s", project.Name, net) + err = s.connectContainerToNetwork(ctx, id, service.Name, name) + if err != nil { + return err + } + } + err = s.containerService.apiClient.ContainerStart(ctx, id, moby.ContainerStartOptions{}) + if err != nil { + return err + } + 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}, + }) + if err != nil { + return err + } + return nil +} diff --git a/local/util.go b/local/util.go new file mode 100644 index 000000000..cabd257c4 --- /dev/null +++ b/local/util.go @@ -0,0 +1,50 @@ +// +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 ( + "encoding/json" + "github.com/opencontainers/go-digest" +) + +func jsonHash(o interface{}) (string, error) { + bytes, err := json.Marshal(o) + if err != nil { + return "", nil + } + return digest.SHA256.FromBytes(bytes).String(), nil +} + +func contains(slice []string, item string) bool { + for _, v := range slice { + if v == item { + return true + } + } + return false +} + +func containsAll(slice []string, items []string) bool { + for _, i := range items { + if !contains(slice, i) { + return false + } + } + return true +}