mirror of https://github.com/docker/compose.git
Merge pull request #1841 from ndeloof/convergence
This commit is contained in:
commit
d20c3b0e22
|
@ -59,7 +59,7 @@ func TestIPC(t *testing.T) {
|
|||
t.Run("down", func(t *testing.T) {
|
||||
_ = c.RunDockerCmd("compose", "--project-name", projectName, "down")
|
||||
})
|
||||
t.Run("stop ipc mode container", func(t *testing.T) {
|
||||
_ = c.RunDockerCmd("stop", "ipc_mode_container")
|
||||
t.Run("remove ipc mode container", func(t *testing.T) {
|
||||
_ = c.RunDockerCmd("rm", "-f", "ipc_mode_container")
|
||||
})
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ func TestNetworkAliassesAndLinks(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("curl links", func(t *testing.T) {
|
||||
res := c.RunDockerCmd("compose", "-f", "./fixtures/network-alias/compose.yaml", "--project-name", projectName, "exec", "-T", "container1", "curl", "container")
|
||||
res := c.RunDockerCmd("compose", "-f", "./fixtures/network-alias/compose.yaml", "--project-name", projectName, "exec", "-T", "container1", "curl", "http://container/")
|
||||
assert.Assert(t, strings.Contains(res.Stdout(), "Welcome to nginx!"), res.Stdout())
|
||||
})
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
|
@ -46,76 +47,147 @@ const (
|
|||
"Remove the custom name to scale the service.\n"
|
||||
)
|
||||
|
||||
func (s *composeService) ensureScale(ctx context.Context, project *types.Project, service types.ServiceConfig, timeout *time.Duration) (*errgroup.Group, []moby.Container, error) {
|
||||
cState, err := GetContextContainerState(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
observedState := cState.GetContainers()
|
||||
actual := observedState.filter(isService(service.Name)).filter(isNotOneOff)
|
||||
scale, err := getScale(service)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
if len(actual) < scale {
|
||||
next, err := nextContainerNumber(actual)
|
||||
if err != nil {
|
||||
return nil, actual, err
|
||||
}
|
||||
missing := scale - len(actual)
|
||||
for i := 0; i < missing; i++ {
|
||||
number := next + i
|
||||
name := getContainerName(project.Name, service, number)
|
||||
eg.Go(func() error {
|
||||
return s.createContainer(ctx, project, service, name, number, false, true)
|
||||
})
|
||||
}
|
||||
}
|
||||
// convergence manages service's container lifecycle.
|
||||
// Based on initially observed state, it reconciles the existing container with desired state, which might include
|
||||
// re-creating container, adding or removing replicas, or starting stopped containers.
|
||||
// Cross services dependencies are managed by creating services in expected order and updating `service:xx` reference
|
||||
// when a service has converged, so dependent ones can be managed with resolved containers references.
|
||||
type convergence struct {
|
||||
service *composeService
|
||||
observedState map[string]Containers
|
||||
}
|
||||
|
||||
if len(actual) > scale {
|
||||
for i := scale; i < len(actual); i++ {
|
||||
container := actual[i]
|
||||
func newConvergence(services []string, state Containers, s *composeService) *convergence {
|
||||
observedState := map[string]Containers{}
|
||||
for _, s := range services {
|
||||
observedState[s] = Containers{}
|
||||
}
|
||||
for _, c := range state.filter(isNotOneOff) {
|
||||
service := c.Labels[api.ServiceLabel]
|
||||
observedState[service] = append(observedState[service], c)
|
||||
}
|
||||
return &convergence{
|
||||
service: s,
|
||||
observedState: observedState,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *convergence) apply(ctx context.Context, project *types.Project, options api.CreateOptions) error {
|
||||
return InDependencyOrder(ctx, project, func(ctx context.Context, name string) error {
|
||||
service, err := project.GetService(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
strategy := options.RecreateDependencies
|
||||
if utils.StringContains(options.Services, name) {
|
||||
strategy = options.Recreate
|
||||
}
|
||||
err = c.ensureService(ctx, project, service, strategy, options.Inherit, options.Timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.updateProject(project, name)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var mu sync.Mutex
|
||||
|
||||
// updateProject updates project after service converged, so dependent services relying on `service:xx` can refer to actual containers.
|
||||
func (c *convergence) updateProject(project *types.Project, service string) {
|
||||
containers := c.observedState[service]
|
||||
container := containers[0]
|
||||
|
||||
// operation is protected by a Mutex so that we can safely update project.Services while running concurrent convergence on services
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
for i, s := range project.Services {
|
||||
if d := getDependentServiceFromMode(s.NetworkMode); d == service {
|
||||
s.NetworkMode = types.NetworkModeContainerPrefix + container.ID
|
||||
}
|
||||
if d := getDependentServiceFromMode(s.Ipc); d == service {
|
||||
s.Ipc = types.NetworkModeContainerPrefix + container.ID
|
||||
}
|
||||
if d := getDependentServiceFromMode(s.Pid); d == service {
|
||||
s.Pid = types.NetworkModeContainerPrefix + container.ID
|
||||
}
|
||||
var links []string
|
||||
for _, serviceLink := range s.Links {
|
||||
parts := strings.Split(serviceLink, ":")
|
||||
serviceName := serviceLink
|
||||
serviceAlias := ""
|
||||
if len(parts) == 2 {
|
||||
serviceName = parts[0]
|
||||
serviceAlias = parts[1]
|
||||
}
|
||||
if serviceName != service {
|
||||
links = append(links, serviceLink)
|
||||
continue
|
||||
}
|
||||
for _, container := range containers {
|
||||
name := getCanonicalContainerName(container)
|
||||
if serviceAlias != "" {
|
||||
links = append(links,
|
||||
fmt.Sprintf("%s:%s", name, serviceAlias))
|
||||
}
|
||||
links = append(links,
|
||||
fmt.Sprintf("%s:%s", name, name),
|
||||
fmt.Sprintf("%s:%s", name, getContainerNameWithoutProject(container)))
|
||||
}
|
||||
s.Links = links
|
||||
}
|
||||
project.Services[i] = s
|
||||
}
|
||||
}
|
||||
|
||||
func (c *convergence) ensureService(ctx context.Context, project *types.Project, service types.ServiceConfig, recreate string, inherit bool, timeout *time.Duration) error {
|
||||
expected, err := getScale(service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
containers := c.observedState[service.Name]
|
||||
actual := len(containers)
|
||||
updated := make(Containers, expected)
|
||||
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
|
||||
for i, container := range containers {
|
||||
if i > expected {
|
||||
// Scale Down
|
||||
eg.Go(func() error {
|
||||
err := s.apiClient.ContainerStop(ctx, container.ID, timeout)
|
||||
err := c.service.apiClient.ContainerStop(ctx, container.ID, timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.apiClient.ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{})
|
||||
})
|
||||
}
|
||||
actual = actual[:scale]
|
||||
}
|
||||
return eg, actual, nil
|
||||
}
|
||||
|
||||
func (s *composeService) ensureService(ctx context.Context, project *types.Project, service types.ServiceConfig, recreate string, inherit bool, timeout *time.Duration) error {
|
||||
eg, actual, err := s.ensureScale(ctx, project, service, timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if recreate == api.RecreateNever {
|
||||
return nil
|
||||
}
|
||||
|
||||
expected, err := ServiceHash(service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, container := range actual {
|
||||
container := container
|
||||
name := getContainerProgressName(container)
|
||||
|
||||
diverged := container.Labels[api.ConfigHashLabel] != expected
|
||||
if diverged || recreate == api.RecreateForce || service.Extensions[extLifecycle] == forceRecreate {
|
||||
eg.Go(func() error {
|
||||
return s.recreateContainer(ctx, project, service, container, inherit, timeout)
|
||||
return c.service.apiClient.ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{})
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if recreate == api.RecreateNever {
|
||||
continue
|
||||
}
|
||||
// Re-create diverged containers
|
||||
configHash, err := ServiceHash(service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := getContainerProgressName(container)
|
||||
diverged := container.Labels[api.ConfigHashLabel] != configHash
|
||||
if diverged || recreate == api.RecreateForce || service.Extensions[extLifecycle] == forceRecreate {
|
||||
i := i
|
||||
eg.Go(func() error {
|
||||
recreated, err := c.service.recreateContainer(ctx, project, service, container, inherit, timeout)
|
||||
updated[i] = recreated
|
||||
return err
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Enforce non-diverged containers are running
|
||||
w := progress.ContextWriter(ctx)
|
||||
switch container.State {
|
||||
case ContainerRunning:
|
||||
|
@ -126,11 +198,31 @@ func (s *composeService) ensureService(ctx context.Context, project *types.Proje
|
|||
w.Event(progress.CreatedEvent(name))
|
||||
default:
|
||||
eg.Go(func() error {
|
||||
return s.startContainer(ctx, container)
|
||||
return c.service.startContainer(ctx, container)
|
||||
})
|
||||
}
|
||||
updated[i] = container
|
||||
}
|
||||
return eg.Wait()
|
||||
|
||||
next, err := nextContainerNumber(containers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := 0; i < expected-actual; i++ {
|
||||
// Scale UP
|
||||
number := next + i
|
||||
name := getContainerName(project.Name, service, number)
|
||||
eg.Go(func() error {
|
||||
container, err := c.service.createContainer(ctx, project, service, name, number, false, true)
|
||||
updated[actual+i-1] = container
|
||||
return err
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
err = eg.Wait()
|
||||
c.observedState[service.Name] = updated
|
||||
return err
|
||||
}
|
||||
|
||||
func getContainerName(projectName string, service types.ServiceConfig, number int) string {
|
||||
|
@ -220,51 +312,54 @@ func getScale(config types.ServiceConfig) (int, error) {
|
|||
return scale, err
|
||||
}
|
||||
|
||||
func (s *composeService) createContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, name string, number int, autoRemove bool, useNetworkAliases bool) error {
|
||||
func (s *composeService) createContainer(ctx context.Context, project *types.Project, service types.ServiceConfig,
|
||||
name string, number int, autoRemove bool, useNetworkAliases bool) (container moby.Container, err error) {
|
||||
w := progress.ContextWriter(ctx)
|
||||
eventName := "Container " + name
|
||||
w.Event(progress.CreatingEvent(eventName))
|
||||
err := s.createMobyContainer(ctx, project, service, name, number, nil, autoRemove, useNetworkAliases)
|
||||
container, err = s.createMobyContainer(ctx, project, service, name, number, nil, autoRemove, useNetworkAliases)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
w.Event(progress.CreatedEvent(eventName))
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
func (s *composeService) recreateContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, container moby.Container, inherit bool, timeout *time.Duration) error {
|
||||
func (s *composeService) recreateContainer(ctx context.Context, project *types.Project, service types.ServiceConfig,
|
||||
replaced moby.Container, inherit bool, timeout *time.Duration) (moby.Container, error) {
|
||||
var created moby.Container
|
||||
w := progress.ContextWriter(ctx)
|
||||
w.Event(progress.NewEvent(getContainerProgressName(container), progress.Working, "Recreate"))
|
||||
err := s.apiClient.ContainerStop(ctx, container.ID, timeout)
|
||||
w.Event(progress.NewEvent(getContainerProgressName(replaced), progress.Working, "Recreate"))
|
||||
err := s.apiClient.ContainerStop(ctx, replaced.ID, timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
return created, err
|
||||
}
|
||||
name := getCanonicalContainerName(container)
|
||||
tmpName := fmt.Sprintf("%s_%s", container.ID[:12], name)
|
||||
err = s.apiClient.ContainerRename(ctx, container.ID, tmpName)
|
||||
name := getCanonicalContainerName(replaced)
|
||||
tmpName := fmt.Sprintf("%s_%s", replaced.ID[:12], name)
|
||||
err = s.apiClient.ContainerRename(ctx, replaced.ID, tmpName)
|
||||
if err != nil {
|
||||
return err
|
||||
return created, err
|
||||
}
|
||||
number, err := strconv.Atoi(container.Labels[api.ContainerNumberLabel])
|
||||
number, err := strconv.Atoi(replaced.Labels[api.ContainerNumberLabel])
|
||||
if err != nil {
|
||||
return err
|
||||
return created, err
|
||||
}
|
||||
|
||||
var inherited *moby.Container
|
||||
if inherit {
|
||||
inherited = &container
|
||||
inherited = &replaced
|
||||
}
|
||||
err = s.createMobyContainer(ctx, project, service, name, number, inherited, false, true)
|
||||
created, err = s.createMobyContainer(ctx, project, service, name, number, inherited, false, true)
|
||||
if err != nil {
|
||||
return err
|
||||
return created, err
|
||||
}
|
||||
err = s.apiClient.ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{})
|
||||
err = s.apiClient.ContainerRemove(ctx, replaced.ID, moby.ContainerRemoveOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
return created, err
|
||||
}
|
||||
w.Event(progress.NewEvent(getContainerProgressName(container), progress.Done, "Recreated"))
|
||||
w.Event(progress.NewEvent(getContainerProgressName(replaced), progress.Done, "Recreated"))
|
||||
setDependentLifecycle(project, service.Name, forceRecreate)
|
||||
return nil
|
||||
return created, err
|
||||
}
|
||||
|
||||
// setDependentLifecycle define the Lifecycle strategy for all services to depend on specified service
|
||||
|
@ -291,35 +386,31 @@ func (s *composeService) startContainer(ctx context.Context, container moby.Cont
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *composeService) createMobyContainer(ctx context.Context, project *types.Project, service types.ServiceConfig, name string, number int,
|
||||
inherit *moby.Container,
|
||||
autoRemove bool,
|
||||
useNetworkAliases bool) error {
|
||||
cState, err := GetContextContainerState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
func (s *composeService) createMobyContainer(ctx context.Context, project *types.Project, service types.ServiceConfig,
|
||||
name string, number int, inherit *moby.Container, autoRemove bool, useNetworkAliases bool) (moby.Container, error) {
|
||||
var created moby.Container
|
||||
containerConfig, hostConfig, networkingConfig, err := s.getCreateOptions(ctx, project, service, number, inherit, autoRemove)
|
||||
if err != nil {
|
||||
return err
|
||||
return created, err
|
||||
}
|
||||
var plat *specs.Platform
|
||||
if service.Platform != "" {
|
||||
p, err := platforms.Parse(service.Platform)
|
||||
var p specs.Platform
|
||||
p, err = platforms.Parse(service.Platform)
|
||||
if err != nil {
|
||||
return err
|
||||
return created, err
|
||||
}
|
||||
plat = &p
|
||||
}
|
||||
created, err := s.apiClient.ContainerCreate(ctx, containerConfig, hostConfig, networkingConfig, plat, name)
|
||||
response, err := s.apiClient.ContainerCreate(ctx, containerConfig, hostConfig, networkingConfig, plat, name)
|
||||
if err != nil {
|
||||
return err
|
||||
return created, err
|
||||
}
|
||||
inspectedContainer, err := s.apiClient.ContainerInspect(ctx, created.ID)
|
||||
inspectedContainer, err := s.apiClient.ContainerInspect(ctx, response.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
return created, err
|
||||
}
|
||||
createdContainer := moby.Container{
|
||||
created = moby.Container{
|
||||
ID: inspectedContainer.ID,
|
||||
Labels: inspectedContainer.Config.Labels,
|
||||
Names: []string{inspectedContainer.Name},
|
||||
|
@ -327,11 +418,7 @@ func (s *composeService) createMobyContainer(ctx context.Context, project *types
|
|||
Networks: inspectedContainer.NetworkSettings.Networks,
|
||||
},
|
||||
}
|
||||
cState.Add(createdContainer)
|
||||
links, err := s.getLinks(ctx, service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
links := append(service.Links, service.ExternalLinks...)
|
||||
for _, netName := range service.NetworksByPriority() {
|
||||
netwrk := project.Networks[netName]
|
||||
cfg := service.Networks[netName]
|
||||
|
@ -342,21 +429,21 @@ func (s *composeService) createMobyContainer(ctx context.Context, project *types
|
|||
aliases = append(aliases, cfg.Aliases...)
|
||||
}
|
||||
}
|
||||
if val, ok := createdContainer.NetworkSettings.Networks[netwrk.Name]; ok {
|
||||
if shortIDAliasExists(createdContainer.ID, val.Aliases...) {
|
||||
if val, ok := created.NetworkSettings.Networks[netwrk.Name]; ok {
|
||||
if shortIDAliasExists(created.ID, val.Aliases...) {
|
||||
continue
|
||||
}
|
||||
err := s.apiClient.NetworkDisconnect(ctx, netwrk.Name, createdContainer.ID, false)
|
||||
err = s.apiClient.NetworkDisconnect(ctx, netwrk.Name, created.ID, false)
|
||||
if err != nil {
|
||||
return err
|
||||
return created, err
|
||||
}
|
||||
}
|
||||
err = s.connectContainerToNetwork(ctx, created.ID, netwrk.Name, cfg, links, aliases...)
|
||||
if err != nil {
|
||||
return err
|
||||
return created, err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return created, err
|
||||
}
|
||||
|
||||
func shortIDAliasExists(containerID string, aliases ...string) bool {
|
||||
|
@ -395,37 +482,6 @@ func (s *composeService) connectContainerToNetwork(ctx context.Context, id strin
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *composeService) getLinks(ctx context.Context, service types.ServiceConfig) ([]string, error) {
|
||||
cState, err := GetContextContainerState(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
links := []string{}
|
||||
for _, serviceLink := range service.Links {
|
||||
s := strings.Split(serviceLink, ":")
|
||||
serviceName := serviceLink
|
||||
serviceAlias := ""
|
||||
if len(s) == 2 {
|
||||
serviceName = s[0]
|
||||
serviceAlias = s[1]
|
||||
}
|
||||
containers := cState.GetContainers()
|
||||
depServiceContainers := containers.filter(isService(serviceName))
|
||||
for _, container := range depServiceContainers {
|
||||
name := getCanonicalContainerName(container)
|
||||
if serviceAlias != "" {
|
||||
links = append(links,
|
||||
fmt.Sprintf("%s:%s", name, serviceAlias))
|
||||
}
|
||||
links = append(links,
|
||||
fmt.Sprintf("%s:%s", name, name),
|
||||
fmt.Sprintf("%s:%s", name, getContainerNameWithoutProject(container)))
|
||||
}
|
||||
}
|
||||
links = append(links, service.ExternalLinks...)
|
||||
return links, nil
|
||||
}
|
||||
|
||||
func (s *composeService) isServiceHealthy(ctx context.Context, project *types.Project, service string) (bool, error) {
|
||||
containers, err := s.getContainers(ctx, project.Name, oneOffExclude, false, service)
|
||||
if err != nil {
|
||||
|
@ -503,26 +559,3 @@ func (s *composeService) startService(ctx context.Context, project *types.Projec
|
|||
}
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func (s *composeService) restartService(ctx context.Context, serviceName string, timeout *time.Duration) error {
|
||||
containerState, err := GetContextContainerState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
containers := containerState.GetContainers().filter(isService(serviceName))
|
||||
w := progress.ContextWriter(ctx)
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
for _, c := range containers {
|
||||
container := c
|
||||
eg.Go(func() error {
|
||||
eventName := getContainerProgressName(container)
|
||||
w.Event(progress.RestartingEvent(eventName))
|
||||
err := s.apiClient.ContainerRestart(ctx, container.ID, timeout)
|
||||
if err == nil {
|
||||
w.Event(progress.StartedEvent(eventName))
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
return eg.Wait()
|
||||
}
|
||||
|
|
|
@ -59,8 +59,6 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
containerState := NewContainersState(observedState)
|
||||
ctx = context.WithValue(ctx, ContainersKey{}, containerState)
|
||||
|
||||
err = s.ensureImagesExists(ctx, project, observedState, options.QuietPull)
|
||||
if err != nil {
|
||||
|
@ -105,12 +103,7 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
|
|||
|
||||
prepareServicesDependsOn(project)
|
||||
|
||||
return InDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
|
||||
if utils.StringContains(options.Services, service.Name) {
|
||||
return s.ensureService(c, project, service, options.Recreate, options.Inherit, options.Timeout)
|
||||
}
|
||||
return s.ensureService(c, project, service, options.RecreateDependencies, options.Inherit, options.Timeout)
|
||||
})
|
||||
return newConvergence(options.Services, observedState, s).apply(ctx, project, options)
|
||||
}
|
||||
|
||||
func prepareVolumes(p *types.Project) error {
|
||||
|
@ -275,12 +268,8 @@ func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project,
|
|||
|
||||
resources := getDeployResources(service)
|
||||
|
||||
networkMode, err := getMode(ctx, service.Name, service.NetworkMode)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
if networkMode == "" {
|
||||
networkMode = getDefaultNetworkMode(p, service)
|
||||
if service.NetworkMode == "" {
|
||||
service.NetworkMode = getDefaultNetworkMode(p, service)
|
||||
}
|
||||
|
||||
var networkConfig *network.NetworkingConfig
|
||||
|
@ -314,11 +303,6 @@ func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project,
|
|||
break //nolint:staticcheck
|
||||
}
|
||||
|
||||
ipcmode, err := getMode(ctx, service.Name, service.Ipc)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
tmpfs := map[string]string{}
|
||||
for _, t := range service.Tmpfs {
|
||||
if arr := strings.SplitN(t, ":", 2); len(arr) > 1 {
|
||||
|
@ -342,9 +326,9 @@ func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project,
|
|||
Mounts: mounts,
|
||||
CapAdd: strslice.StrSlice(service.CapAdd),
|
||||
CapDrop: strslice.StrSlice(service.CapDrop),
|
||||
NetworkMode: container.NetworkMode(networkMode),
|
||||
NetworkMode: container.NetworkMode(service.NetworkMode),
|
||||
Init: service.Init,
|
||||
IpcMode: container.IpcMode(ipcmode),
|
||||
IpcMode: container.IpcMode(service.Ipc),
|
||||
ReadonlyRootfs: service.ReadOnly,
|
||||
RestartPolicy: getRestartPolicy(service),
|
||||
ShmSize: int64(service.ShmSize),
|
||||
|
@ -913,24 +897,6 @@ func getAliases(s types.ServiceConfig, c *types.ServiceNetworkConfig) []string {
|
|||
return aliases
|
||||
}
|
||||
|
||||
func getMode(ctx context.Context, serviceName string, mode string) (string, error) {
|
||||
cState, err := GetContextContainerState(ctx)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
observedState := cState.GetContainers()
|
||||
depService := getDependentServiceFromMode(mode)
|
||||
if depService != "" {
|
||||
depServiceContainers := observedState.filter(isService(depService))
|
||||
if len(depServiceContainers) > 0 {
|
||||
return types.NetworkModeContainerPrefix + depServiceContainers[0].ID, nil
|
||||
}
|
||||
return "", fmt.Errorf(`no containers started for %q in service %q -> %v`,
|
||||
mode, serviceName, observedState)
|
||||
}
|
||||
return mode, nil
|
||||
}
|
||||
|
||||
func getNetworksForService(s types.ServiceConfig) map[string]*types.ServiceNetworkConfig {
|
||||
if len(s.Networks) > 0 {
|
||||
return s.Networks
|
||||
|
|
|
@ -63,16 +63,16 @@ var (
|
|||
)
|
||||
|
||||
// InDependencyOrder applies the function to the services of the project taking in account the dependency order
|
||||
func InDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, types.ServiceConfig) error) error {
|
||||
func InDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, string) error) error {
|
||||
return visit(ctx, project, upDirectionTraversalConfig, fn, ServiceStopped)
|
||||
}
|
||||
|
||||
// InReverseDependencyOrder applies the function to the services of the project in reverse order of dependencies
|
||||
func InReverseDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, types.ServiceConfig) error) error {
|
||||
func InReverseDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, string) error) error {
|
||||
return visit(ctx, project, downDirectionTraversalConfig, fn, ServiceStarted)
|
||||
}
|
||||
|
||||
func visit(ctx context.Context, project *types.Project, traversalConfig graphTraversalConfig, fn func(context.Context, types.ServiceConfig) error, initialStatus ServiceStatus) error {
|
||||
func visit(ctx context.Context, project *types.Project, traversalConfig graphTraversalConfig, fn func(context.Context, string) error, initialStatus ServiceStatus) error {
|
||||
g := NewGraph(project.Services, initialStatus)
|
||||
if b, err := g.HasCycles(); b {
|
||||
return err
|
||||
|
@ -89,12 +89,12 @@ func visit(ctx context.Context, project *types.Project, traversalConfig graphTra
|
|||
}
|
||||
|
||||
// Note: this could be `graph.walk` or whatever
|
||||
func run(ctx context.Context, graph *Graph, eg *errgroup.Group, nodes []*Vertex, traversalConfig graphTraversalConfig, fn func(context.Context, types.ServiceConfig) error) error {
|
||||
func run(ctx context.Context, graph *Graph, eg *errgroup.Group, nodes []*Vertex, traversalConfig graphTraversalConfig, fn func(context.Context, string) error) error {
|
||||
for _, node := range nodes {
|
||||
n := node
|
||||
// Don't start this service yet if all of its children have
|
||||
// not been started yet.
|
||||
if len(traversalConfig.filterAdjacentByStatusFn(graph, n.Service.Name, traversalConfig.adjacentServiceStatusToSkip)) != 0 {
|
||||
if len(traversalConfig.filterAdjacentByStatusFn(graph, n.Service, traversalConfig.adjacentServiceStatusToSkip)) != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -104,7 +104,7 @@ func run(ctx context.Context, graph *Graph, eg *errgroup.Group, nodes []*Vertex,
|
|||
return err
|
||||
}
|
||||
|
||||
graph.UpdateStatus(n.Service.Name, traversalConfig.targetServiceStatus)
|
||||
graph.UpdateStatus(n.Service, traversalConfig.targetServiceStatus)
|
||||
|
||||
return run(ctx, graph, eg, traversalConfig.adjacentNodesFn(n), traversalConfig, fn)
|
||||
})
|
||||
|
@ -122,7 +122,7 @@ type Graph struct {
|
|||
// Vertex represents a service in the dependencies structure
|
||||
type Vertex struct {
|
||||
Key string
|
||||
Service types.ServiceConfig
|
||||
Service string
|
||||
Status ServiceStatus
|
||||
Children map[string]*Vertex
|
||||
Parents map[string]*Vertex
|
||||
|
@ -162,7 +162,7 @@ func NewGraph(services types.Services, initialStatus ServiceStatus) *Graph {
|
|||
}
|
||||
|
||||
for _, s := range services {
|
||||
graph.AddVertex(s.Name, s, initialStatus)
|
||||
graph.AddVertex(s.Name, s.Name, initialStatus)
|
||||
}
|
||||
|
||||
for _, s := range services {
|
||||
|
@ -175,7 +175,7 @@ func NewGraph(services types.Services, initialStatus ServiceStatus) *Graph {
|
|||
}
|
||||
|
||||
// NewVertex is the constructor function for the Vertex
|
||||
func NewVertex(key string, service types.ServiceConfig, initialStatus ServiceStatus) *Vertex {
|
||||
func NewVertex(key string, service string, initialStatus ServiceStatus) *Vertex {
|
||||
return &Vertex{
|
||||
Key: key,
|
||||
Service: service,
|
||||
|
@ -186,7 +186,7 @@ func NewVertex(key string, service types.ServiceConfig, initialStatus ServiceSta
|
|||
}
|
||||
|
||||
// AddVertex adds a vertex to the Graph
|
||||
func (g *Graph) AddVertex(key string, service types.ServiceConfig, initialStatus ServiceStatus) {
|
||||
func (g *Graph) AddVertex(key string, service string, initialStatus ServiceStatus) {
|
||||
g.lock.Lock()
|
||||
defer g.lock.Unlock()
|
||||
|
||||
|
|
|
@ -47,8 +47,8 @@ var project = types.Project{
|
|||
func TestInDependencyUpCommandOrder(t *testing.T) {
|
||||
order := make(chan string)
|
||||
//nolint:errcheck, unparam
|
||||
go InDependencyOrder(context.TODO(), &project, func(ctx context.Context, config types.ServiceConfig) error {
|
||||
order <- config.Name
|
||||
go InDependencyOrder(context.TODO(), &project, func(ctx context.Context, config string) error {
|
||||
order <- config
|
||||
return nil
|
||||
})
|
||||
assert.Equal(t, <-order, "test3")
|
||||
|
@ -59,8 +59,8 @@ func TestInDependencyUpCommandOrder(t *testing.T) {
|
|||
func TestInDependencyReverseDownCommandOrder(t *testing.T) {
|
||||
order := make(chan string)
|
||||
//nolint:errcheck, unparam
|
||||
go InReverseDependencyOrder(context.TODO(), &project, func(ctx context.Context, config types.ServiceConfig) error {
|
||||
order <- config.Name
|
||||
go InReverseDependencyOrder(context.TODO(), &project, func(ctx context.Context, config string) error {
|
||||
order <- config
|
||||
return nil
|
||||
})
|
||||
assert.Equal(t, <-order, "test1")
|
||||
|
|
|
@ -50,7 +50,6 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx = context.WithValue(ctx, ContainersKey{}, NewContainersState(containers))
|
||||
|
||||
if options.Project == nil {
|
||||
project, err := s.projectFromContainerLabels(containers, projectName)
|
||||
|
@ -64,8 +63,8 @@ func (s *composeService) down(ctx context.Context, projectName string, options a
|
|||
resourceToRemove = true
|
||||
}
|
||||
|
||||
err = InReverseDependencyOrder(ctx, options.Project, func(c context.Context, service types.ServiceConfig) error {
|
||||
serviceContainers := containers.filter(isService(service.Name))
|
||||
err = InReverseDependencyOrder(ctx, options.Project, func(c context.Context, service string) error {
|
||||
serviceContainers := containers.filter(isService(service))
|
||||
err := s.removeContainers(ctx, w, serviceContainers, options.Timeout, options.Volumes)
|
||||
return err
|
||||
})
|
||||
|
@ -236,11 +235,6 @@ func (s *composeService) removeContainers(ctx context.Context, w progress.Writer
|
|||
w.Event(progress.ErrorMessageEvent(eventName, "Error while Removing"))
|
||||
return err
|
||||
}
|
||||
contextContainerState, err := GetContextContainerState(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
contextContainerState.Remove(toDelete.ID)
|
||||
w.Event(progress.RemovedEvent(eventName))
|
||||
return nil
|
||||
})
|
||||
|
|
|
@ -31,10 +31,6 @@ func serviceFilter(serviceName string) filters.KeyValuePair {
|
|||
return filters.Arg("label", fmt.Sprintf("%s=%s", api.ServiceLabel, serviceName))
|
||||
}
|
||||
|
||||
func slugFilter(slug string) filters.KeyValuePair {
|
||||
return filters.Arg("label", fmt.Sprintf("%s=%s", api.SlugLabel, slug))
|
||||
}
|
||||
|
||||
func oneOffFilter(b bool) filters.KeyValuePair {
|
||||
v := "False"
|
||||
if b {
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/docker/compose-cli/pkg/api"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/docker/compose-cli/pkg/progress"
|
||||
"github.com/docker/compose-cli/pkg/utils"
|
||||
|
@ -33,7 +34,7 @@ func (s *composeService) Restart(ctx context.Context, project *types.Project, op
|
|||
}
|
||||
|
||||
func (s *composeService) restart(ctx context.Context, project *types.Project, options api.RestartOptions) error {
|
||||
ctx, err := s.getUpdatedContainersStateContext(ctx, project.Name)
|
||||
observedState, err := s.getContainers(ctx, project.Name, oneOffInclude, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -42,11 +43,25 @@ func (s *composeService) restart(ctx context.Context, project *types.Project, op
|
|||
options.Services = project.ServiceNames()
|
||||
}
|
||||
|
||||
err = InDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
|
||||
if utils.StringContains(options.Services, service.Name) {
|
||||
return s.restartService(ctx, service.Name, options.Timeout)
|
||||
w := progress.ContextWriter(ctx)
|
||||
err = InDependencyOrder(ctx, project, func(c context.Context, service string) error {
|
||||
if !utils.StringContains(options.Services, service) {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
for _, c := range observedState.filter(isService(service)) {
|
||||
container := c
|
||||
eg.Go(func() error {
|
||||
eventName := getContainerProgressName(container)
|
||||
w.Event(progress.RestartingEvent(eventName))
|
||||
err := s.apiClient.ContainerRestart(ctx, container.ID, options.Timeout)
|
||||
if err == nil {
|
||||
w.Event(progress.StartedEvent(eventName))
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
return eg.Wait()
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -25,7 +25,6 @@ import (
|
|||
"github.com/compose-spec/compose-go/types"
|
||||
moby "github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
)
|
||||
|
||||
|
@ -34,8 +33,6 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
|
|||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
containerState := NewContainersState(observedState)
|
||||
ctx = context.WithValue(ctx, ContainersKey{}, containerState)
|
||||
|
||||
service, err := project.GetService(opts.Service)
|
||||
if err != nil {
|
||||
|
@ -63,10 +60,11 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
|
|||
if err := s.waitDependencies(ctx, project, service); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := s.createContainer(ctx, project, service, service.ContainerName, 1, opts.AutoRemove, opts.UseNetworkAliases); err != nil {
|
||||
created, err := s.createContainer(ctx, project, service, service.ContainerName, 1, opts.AutoRemove, opts.UseNetworkAliases)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
containerID := service.ContainerName
|
||||
containerID := created.ID
|
||||
|
||||
if opts.Detach {
|
||||
err := s.apiClient.ContainerStart(ctx, containerID, moby.ContainerStartOptions{})
|
||||
|
@ -77,21 +75,13 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
|
|||
return 0, nil
|
||||
}
|
||||
|
||||
containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
|
||||
Filters: filters.NewArgs(slugFilter(slug)),
|
||||
All: true,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
oneoffContainer := containers[0]
|
||||
restore, err := s.attachContainerStreams(ctx, oneoffContainer.ID, service.Tty, opts.Reader, opts.Writer)
|
||||
restore, err := s.attachContainerStreams(ctx, containerID, service.Tty, opts.Reader, opts.Writer)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer restore()
|
||||
|
||||
statusC, errC := s.apiClient.ContainerWait(context.Background(), oneoffContainer.ID, container.WaitConditionNextExit)
|
||||
statusC, errC := s.apiClient.ContainerWait(context.Background(), containerID, container.WaitConditionNextExit)
|
||||
|
||||
err = s.apiClient.ContainerStart(ctx, containerID, moby.ContainerStartOptions{})
|
||||
if err != nil {
|
||||
|
|
|
@ -53,7 +53,11 @@ func (s *composeService) start(ctx context.Context, project *types.Project, opti
|
|||
})
|
||||
}
|
||||
|
||||
err := InDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
|
||||
err := InDependencyOrder(ctx, project, func(c context.Context, name string) error {
|
||||
service, err := project.GetService(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.startService(ctx, project, service)
|
||||
})
|
||||
if err != nil {
|
||||
|
|
|
@ -1,111 +0,0 @@
|
|||
/*
|
||||
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 compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ContainersKey is the context key to access context value os a ContainersStatus
|
||||
type ContainersKey struct{}
|
||||
|
||||
// ContainersState state management interface
|
||||
type ContainersState interface {
|
||||
Get(string) *types.Container
|
||||
GetContainers() Containers
|
||||
Add(c types.Container)
|
||||
AddAll(cs Containers)
|
||||
Remove(string) types.Container
|
||||
}
|
||||
|
||||
// NewContainersState creates a new container state manager
|
||||
func NewContainersState(cs Containers) ContainersState {
|
||||
s := containersState{
|
||||
observedContainers: &cs,
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
// ContainersStatus works as a collection container for the observed containers
|
||||
type containersState struct {
|
||||
observedContainers *Containers
|
||||
}
|
||||
|
||||
func (s *containersState) AddAll(cs Containers) {
|
||||
for _, c := range cs {
|
||||
lValue := append(*s.observedContainers, c)
|
||||
s.observedContainers = &lValue
|
||||
}
|
||||
}
|
||||
|
||||
func (s *containersState) Add(c types.Container) {
|
||||
if s.Get(c.ID) == nil {
|
||||
lValue := append(*s.observedContainers, c)
|
||||
s.observedContainers = &lValue
|
||||
}
|
||||
}
|
||||
|
||||
func (s *containersState) Remove(id string) types.Container {
|
||||
var c types.Container
|
||||
var newObserved Containers
|
||||
for _, o := range *s.observedContainers {
|
||||
if o.ID == id {
|
||||
c = o
|
||||
continue
|
||||
}
|
||||
newObserved = append(newObserved, o)
|
||||
}
|
||||
s.observedContainers = &newObserved
|
||||
return c
|
||||
}
|
||||
|
||||
func (s *containersState) Get(id string) *types.Container {
|
||||
for _, o := range *s.observedContainers {
|
||||
if id == o.ID {
|
||||
return &o
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *containersState) GetContainers() Containers {
|
||||
if s.observedContainers != nil && *s.observedContainers != nil {
|
||||
return *s.observedContainers
|
||||
}
|
||||
return make(Containers, 0)
|
||||
}
|
||||
|
||||
// GetContextContainerState gets the container state manager
|
||||
func GetContextContainerState(ctx context.Context) (ContainersState, error) {
|
||||
cState, ok := ctx.Value(ContainersKey{}).(*containersState)
|
||||
if !ok {
|
||||
return nil, errors.New("containers' containersState not available in context")
|
||||
}
|
||||
return cState, nil
|
||||
}
|
||||
|
||||
func (s composeService) getUpdatedContainersStateContext(ctx context.Context, projectName string) (context.Context, error) {
|
||||
observedState, err := s.getContainers(ctx, projectName, oneOffInclude, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
containerState := NewContainersState(observedState)
|
||||
return context.WithValue(ctx, ContainersKey{}, containerState), nil
|
||||
}
|
|
@ -44,7 +44,7 @@ func (s *composeService) stop(ctx context.Context, project *types.Project, optio
|
|||
return err
|
||||
}
|
||||
|
||||
return InReverseDependencyOrder(ctx, project, func(c context.Context, service types.ServiceConfig) error {
|
||||
return s.stopContainers(ctx, w, containers.filter(isService(service.Name)), options.Timeout)
|
||||
return InReverseDependencyOrder(ctx, project, func(c context.Context, service string) error {
|
||||
return s.stopContainers(ctx, w, containers.filter(isService(service)), options.Timeout)
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue