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).
This commit is contained in:
Pieter van de Bruggen 2019-07-23 13:42:18 -07:00
parent b5f64c0a2e
commit a4bd90ff8a
5 changed files with 74 additions and 65 deletions

View file

@ -216,20 +216,26 @@ func (c *DockerCommand) RefreshContainersAndServices() error {
} }
} }
c.assignContainersToServices(containers, services)
var displayContainers = containers 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 services first by whether they have a linked container, and second by alphabetical order
sort.Slice(services, func(i, j int) bool { 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 return true
} }
if services[i].Container == nil && services[j].Container != nil { if len(services[i].Containers) == 0 && len(services[j].Containers) > 0 {
return false return false
} }
@ -243,17 +249,40 @@ func (c *DockerCommand) RefreshContainersAndServices() error {
return nil return nil
} }
func (c *DockerCommand) assignContainersToServices(containers []*Container, services []*Service) { func (c *DockerCommand) assignContainersToServices(containers []*Container, services []*Service) ([]*Container, error) {
L: // Get a list of the Container IDs for this Docker Compose project.
for _, service := range services { composeCommand := c.Config.UserConfig.CommandTemplates.DockerCompose
for _, container := range containers { output, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("%s ps -q", composeCommand))
if !container.OneOff && container.ServiceName == service.Name { if err != nil {
service.Container = container return nil, err
continue L
}
}
service.Container = nil
} }
// 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 // filterOutExited filters out the exited containers if c.ShowExited is false
@ -270,22 +299,6 @@ func (c *DockerCommand) filterOutExited(containers []*Container) []*Container {
return toReturn 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 // GetContainers gets the docker containers
func (c *DockerCommand) GetContainers() ([]*Container, error) { func (c *DockerCommand) GetContainers() ([]*Container, error) {
c.ContainerMutex.Lock() c.ContainerMutex.Lock()
@ -349,25 +362,20 @@ func (c *DockerCommand) GetServices() ([]*Service, error) {
} }
composeCommand := c.Config.UserConfig.CommandTemplates.DockerCompose 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 { if err != nil {
return nil, err return nil, err
} }
// output looks like: serviceNames := utils.SplitLines(output)
// service1 998d6d286b0499e0ff23d66302e720991a2asdkf9c30d0542034f610daf8a971 services := make([]*Service, len(serviceNames))
// service2 asdld98asdklasd9bccd02438de0994f8e19cbe691feb3755336ec5ca2c55971 for i, name := range serviceNames {
lines := utils.SplitLines(output)
services := make([]*Service, len(lines))
for i, str := range lines {
arr := strings.Split(str, " ")
services[i] = &Service{ services[i] = &Service{
Name: arr[0], Name: name,
ID: arr[1],
OSCommand: c.OSCommand, OSCommand: c.OSCommand,
Log: c.Log, Log: c.Log,
DockerCommand: c, DockerCommand: c,
Containers: make([]*Container, 0, 1),
} }
} }

View file

@ -13,27 +13,26 @@ import (
// Service : A docker Service // Service : A docker Service
type Service struct { type Service struct {
Name string Name string
ID string
OSCommand *OSCommand OSCommand *OSCommand
Log *logrus.Entry Log *logrus.Entry
Container *Container Containers []*Container
DockerCommand LimitedDockerCommand DockerCommand LimitedDockerCommand
} }
// GetDisplayStrings returns the dispaly string of Container // GetDisplayStrings returns the dispaly string of Container
func (s *Service) GetDisplayStrings(isFocused bool) []string { 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, ""} return []string{utils.ColoredString("none", color.FgBlue), s.Name, ""}
} }
cont := s.Container cont := s.Containers[0]
return []string{cont.GetDisplayStatus(), s.Name, cont.GetDisplayCPUPerc()} return []string{cont.GetDisplayStatus(), s.Name, cont.GetDisplayCPUPerc()}
} }
// Remove removes the service's containers // Remove removes the service's containers
func (s *Service) Remove(options types.ContainerRemoveOptions) error { 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 // Stop stops the service's containers
@ -58,12 +57,12 @@ func (s *Service) Restart() error {
// Attach attaches to the service // Attach attaches to the service
func (s *Service) Attach() (*exec.Cmd, error) { func (s *Service) Attach() (*exec.Cmd, error) {
return s.Container.Attach() return s.Containers[0].Attach()
} }
// Top returns process information // Top returns process information
func (s *Service) Top() (container.ContainerTopOKBody, error) { 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 // ViewLogs attaches to a subprocess viewing the service's logs

View file

@ -247,7 +247,7 @@ func (gui *Gui) refreshContainersAndServices() error {
// see if our selected service has moved // see if our selected service has moved
if selectedService != nil { if selectedService != nil {
for i, service := range gui.DockerCommand.Services { for i, service := range gui.DockerCommand.Services {
if service.ID == selectedService.ID { if service.Name == selectedService.Name {
if i == sl { if i == sl {
break break
} }

View file

@ -34,8 +34,8 @@ func (gui *Gui) refreshProject() error {
projectName := path.Base(gui.Config.ProjectDir) projectName := path.Base(gui.Config.ProjectDir)
if gui.DockerCommand.InDockerComposeProject { if gui.DockerCommand.InDockerComposeProject {
for _, service := range gui.DockerCommand.Services { for _, service := range gui.DockerCommand.Services {
if service.Container != nil { if len(service.Containers) > 0 {
projectName = service.Container.Details.Config.Labels["com.docker.compose.project"] projectName = service.Containers[0].Details.Config.Labels["com.docker.compose.project"]
break break
} }
} }

View file

@ -46,15 +46,17 @@ func (gui *Gui) handleServiceSelect(g *gocui.Gui, v *gocui.View) error {
} }
containerID := "" containerID := ""
if service.Container != nil { containerNum := ""
containerID = service.Container.ID 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 { if err := gui.focusPoint(0, gui.State.Panels.Services.SelectedLine, len(gui.DockerCommand.Services), v); err != nil {
return err 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) { if !gui.shouldRefresh(key) {
return nil return nil
} }
@ -74,10 +76,10 @@ func (gui *Gui) handleServiceSelect(g *gocui.Gui, v *gocui.View) error {
return err return err
} }
case "container-config": case "container-config":
if service.Container == nil { if len(service.Containers) == 0 {
return gui.renderString(gui.g, "main", gui.Tr.NoContainer) 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 return err
} }
case "top": 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 { func (gui *Gui) renderServiceStats(service *commands.Service) error {
if service.Container == nil { if len(service.Containers) == 0 {
return nil return nil
} }
return gui.renderContainerStats(service.Container) return gui.renderContainerStats(service.Containers[0])
} }
func (gui *Gui) renderServiceTop(service *commands.Service) error { func (gui *Gui) renderServiceTop(service *commands.Service) error {
@ -120,13 +122,13 @@ func (gui *Gui) renderServiceLogs(service *commands.Service) error {
return nil return nil
} }
if service.Container == nil { if len(service.Containers) == 0 {
return gui.T.NewTask(func(stop chan struct{}) { return gui.T.NewTask(func(stop chan struct{}) {
gui.clearMainView() gui.clearMainView()
}) })
} }
return gui.renderContainerLogs(service.Container) return gui.renderContainerLogs(service.Containers[0])
} }
func (gui *Gui) handleServicesNextLine(g *gocui.Gui, v *gocui.View) error { 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 return nil
} }
if service.Container == nil { if len(service.Containers) == 0 {
return gui.createErrorPanel(gui.g, gui.Tr.NoContainers) 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{ commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
Service: service, Service: service,
Container: service.Container, Container: service.Containers[0],
}) })
var customCommands []config.CustomCommand 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...) customCommands = append(customCommands, gui.Config.UserConfig.CustomCommands.Containers...)
} }