From a4bd90ff8a88509da965c51d305a19075f0e1e16 Mon Sep 17 00:00:00 2001 From: Pieter van de Bruggen Date: Tue, 23 Jul 2019 13:42:18 -0700 Subject: [PATCH] Correlate Services and Containers by Service Name The config hash ends up not being a unique identifier of a Service. Additionally, Services may have multiple associated Containers. ``` yaml # Sample docker-compose.yml version: "2.2" services: named-service: container_name: 'named-container' image: 'alpine' command: 'sh -c "while :; do sleep 1; done"' anonymous-service: image: 'alpine' command: 'sh -c "while :; do sleep 1; done"' scaled-service: image: 'alpine' scale: 3 command: 'sh -c "while :; do sleep 1; done"' ``` ``` bash $ docker-compose config --hash="*" named-service e6e7c4564dfc435ba96725098c62e6b6cf8da9f0b76b0a08be2baddedad6d007 anonymous-service 254f4815ef36bc9fcc2ce80f82278187c9e233309a4106951551f3692657ce9c scaled-service 254f4815ef36bc9fcc2ce80f82278187c9e233309a4106951551f3692657ce9c ``` This commit changes how Services are discovered (`docker-compose config --services`), how they're associated (by Container ID, rather than config hash or name), and allows for Services to reference multiple Containers (though only the first is directly used for anything at this time). --- pkg/commands/docker.go | 94 ++++++++++++++++++++----------------- pkg/commands/service.go | 13 +++-- pkg/gui/containers_panel.go | 2 +- pkg/gui/project_panel.go | 4 +- pkg/gui/services_panel.go | 26 +++++----- 5 files changed, 74 insertions(+), 65 deletions(-) diff --git a/pkg/commands/docker.go b/pkg/commands/docker.go index 86a70f90..0ddede2a 100644 --- a/pkg/commands/docker.go +++ b/pkg/commands/docker.go @@ -216,20 +216,26 @@ func (c *DockerCommand) RefreshContainersAndServices() error { } } - c.assignContainersToServices(containers, services) - var displayContainers = containers - if !c.Config.UserConfig.Gui.ShowAllContainers { - displayContainers = c.obtainStandaloneContainers(containers, services) + + if c.InDockerComposeProject { + var standaloneContainers, err = c.assignContainersToServices(containers, services) + if err != nil { + return err + } + + if !c.Config.UserConfig.Gui.ShowAllContainers { + displayContainers = standaloneContainers + } } // sort services first by whether they have a linked container, and second by alphabetical order sort.Slice(services, func(i, j int) bool { - if services[i].Container != nil && services[j].Container == nil { + if len(services[i].Containers) > 0 && len(services[j].Containers) == 0 { return true } - if services[i].Container == nil && services[j].Container != nil { + if len(services[i].Containers) == 0 && len(services[j].Containers) > 0 { return false } @@ -243,17 +249,40 @@ func (c *DockerCommand) RefreshContainersAndServices() error { return nil } -func (c *DockerCommand) assignContainersToServices(containers []*Container, services []*Service) { -L: - for _, service := range services { - for _, container := range containers { - if !container.OneOff && container.ServiceName == service.Name { - service.Container = container - continue L - } - } - service.Container = nil +func (c *DockerCommand) assignContainersToServices(containers []*Container, services []*Service) ([]*Container, error) { + // Get a list of the Container IDs for this Docker Compose project. + composeCommand := c.Config.UserConfig.CommandTemplates.DockerCompose + output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("%s ps -q", composeCommand)) + if err != nil { + return nil, err } + + // Create an index for Container IDs to make lookups less expensive. + lines := utils.SplitLines(output) + serviceContainerIds := make(map[string]bool) + for _, line := range lines { + serviceContainerIds[line] = true + } + + // Index Services by name. + serviceMap := make(map[string]*Service) + for _, service := range services { + serviceMap[service.Name] = service + } + + // Walk the list of Containers, categorizing them as either associated with a + // Service or as standalone. + standaloneContainers := make([]*Container, 0, len(containers) - len(serviceContainerIds)) + for _, container := range containers { + if !container.OneOff && serviceContainerIds[container.ID] { + service := serviceMap[container.ServiceName] + service.Containers = append(service.Containers, container) + } else { + standaloneContainers = append(standaloneContainers, container) + } + } + + return standaloneContainers, nil } // filterOutExited filters out the exited containers if c.ShowExited is false @@ -270,22 +299,6 @@ func (c *DockerCommand) filterOutExited(containers []*Container) []*Container { return toReturn } -// obtainStandaloneContainers returns standalone containers. Standalone containers are containers which are either one-off containers, or whose service is not part of this docker-compose context -func (c *DockerCommand) obtainStandaloneContainers(containers []*Container, services []*Service) []*Container { - standaloneContainers := []*Container{} -L: - for _, container := range containers { - for _, service := range services { - if !container.OneOff && container.ServiceName != "" && container.ServiceName == service.Name { - continue L - } - } - standaloneContainers = append(standaloneContainers, container) - } - - return standaloneContainers -} - // GetContainers gets the docker containers func (c *DockerCommand) GetContainers() ([]*Container, error) { c.ContainerMutex.Lock() @@ -349,25 +362,20 @@ func (c *DockerCommand) GetServices() ([]*Service, error) { } composeCommand := c.Config.UserConfig.CommandTemplates.DockerCompose - output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("%s config --hash=*", composeCommand)) + output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("%s config --services", composeCommand)) if err != nil { return nil, err } - // output looks like: - // service1 998d6d286b0499e0ff23d66302e720991a2asdkf9c30d0542034f610daf8a971 - // service2 asdld98asdklasd9bccd02438de0994f8e19cbe691feb3755336ec5ca2c55971 - - lines := utils.SplitLines(output) - services := make([]*Service, len(lines)) - for i, str := range lines { - arr := strings.Split(str, " ") + serviceNames := utils.SplitLines(output) + services := make([]*Service, len(serviceNames)) + for i, name := range serviceNames { services[i] = &Service{ - Name: arr[0], - ID: arr[1], + Name: name, OSCommand: c.OSCommand, Log: c.Log, DockerCommand: c, + Containers: make([]*Container, 0, 1), } } diff --git a/pkg/commands/service.go b/pkg/commands/service.go index 59580ef4..dbc1fdf8 100644 --- a/pkg/commands/service.go +++ b/pkg/commands/service.go @@ -13,27 +13,26 @@ import ( // Service : A docker Service type Service struct { Name string - ID string OSCommand *OSCommand Log *logrus.Entry - Container *Container + Containers []*Container DockerCommand LimitedDockerCommand } // GetDisplayStrings returns the dispaly string of Container func (s *Service) GetDisplayStrings(isFocused bool) []string { - if s.Container == nil { + if len(s.Containers) == 0 { return []string{utils.ColoredString("none", color.FgBlue), s.Name, ""} } - cont := s.Container + cont := s.Containers[0] return []string{cont.GetDisplayStatus(), s.Name, cont.GetDisplayCPUPerc()} } // Remove removes the service's containers func (s *Service) Remove(options types.ContainerRemoveOptions) error { - return s.Container.Remove(options) + return s.Containers[0].Remove(options) } // Stop stops the service's containers @@ -58,12 +57,12 @@ func (s *Service) Restart() error { // Attach attaches to the service func (s *Service) Attach() (*exec.Cmd, error) { - return s.Container.Attach() + return s.Containers[0].Attach() } // Top returns process information func (s *Service) Top() (container.ContainerTopOKBody, error) { - return s.Container.Top() + return s.Containers[0].Top() } // ViewLogs attaches to a subprocess viewing the service's logs diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index d1c61f5e..38886999 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -247,7 +247,7 @@ func (gui *Gui) refreshContainersAndServices() error { // see if our selected service has moved if selectedService != nil { for i, service := range gui.DockerCommand.Services { - if service.ID == selectedService.ID { + if service.Name == selectedService.Name { if i == sl { break } diff --git a/pkg/gui/project_panel.go b/pkg/gui/project_panel.go index 76435f6b..dfa4b93a 100644 --- a/pkg/gui/project_panel.go +++ b/pkg/gui/project_panel.go @@ -34,8 +34,8 @@ func (gui *Gui) refreshProject() error { projectName := path.Base(gui.Config.ProjectDir) if gui.DockerCommand.InDockerComposeProject { for _, service := range gui.DockerCommand.Services { - if service.Container != nil { - projectName = service.Container.Details.Config.Labels["com.docker.compose.project"] + if len(service.Containers) > 0 { + projectName = service.Containers[0].Details.Config.Labels["com.docker.compose.project"] break } } diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index 0c1e7a43..4718901a 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -46,15 +46,17 @@ func (gui *Gui) handleServiceSelect(g *gocui.Gui, v *gocui.View) error { } containerID := "" - if service.Container != nil { - containerID = service.Container.ID + containerNum := "" + if len(service.Containers) != 0 { + containerID = service.Containers[0].ID + containerNum = service.Containers[0].ContainerNumber } if err := gui.focusPoint(0, gui.State.Panels.Services.SelectedLine, len(gui.DockerCommand.Services), v); err != nil { return err } - key := "services-" + service.ID + "-" + containerID + "-" + gui.getServiceContexts()[gui.State.Panels.Services.ContextIndex] + key := "services-" + service.Name + "-" + containerID + "-" + containerNum + "-" + gui.getServiceContexts()[gui.State.Panels.Services.ContextIndex] if !gui.shouldRefresh(key) { return nil } @@ -74,10 +76,10 @@ func (gui *Gui) handleServiceSelect(g *gocui.Gui, v *gocui.View) error { return err } case "container-config": - if service.Container == nil { + if len(service.Containers) == 0 { return gui.renderString(gui.g, "main", gui.Tr.NoContainer) } - if err := gui.renderContainerConfig(service.Container); err != nil { + if err := gui.renderContainerConfig(service.Containers[0]); err != nil { return err } case "top": @@ -92,11 +94,11 @@ func (gui *Gui) handleServiceSelect(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) renderServiceStats(service *commands.Service) error { - if service.Container == nil { + if len(service.Containers) == 0 { return nil } - return gui.renderContainerStats(service.Container) + return gui.renderContainerStats(service.Containers[0]) } func (gui *Gui) renderServiceTop(service *commands.Service) error { @@ -120,13 +122,13 @@ func (gui *Gui) renderServiceLogs(service *commands.Service) error { return nil } - if service.Container == nil { + if len(service.Containers) == 0 { return gui.T.NewTask(func(stop chan struct{}) { gui.clearMainView() }) } - return gui.renderContainerLogs(service.Container) + return gui.renderContainerLogs(service.Containers[0]) } func (gui *Gui) handleServicesNextLine(g *gocui.Gui, v *gocui.View) error { @@ -252,7 +254,7 @@ func (gui *Gui) handleServiceAttach(g *gocui.Gui, v *gocui.View) error { return nil } - if service.Container == nil { + if len(service.Containers) == 0 { return gui.createErrorPanel(gui.g, gui.Tr.NoContainers) } @@ -374,7 +376,7 @@ func (gui *Gui) handleServicesCustomCommand(g *gocui.Gui, v *gocui.View) error { commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{ Service: service, - Container: service.Container, + Container: service.Containers[0], }) var customCommands []config.CustomCommand @@ -396,7 +398,7 @@ L: } } - if service.Container != nil { + if len(service.Containers) > 0 { customCommands = append(customCommands, gui.Config.UserConfig.CustomCommands.Containers...) }