diff --git a/.gitignore b/.gitignore index ebc489e2..0839d745 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -TODO.md \ No newline at end of file +TODO.md +Lazydocker.code-workspace \ No newline at end of file diff --git a/pkg/commands/container.go b/pkg/commands/container.go index 68499b22..d1e32f50 100644 --- a/pkg/commands/container.go +++ b/pkg/commands/container.go @@ -15,19 +15,35 @@ import ( // Container : A docker Container type Container struct { - Name string - ServiceName string - ID string - Container types.Container - DisplayString string - Client *client.Client - OSCommand *OSCommand - Log *logrus.Entry + Name string + ServiceName string + ContainerNumber string // might make this an int in the future if need be + ProjectName string + ID string + Container types.Container + DisplayString string + Client *client.Client + OSCommand *OSCommand + Log *logrus.Entry + Stats ContainerCliStat +} + +// ContainerCliStat is a stat object returned by the CLI docker stat command +type ContainerCliStat struct { + BlockIO string `json:"BlockIO"` + CPUPerc string `json:"CPUPerc"` + Container string `json:"Container"` + ID string `json:"ID"` + MemPerc string `json:"MemPerc"` + MemUsage string `json:"MemUsage"` + Name string `json:"Name"` + NetIO string `json:"NetIO"` + PIDs string `json:"PIDs"` } // GetDisplayStrings returns the dispaly string of Container func (c *Container) GetDisplayStrings(isFocused bool) []string { - return []string{utils.ColoredString(c.Container.State, c.GetColor()), utils.ColoredString(c.Name, color.FgWhite)} + return []string{utils.ColoredString(c.Container.State, c.GetColor()), utils.ColoredString(c.Name, color.FgWhite), c.Stats.CPUPerc} } // GetColor Container color @@ -79,8 +95,20 @@ 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.GetUserConfig().GetString("commandTemplates.restartService") + command := utils.ApplyTemplate(templateString, c) + return c.OSCommand.RunCommand(command) +} + // Attach attaches the container func (c *Container) Attach() *exec.Cmd { cmd := c.OSCommand.PrepareSubProcess("docker", "attach", "--sig-proxy=false", c.ID) return cmd } + +// Top returns process information +func (c *Container) Top() (types.ContainerProcessList, error) { + return c.Client.ContainerTop(context.Background(), c.ID, []string{}) +} diff --git a/pkg/commands/docker.go b/pkg/commands/docker.go index c1219cdd..d386235d 100644 --- a/pkg/commands/docker.go +++ b/pkg/commands/docker.go @@ -2,6 +2,7 @@ package commands import ( "context" + "encoding/json" "strings" "github.com/docker/docker/api/types" @@ -37,6 +38,40 @@ func NewDockerCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localize }, nil } +// UpdateContainerStats takes a slice of containers and returns the same slice but with new stats added +// TODO: consider using this for everything stats-related +func (c *DockerCommand) UpdateContainerStats(containers []*Container) ([]*Container, error) { + // TODO: consider using a stream rather than polling + command := `docker stats --all --no-trunc --no-stream --format '{{json .}}'` + output, err := c.OSCommand.RunCommandWithOutput(command) + if err != nil { + return nil, err + } + + jsonStats := "[" + strings.Join( + strings.Split( + strings.Trim(output, "\n"), "\n", + ), ",", + ) + "]" + + c.Log.Warn(jsonStats) + + var stats []ContainerCliStat + if err := json.Unmarshal([]byte(jsonStats), &stats); err != nil { + return nil, err + } + + for _, stat := range stats { + for _, container := range containers { + if container.ID == stat.ID { + container.Stats = stat + } + } + } + + return containers, nil +} + // GetContainers returns a slice of docker containers func (c *DockerCommand) GetContainers() ([]*Container, error) { containers, err := c.Client.ContainerList(context.Background(), types.ContainerListOptions{All: true}) @@ -47,22 +82,24 @@ func (c *DockerCommand) GetContainers() ([]*Container, error) { ownContainers := make([]*Container, len(containers)) for i, container := range containers { - serviceName, ok := container.Labels["com.docker.compose.service"] - if !ok { - serviceName = "" - c.Log.Warn("Could not get service name from docker container") - } ownContainers[i] = &Container{ - ID: container.ID, - Name: strings.TrimLeft(container.Names[0], "/"), - ServiceName: serviceName, - Container: container, - Client: c.Client, - OSCommand: c.OSCommand, - Log: c.Log, + ID: container.ID, + Name: strings.TrimLeft(container.Names[0], "/"), + ServiceName: container.Labels["com.docker.compose.service"], + ProjectName: container.Labels["com.docker.compose.project"], + ContainerNumber: container.Labels["com.docker.compose.container"], + Container: container, + Client: c.Client, + OSCommand: c.OSCommand, + Log: c.Log, } } + // ownContainers, err = c.UpdateContainerStats(ownContainers) + // if err != nil { + // return nil, err + // } + return ownContainers, nil } diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index 21402900..e5b04489 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -248,6 +248,8 @@ update: days: 14 # how often a update is checked for reporting: 'undetermined' # one of: 'on' | 'off' | 'undetermined' confirmOnQuit: false +commandTemplates: + restartService: "docker-compose restart {{ .ServiceName }}" `) } diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index 5e1e1274..92f65ba9 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -8,9 +8,7 @@ import ( "io" "os" "os/exec" - "os/signal" "strings" - "syscall" "time" "github.com/docker/docker/api/types" @@ -374,11 +372,14 @@ func (gui *Gui) handleContainerStop(g *gocui.Gui, v *gocui.View) error { } return gui.createConfirmationPanel(gui.g, v, gui.Tr.SLocalize("Confirm"), gui.Tr.SLocalize("StopContainer"), func(g *gocui.Gui, v *gocui.View) error { - if err := container.Stop(); err != nil { - return gui.createErrorPanel(gui.g, err.Error()) - } + return gui.WithWaitingStatus(gui.Tr.SLocalize("StoppingStatus"), func() error { + if err := container.Stop(); err != nil { + return gui.createErrorPanel(gui.g, err.Error()) + } + + return gui.refreshContainers() + }) - return gui.refreshContainers() }, nil) } @@ -405,7 +406,9 @@ func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error { gui.State.Panels.Main.WriterID++ - // Create arbitrary command. + // c := container.Attach() + + // // Create arbitrary command. c := exec.Command("bash") // Start the command with a pty. @@ -414,20 +417,6 @@ func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error { return err } - // Handle pty size. - ch := make(chan os.Signal, 1) - signal.Notify(ch, syscall.SIGWINCH) - go func() { - - for range ch { - x, y := gui.getMainView().Size() - if err := pty.Setsize(ptmx, &pty.Winsize{Cols: uint16(x), Rows: uint16(y), X: 0, Y: 0}); err != nil { - gui.Log.Error(err) - } - } - }() - ch <- syscall.SIGWINCH // Initial resize. - go func() { // Set stdin in raw mode. oldState, err := terminal.MakeRaw(int(os.Stdin.Fd())) @@ -445,7 +434,7 @@ func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error { gui.State.Panels.Main.ObjectKey = "attached-" + container.ID scanner := bufio.NewScanner(ptmx) - scanner.Split(bufio.ScanRunes) + scanner.Split(bufio.ScanBytes) content := "" for scanner.Scan() { @@ -465,3 +454,18 @@ func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error { return nil } + +func (gui *Gui) handleServiceRestart(g *gocui.Gui, v *gocui.View) error { + container, err := gui.getSelectedContainer(g) + if err != nil { + return nil + } + + return gui.WithWaitingStatus(gui.Tr.SLocalize("RestartingStatus"), func() error { + if err := container.RestartService(); err != nil { + return gui.createErrorPanel(gui.g, err.Error()) + } + + return gui.refreshContainers() + }) +} diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 977515a0..4b365e79 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -179,6 +179,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Handler: gui.handleContainerAttach, Description: gui.Tr.SLocalize("attachContainer"), }, + { + ViewName: "containers", + Key: 'R', + Modifier: gocui.ModNone, + Handler: gui.handleServiceRestart, + Description: gui.Tr.SLocalize("restartService"), + }, { ViewName: "images", Key: '[', diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index fdf4ae55..2478abc3 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -780,6 +780,10 @@ func addEnglish(i18nObject *i18n.Bundle) error { ID: "RestartingStatus", Other: "restarting", }, + &i18n.Message{ + ID: "StoppingStatus", + Other: "stopping", + }, &i18n.Message{ ID: "removeContainer", Other: "remove container", @@ -792,6 +796,10 @@ func addEnglish(i18nObject *i18n.Bundle) error { ID: "restartContainer", Other: "restart container", }, + &i18n.Message{ + ID: "restartService", + Other: "restart container's service", + }, &i18n.Message{ ID: "previousContext", Other: "previous context", diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 506edae0..1d04699e 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -1,8 +1,10 @@ package utils import ( + "bytes" "encoding/json" "fmt" + "html/template" "math" "reflect" "regexp" @@ -288,3 +290,9 @@ func FormatDecimalBytes(b int) string { } return "a lot" } + +func ApplyTemplate(str string, object interface{}) string { + var buf bytes.Buffer + template.Must(template.New("").Parse(str)).Execute(&buf, object) + return buf.String() +}