diff --git a/pkg/commands/container.go b/pkg/commands/container.go index 6a3c506e..c4a3a8b6 100644 --- a/pkg/commands/container.go +++ b/pkg/commands/container.go @@ -36,6 +36,7 @@ type Container struct { StatHistory []RecordedStats Details Details MonitoringStats bool + DockerCommand LimitedDockerCommand } // RecordedStats contains both the container stats we've received from docker, and our own derived stats from those container stats. When configuring a graph, you're basically specifying the path of a value in this struct @@ -354,13 +355,6 @@ func (c *Container) Restart() error { return c.Client.ContainerRestart(context.Background(), c.ID, nil) } -// RestartService restarts the container -func (c *Container) RestartService() error { - templateString := c.OSCommand.Config.UserConfig.CommandTemplates.RestartService - command := utils.ApplyTemplate(templateString, c) - return c.OSCommand.RunCommand(command) -} - // Attach attaches the container func (c *Container) Attach() (*exec.Cmd, error) { // verify that we can in fact attach to this container @@ -394,7 +388,10 @@ func (c *Container) EraseOldHistory() { // ViewLogs attaches to a subprocess viewing the container's logs func (c *Container) ViewLogs() (*exec.Cmd, error) { templateString := c.OSCommand.Config.UserConfig.CommandTemplates.ViewContainerLogs - command := utils.ApplyTemplate(templateString, c) + command := utils.ApplyTemplate( + templateString, + c.DockerCommand.NewCommandObject(CommandObject{Container: c}), + ) cmd := c.OSCommand.ExecutableFromString(command) c.OSCommand.PrepareForChildren(cmd) @@ -407,3 +404,17 @@ func (c *DockerCommand) PruneContainers() error { _, err := c.Client.ContainersPrune(context.Background(), filters.Args{}) return err } + +// TTYLogsCommand returns a command for obtaining the logs from a TTY container +func (c *Container) TTYLogsCommand() *exec.Cmd { + command := utils.ApplyTemplate( + c.Config.UserConfig.CommandTemplates.ContainerTTYLogs, + c.DockerCommand.NewCommandObject(CommandObject{Container: c}), + ) + return c.OSCommand.RunCustomCommand(command) +} + +// Inspect returns details about the container +func (c *Container) Inspect() (types.ContainerJSON, error) { + return c.Client.ContainerInspect(context.Background(), c.ID) +} diff --git a/pkg/commands/docker.go b/pkg/commands/docker.go index 45280ee4..5f470c71 100644 --- a/pkg/commands/docker.go +++ b/pkg/commands/docker.go @@ -14,6 +14,7 @@ import ( "github.com/acarl005/stripansi" "github.com/docker/docker/api/types" "github.com/docker/docker/client" + "github.com/imdario/mergo" "github.com/jesseduffield/lazydocker/pkg/config" "github.com/jesseduffield/lazydocker/pkg/i18n" "github.com/jesseduffield/lazydocker/pkg/utils" @@ -39,6 +40,25 @@ type DockerCommand struct { Volumes []*Volume } +// LimitedDockerCommand is a stripped-down DockerCommand with just the methods the container/service/image might need +type LimitedDockerCommand interface { + NewCommandObject(CommandObject) CommandObject +} + +// CommandObject is what we pass to our template resolvers when we are running a custom command. We do not guarantee that all fields will be populated: just the ones that make sense for the current context +type CommandObject struct { + DockerCompose string + Service *Service + Container *Container +} + +// NewCommandObject takes a command object and returns a default command object with the passed command object merged in +func (c *DockerCommand) NewCommandObject(obj CommandObject) CommandObject { + defaultObj := CommandObject{DockerCompose: c.Config.UserConfig.CommandTemplates.DockerCompose} + mergo.Merge(&defaultObj, obj) + return defaultObj +} + // NewDockerCommand it runs git commands func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.TranslationSet, config *config.AppConfig, errorChan chan error) (*DockerCommand, error) { cli, err := client.NewEnvClient() @@ -46,22 +66,35 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Translat return nil, err } - inDockerComposeProject := true - err = osCommand.RunCommand(config.UserConfig.CommandTemplates.CheckDockerComposeConfig) - if err != nil { - inDockerComposeProject = false - log.Warn(err.Error()) - } - - return &DockerCommand{ + dockerCommand := &DockerCommand{ Log: log, OSCommand: osCommand, Tr: tr, Config: config, Client: cli, - InDockerComposeProject: inDockerComposeProject, ErrorChan: errorChan, - }, nil + InDockerComposeProject: true, + } + + command := utils.ApplyTemplate( + config.UserConfig.CommandTemplates.CheckDockerComposeConfig, + dockerCommand.NewCommandObject(CommandObject{}), + ) + + log.Warn(command) + + err = osCommand.RunCommand( + utils.ApplyTemplate( + config.UserConfig.CommandTemplates.CheckDockerComposeConfig, + dockerCommand.NewCommandObject(CommandObject{}), + ), + ) + if err != nil { + dockerCommand.InDockerComposeProject = false + log.Warn(err.Error()) + } + + return dockerCommand, nil } // MonitorContainerStats is a function @@ -263,11 +296,12 @@ func (c *DockerCommand) GetContainers() ([]*Container, error) { // initialise the container if it's completely new if newContainer == nil { newContainer = &Container{ - ID: container.ID, - Client: c.Client, - OSCommand: c.OSCommand, - Log: c.Log, - Config: c.Config, + ID: container.ID, + Client: c.Client, + OSCommand: c.OSCommand, + Log: c.Log, + Config: c.Config, + DockerCommand: c, } } @@ -310,10 +344,11 @@ func (c *DockerCommand) GetServices() ([]*Service, error) { for i, str := range lines { arr := strings.Split(str, " ") services[i] = &Service{ - Name: arr[0], - ID: arr[1], - OSCommand: c.OSCommand, - Log: c.Log, + Name: arr[0], + ID: arr[1], + OSCommand: c.OSCommand, + Log: c.Log, + DockerCommand: c, } } @@ -353,7 +388,13 @@ func (c *DockerCommand) UpdateContainerDetails() error { // ViewAllLogs attaches to a subprocess viewing all the logs from docker-compose func (c *DockerCommand) ViewAllLogs() (*exec.Cmd, error) { - cmd := c.OSCommand.ExecutableFromString(c.OSCommand.Config.UserConfig.CommandTemplates.ViewAllLogs) + cmd := c.OSCommand.ExecutableFromString( + utils.ApplyTemplate( + c.OSCommand.Config.UserConfig.CommandTemplates.ViewAllLogs, + c.NewCommandObject(CommandObject{}), + ), + ) + c.OSCommand.PrepareForChildren(cmd) return cmd, nil @@ -361,7 +402,13 @@ func (c *DockerCommand) ViewAllLogs() (*exec.Cmd, error) { // DockerComposeConfig returns the result of 'docker-compose config' func (c *DockerCommand) DockerComposeConfig() string { - output, err := c.OSCommand.RunCommandWithOutput(c.OSCommand.Config.UserConfig.CommandTemplates.DockerComposeConfig) + output, err := c.OSCommand.RunCommandWithOutput( + utils.ApplyTemplate( + c.OSCommand.Config.UserConfig.CommandTemplates.DockerComposeConfig, + c.NewCommandObject(CommandObject{}), + ), + ) + if err != nil { output = err.Error() } diff --git a/pkg/commands/image.go b/pkg/commands/image.go index 65f64137..a49b7588 100644 --- a/pkg/commands/image.go +++ b/pkg/commands/image.go @@ -14,13 +14,14 @@ import ( // Image : A docker Image type Image struct { - Name string - Tag string - ID string - Image types.ImageSummary - Client *client.Client - OSCommand *OSCommand - Log *logrus.Entry + Name string + Tag string + ID string + Image types.ImageSummary + Client *client.Client + OSCommand *OSCommand + Log *logrus.Entry + DockerCommand LimitedDockerCommand } // GetDisplayStrings returns the display string of Image @@ -99,9 +100,6 @@ func (i *Image) RenderHistory() (string, error) { return utils.RenderList(layers, utils.WithHeader([]string{"ID", "TAG", "SIZE", "COMMAND"})) } - - - // GetImages returns a slice of docker images func (c *DockerCommand) GetImages() ([]*Image, error) { images, err := c.Client.ImageList(context.Background(), types.ImageListOptions{}) @@ -127,13 +125,14 @@ func (c *DockerCommand) GetImages() ([]*Image, error) { } ownImages[i] = &Image{ - ID: image.ID, - Name: nameParts[0], - Tag: tag, - Image: image, - Client: c.Client, - OSCommand: c.OSCommand, - Log: c.Log, + ID: image.ID, + Name: nameParts[0], + Tag: tag, + Image: image, + Client: c.Client, + OSCommand: c.OSCommand, + Log: c.Log, + DockerCommand: c, } } diff --git a/pkg/commands/service.go b/pkg/commands/service.go index 079cb279..76e9a61a 100644 --- a/pkg/commands/service.go +++ b/pkg/commands/service.go @@ -11,11 +11,12 @@ import ( // Service : A docker Service type Service struct { - Name string - ID string - OSCommand *OSCommand - Log *logrus.Entry - Container *Container + Name string + ID string + OSCommand *OSCommand + Log *logrus.Entry + Container *Container + DockerCommand LimitedDockerCommand } // GetDisplayStrings returns the dispaly string of Container @@ -37,14 +38,20 @@ func (s *Service) Remove(options types.ContainerRemoveOptions) error { // Stop stops the service's containers func (s *Service) Stop() error { templateString := s.OSCommand.Config.UserConfig.CommandTemplates.StopService - command := utils.ApplyTemplate(templateString, s) + command := utils.ApplyTemplate( + templateString, + s.DockerCommand.NewCommandObject(CommandObject{Service: s}), + ) return s.OSCommand.RunCommand(command) } // Restart restarts the service func (s *Service) Restart() error { templateString := s.OSCommand.Config.UserConfig.CommandTemplates.RestartService - command := utils.ApplyTemplate(templateString, s) + command := utils.ApplyTemplate( + templateString, + s.DockerCommand.NewCommandObject(CommandObject{Service: s}), + ) return s.OSCommand.RunCommand(command) } @@ -61,7 +68,10 @@ func (s *Service) Top() (types.ContainerProcessList, error) { // ViewLogs attaches to a subprocess viewing the service's logs func (s *Service) ViewLogs() (*exec.Cmd, error) { templateString := s.OSCommand.Config.UserConfig.CommandTemplates.ViewServiceLogs - command := utils.ApplyTemplate(templateString, s) + command := utils.ApplyTemplate( + templateString, + s.DockerCommand.NewCommandObject(CommandObject{Service: s}), + ) cmd := s.OSCommand.ExecutableFromString(command) s.OSCommand.PrepareForChildren(cmd) diff --git a/pkg/commands/volume.go b/pkg/commands/volume.go index 4f720315..af44e075 100644 --- a/pkg/commands/volume.go +++ b/pkg/commands/volume.go @@ -12,11 +12,12 @@ import ( // Volume : A docker Volume type Volume struct { - Name string - Volume *types.Volume - Client *client.Client - OSCommand *OSCommand - Log *logrus.Entry + Name string + Volume *types.Volume + Client *client.Client + OSCommand *OSCommand + Log *logrus.Entry + DockerCommand LimitedDockerCommand } // GetDisplayStrings returns the dispaly string of Container @@ -41,11 +42,12 @@ func (c *DockerCommand) RefreshVolumes() error { for i, volume := range volumes { ownVolumes[i] = &Volume{ - Name: volume.Name, - Volume: volume, - Client: c.Client, - OSCommand: c.OSCommand, - Log: c.Log, + Name: volume.Name, + Volume: volume, + Client: c.Client, + OSCommand: c.OSCommand, + Log: c.Log, + DockerCommand: c, } } diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index 73996a7d..4c162da5 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -164,18 +164,18 @@ func GetDefaultConfig() UserConfig { Reporting: "undetermined", ConfirmOnQuit: false, CommandTemplates: CommandTemplatesConfig{ - RestartService: "docker-compose restart {{ .Name }}", - RebuildService: "docker-compose up -d --build {{ .Name }}", DockerCompose: "docker-compose", - StopService: "docker-compose stop {{ .Name }}", - ServiceLogs: "docker-compose logs {{ .Name }}", - AllLogs: "docker-compose logs --tail=100", - ViewAllLogs: "docker-compose logs", - DockerComposeConfig: "docker-compose config", - CheckDockerComposeConfig: "docker-compose config --quiet", - ContainerLogs: "docker logs --timestamps --follow --tail=100 {{ .ID }}", - ViewContainerLogs: "docker logs --timestamps --follow --tail=100 {{ .ID }}", - ContainerTTYLogs: "docker logs --follow --tail=100 {{ .ID }}", + RestartService: "{{ .DockerCompose }} restart {{ .Service.Name }}", + RebuildService: "{{ .DockerCompose }} up -d --build {{ .Service.Name }}", + StopService: "{{ .DockerCompose }} stop {{ .Service.Name }}", + ServiceLogs: "{{ .DockerCompose }} logs {{ .Service.Name }}", + AllLogs: "{{ .DockerCompose }} logs --tail=100", + ViewAllLogs: "{{ .DockerCompose }} logs", + DockerComposeConfig: "{{ .DockerCompose }} config", + CheckDockerComposeConfig: "{{ .DockerCompose }} config --quiet", + ContainerLogs: "docker logs --timestamps --follow --tail=100 {{ .Container.ID }}", + ViewContainerLogs: "docker logs --timestamps --follow --tail=100 {{ .Container.ID }}", + ContainerTTYLogs: "docker logs --follow --tail=100 {{ .Container.ID }}", }, OS: GetPlatformDefaultConfig(), Update: UpdateConfig{ diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index 370e78f8..3450c0e9 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -2,7 +2,6 @@ package gui import ( "bufio" - "context" "encoding/json" "fmt" "os/exec" @@ -190,8 +189,7 @@ func (gui *Gui) renderLogs(container *commands.Container, template string, setup gui.T.NewTickerTask(time.Millisecond*200, nil, func(stop, notifyStopped chan struct{}) { gui.clearMainView() - command := utils.ApplyTemplate(gui.Config.UserConfig.CommandTemplates.ContainerTTYLogs, container) - cmd := gui.OSCommand.RunCustomCommand(command) + cmd := container.TTYLogsCommand() setup(cmd) @@ -213,7 +211,7 @@ func (gui *Gui) renderLogs(container *commands.Container, template string, setup case <-stop: return default: - result, err := gui.DockerCommand.Client.ContainerInspect(context.Background(), container.ID) + result, err := container.Inspect() if err != nil { // if we get an error, then the container has probably been removed so we'll get out of here gui.Log.Error(err) diff --git a/pkg/gui/services_panel.go b/pkg/gui/services_panel.go index 1cb82c17..bca9d818 100644 --- a/pkg/gui/services_panel.go +++ b/pkg/gui/services_panel.go @@ -284,12 +284,18 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error { return nil } - rebuildCommand := utils.ApplyTemplate(gui.Config.UserConfig.CommandTemplates.RebuildService, service) + rebuildCommand := utils.ApplyTemplate( + gui.Config.UserConfig.CommandTemplates.RebuildService, + service, + ) options := []*commandOption{ { description: gui.Tr.Restart, - command: utils.ApplyTemplate(gui.Config.UserConfig.CommandTemplates.RestartService, service), + 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 { @@ -302,7 +308,10 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error { }, { description: gui.Tr.Rebuild, - command: utils.ApplyTemplate(gui.Config.UserConfig.CommandTemplates.RebuildService, service), + 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 diff --git a/pkg/gui/status_panel.go b/pkg/gui/status_panel.go index 0be05354..f10525b7 100644 --- a/pkg/gui/status_panel.go +++ b/pkg/gui/status_panel.go @@ -6,6 +6,8 @@ import ( "github.com/go-errors/errors" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazydocker/pkg/commands" + "github.com/jesseduffield/lazydocker/pkg/utils" ) func (gui *Gui) getStatusContexts() []string { @@ -105,7 +107,12 @@ func (gui *Gui) renderAllLogs() error { gui.clearMainView() - cmd := gui.OSCommand.RunCustomCommand(gui.Config.UserConfig.CommandTemplates.AllLogs) + cmd := gui.OSCommand.RunCustomCommand( + utils.ApplyTemplate( + gui.Config.UserConfig.CommandTemplates.AllLogs, + gui.DockerCommand.NewCommandObject(commands.CommandObject{}), + ), + ) cmd.Stdout = mainView cmd.Stderr = mainView