lazydocker/pkg/gui/services_panel.go
Pieter van de Bruggen a4bd90ff8a 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).
2019-07-23 13:42:18 -07:00

406 lines
11 KiB
Go

package gui
import (
"fmt"
"time"
"github.com/fatih/color"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazydocker/pkg/commands"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/utils"
)
// list panel functions
func (gui *Gui) getServiceContexts() []string {
return []string{"logs", "stats", "container-config", "top"}
}
func (gui *Gui) getServiceContextTitles() []string {
return []string{gui.Tr.LogsTitle, gui.Tr.StatsTitle, gui.Tr.ContainerConfigTitle, gui.Tr.TopTitle}
}
func (gui *Gui) getSelectedService() (*commands.Service, error) {
selectedLine := gui.State.Panels.Services.SelectedLine
if selectedLine == -1 {
return &commands.Service{}, errors.New("no service selected")
}
return gui.DockerCommand.Services[selectedLine], nil
}
func (gui *Gui) handleServicesClick(g *gocui.Gui, v *gocui.View) error {
itemCount := len(gui.DockerCommand.Services)
handleSelect := gui.handleServiceSelect
selectedLine := &gui.State.Panels.Services.SelectedLine
return gui.handleClick(v, itemCount, selectedLine, handleSelect)
}
func (gui *Gui) handleServiceSelect(g *gocui.Gui, v *gocui.View) error {
service, err := gui.getSelectedService()
if err != nil {
return nil
}
containerID := ""
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.Name + "-" + containerID + "-" + containerNum + "-" + gui.getServiceContexts()[gui.State.Panels.Services.ContextIndex]
if !gui.shouldRefresh(key) {
return nil
}
mainView := gui.getMainView()
mainView.Tabs = gui.getServiceContextTitles()
mainView.TabIndex = gui.State.Panels.Services.ContextIndex
switch gui.getServiceContexts()[gui.State.Panels.Services.ContextIndex] {
case "logs":
if err := gui.renderServiceLogs(service); err != nil {
return err
}
case "stats":
if err := gui.renderServiceStats(service); err != nil {
return err
}
case "container-config":
if len(service.Containers) == 0 {
return gui.renderString(gui.g, "main", gui.Tr.NoContainer)
}
if err := gui.renderContainerConfig(service.Containers[0]); err != nil {
return err
}
case "top":
if err := gui.renderServiceTop(service); err != nil {
return err
}
default:
return errors.New("Unknown context for services panel")
}
return nil
}
func (gui *Gui) renderServiceStats(service *commands.Service) error {
if len(service.Containers) == 0 {
return nil
}
return gui.renderContainerStats(service.Containers[0])
}
func (gui *Gui) renderServiceTop(service *commands.Service) error {
mainView := gui.getMainView()
mainView.Autoscroll = false
mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel
return gui.T.NewTickerTask(time.Second, func(stop chan struct{}) { gui.clearMainView() }, func(stop, notifyStopped chan struct{}) {
contents, err := service.RenderTop()
if err != nil {
gui.reRenderString(gui.g, "main", err.Error())
}
gui.reRenderString(gui.g, "main", contents)
})
}
func (gui *Gui) renderServiceLogs(service *commands.Service) error {
service, err := gui.getSelectedService()
if err != nil {
return nil
}
if len(service.Containers) == 0 {
return gui.T.NewTask(func(stop chan struct{}) {
gui.clearMainView()
})
}
return gui.renderContainerLogs(service.Containers[0])
}
func (gui *Gui) handleServicesNextLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() || gui.g.CurrentView() != v {
return nil
}
panelState := gui.State.Panels.Services
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Services), false)
return gui.handleServiceSelect(gui.g, v)
}
func (gui *Gui) handleServicesPrevLine(g *gocui.Gui, v *gocui.View) error {
if gui.popupPanelFocused() || gui.g.CurrentView() != v {
return nil
}
panelState := gui.State.Panels.Services
gui.changeSelectedLine(&panelState.SelectedLine, len(gui.DockerCommand.Services), true)
return gui.handleServiceSelect(gui.g, v)
}
func (gui *Gui) handleServicesNextContext(g *gocui.Gui, v *gocui.View) error {
contexts := gui.getServiceContexts()
if gui.State.Panels.Services.ContextIndex >= len(contexts)-1 {
gui.State.Panels.Services.ContextIndex = 0
} else {
gui.State.Panels.Services.ContextIndex++
}
gui.handleServiceSelect(gui.g, v)
return nil
}
func (gui *Gui) handleServicesPrevContext(g *gocui.Gui, v *gocui.View) error {
contexts := gui.getServiceContexts()
if gui.State.Panels.Services.ContextIndex <= 0 {
gui.State.Panels.Services.ContextIndex = len(contexts) - 1
} else {
gui.State.Panels.Services.ContextIndex--
}
gui.handleServiceSelect(gui.g, v)
return nil
}
type commandOption struct {
description string
command string
f func() error
}
// GetDisplayStrings is a function.
func (r *commandOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.description, color.New(color.FgCyan).Sprint(r.command)}
}
func (gui *Gui) handleServiceRemoveMenu(g *gocui.Gui, v *gocui.View) error {
service, err := gui.getSelectedService()
if err != nil {
return nil
}
composeCommand := gui.Config.UserConfig.CommandTemplates.DockerCompose
options := []*commandOption{
{
description: gui.Tr.Remove,
command: fmt.Sprintf("%s rm --stop --force %s", composeCommand, service.Name),
},
{
description: gui.Tr.RemoveWithVolumes,
command: fmt.Sprintf("%s rm --stop --force -v %s", composeCommand, service.Name),
},
{
description: gui.Tr.Cancel,
},
}
return gui.createCommandMenu(options, gui.Tr.RemovingStatus)
}
func (gui *Gui) handleServiceStop(g *gocui.Gui, v *gocui.View) error {
service, err := gui.getSelectedService()
if err != nil {
return nil
}
return gui.createConfirmationPanel(gui.g, v, gui.Tr.Confirm, gui.Tr.StopService, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
if err := service.Stop(); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshContainersAndServices()
})
}, nil)
}
func (gui *Gui) handleServiceRestart(g *gocui.Gui, v *gocui.View) error {
service, err := gui.getSelectedService()
if err != nil {
return nil
}
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
if err := service.Restart(); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return gui.refreshContainersAndServices()
})
}
func (gui *Gui) handleServiceAttach(g *gocui.Gui, v *gocui.View) error {
service, err := gui.getSelectedService()
if err != nil {
return nil
}
if len(service.Containers) == 0 {
return gui.createErrorPanel(gui.g, gui.Tr.NoContainers)
}
c, err := service.Attach()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.SubProcess = c
return gui.Errors.ErrSubProcess
}
func (gui *Gui) handleServiceViewLogs(g *gocui.Gui, v *gocui.View) error {
service, err := gui.getSelectedService()
if err != nil {
return nil
}
c, err := service.ViewLogs()
if err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
gui.SubProcess = c
return gui.Errors.ErrSubProcess
}
func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
service, err := gui.getSelectedService()
if err != nil {
return nil
}
rebuildCommand := utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.RebuildService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
)
recreateCommand := utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.RecreateService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
)
options := []*commandOption{
{
description: gui.Tr.Restart,
command: utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.RestartService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
),
f: func() error {
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
if err := service.Restart(); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return nil
})
},
}, {
description: gui.Tr.Recreate,
command: utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.RecreateService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
),
f: func() error {
return gui.WithWaitingStatus(gui.Tr.RestartingStatus, func() error {
if err := gui.OSCommand.RunCommand(recreateCommand); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return nil
})
},
},
{
description: gui.Tr.Rebuild,
command: utils.ApplyTemplate(
gui.Config.UserConfig.CommandTemplates.RebuildService,
gui.DockerCommand.NewCommandObject(commands.CommandObject{Service: service}),
),
f: func() error {
gui.SubProcess = gui.OSCommand.RunCustomCommand(rebuildCommand)
return gui.Errors.ErrSubProcess
},
},
{
description: gui.Tr.Cancel,
f: func() error { return nil },
},
}
handleMenuPress := func(index int) error { return options[index].f() }
return gui.createMenu("", options, len(options), handleMenuPress)
}
func (gui *Gui) createCommandMenu(options []*commandOption, status string) error {
handleMenuPress := func(index int) error {
if options[index].command == "" {
return nil
}
return gui.WithWaitingStatus(status, func() error {
if err := gui.OSCommand.RunCommand(options[index].command); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
return nil
})
}
return gui.createMenu("", options, len(options), handleMenuPress)
}
func (gui *Gui) handleServicesCustomCommand(g *gocui.Gui, v *gocui.View) error {
service, err := gui.getSelectedService()
if err != nil {
return nil
}
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
Service: service,
Container: service.Containers[0],
})
var customCommands []config.CustomCommand
customServiceCommands := gui.Config.UserConfig.CustomCommands.Services
// we only include service commands if they have no serviceNames defined or if our service happens to be one of the serviceNames defined
L:
for _, cmd := range customServiceCommands {
if len(cmd.ServiceNames) == 0 {
customCommands = append(customCommands, cmd)
continue L
}
for _, serviceName := range cmd.ServiceNames {
if serviceName == service.Name {
// appending these to the top given they're more likely to be selected
customCommands = append([]config.CustomCommand{cmd}, customCommands...)
continue L
}
}
}
if len(service.Containers) > 0 {
customCommands = append(customCommands, gui.Config.UserConfig.CustomCommands.Containers...)
}
return gui.createCustomCommandMenu(customCommands, commandObject)
}