support bulk commands in each of the panels

This commit is contained in:
Jesse Duffield 2019-07-06 11:40:23 +10:00
parent c99bdaa5ff
commit 1f9f82601b
10 changed files with 265 additions and 70 deletions

View file

@ -2,12 +2,14 @@ package commands
import (
"context"
"github.com/docker/docker/api/types/container"
"fmt"
"os/exec"
"strconv"
"strings"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
@ -317,6 +319,7 @@ func (c *Container) GetColor() color.Attribute {
// Remove removes the container
func (c *Container) Remove(options types.ContainerRemoveOptions) error {
c.Log.Warn(fmt.Sprintf("removing container %s", c.Name))
if err := c.Client.ContainerRemove(context.Background(), c.ID, options); err != nil {
if strings.Contains(err.Error(), "Stop the container before attempting removal or force remove") {
return ComplexError{
@ -333,16 +336,20 @@ func (c *Container) Remove(options types.ContainerRemoveOptions) error {
// Stop stops the container
func (c *Container) Stop() error {
c.Log.Warn(fmt.Sprintf("stopping container %s", c.Name))
return c.Client.ContainerStop(context.Background(), c.ID, nil)
}
// Restart restarts the container
func (c *Container) Restart() error {
c.Log.Warn(fmt.Sprintf("restarting container %s", c.Name))
return c.Client.ContainerRestart(context.Background(), c.ID, nil)
}
// Attach attaches the container
func (c *Container) Attach() (*exec.Cmd, error) {
c.Log.Warn(fmt.Sprintf("attaching to container %s", c.Name))
// verify that we can in fact attach to this container
if !c.Details.Config.OpenStdin {
return nil, errors.New(c.Tr.UnattachableContainerError)

View file

@ -50,6 +50,10 @@ type UserConfig struct {
// those are found in the commands package
CustomCommands CustomCommands `yaml:"customCommands,omitempty"`
// BulkCommands are commands that apply to all items in a panel e.g.
// killing all containers, stopping all services, or pruning all images
BulkCommands CustomCommands `yaml:"bulkCommands,omitempty"`
// OS determines what defaults are set for opening files and links
OS OSConfig `yaml:"oS,omitempty"`
@ -259,10 +263,10 @@ type CustomCommands struct {
// Services contains the custom commands for services
Services []CustomCommand `yaml:"services,omitempty"`
// Services contains the custom commands for services
// Images contains the custom commands for images
Images []CustomCommand `yaml:"images,omitempty"`
// Services contains the custom commands for services
// Volumes contains the custom commands for volumes
Volumes []CustomCommand `yaml:"volumes,omitempty"`
}
@ -288,6 +292,9 @@ type CustomCommand struct {
// field has no effect on customcommands under the 'communications' part of
// the customCommand config.
ServiceNames []string `yaml:"serviceNames"`
// InternalFunction is the name of a function inside lazydocker that we want to run, as opposed to a command-line command. This is only used internally and can't be configured by the user
InternalFunction func() error `yaml:"internalFunction"`
}
// GetDefaultConfig returns the application default configuration NOTE (to
@ -344,6 +351,52 @@ func GetDefaultConfig() UserConfig {
Images: []CustomCommand{},
Volumes: []CustomCommand{},
},
BulkCommands: CustomCommands{
Services: []CustomCommand{
{
Name: "up",
Command: "{{ .DockerCompose }} up -d",
},
{
Name: "up (attached)",
Command: "{{ .DockerCompose }} up",
Attach: true,
},
{
Name: "stop",
Command: "{{ .DockerCompose }} stop",
},
{
Name: "pull",
Command: "{{ .DockerCompose }} pull",
Attach: true,
},
{
Name: "build",
Command: "{{ .DockerCompose }} build --parallel --force-rm",
Attach: true,
},
{
Name: "down",
Command: "{{ .DockerCompose }} down",
},
{
Name: "down with volumes",
Command: "{{ .DockerCompose }} down --volumes",
},
{
Name: "down with images",
Command: "{{ .DockerCompose }} down --rmi all",
},
{
Name: "down with volumes and images",
Command: "{{ .DockerCompose }} down --volumes --rmi all",
},
},
Containers: []CustomCommand{},
Images: []CustomCommand{},
Volumes: []CustomCommand{},
},
OS: GetPlatformDefaultConfig(),
Update: UpdateConfig{
Method: "never",

View file

@ -11,6 +11,7 @@ import (
"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"
)
@ -476,8 +477,8 @@ func (gui *Gui) handleContainerAttach(g *gocui.Gui, v *gocui.View) error {
return gui.Errors.ErrSubProcess
}
func (gui *Gui) handlePruneContainers(g *gocui.Gui, v *gocui.View) error {
return gui.createConfirmationPanel(gui.g, v, gui.Tr.Confirm, gui.Tr.ConfirmPruneContainers, func(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handlePruneContainers() error {
return gui.createConfirmationPanel(gui.g, gui.getContainersView(), gui.Tr.Confirm, gui.Tr.ConfirmPruneContainers, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
err := gui.DockerCommand.PruneContainers()
if err != nil {
@ -517,3 +518,51 @@ func (gui *Gui) handleContainersCustomCommand(g *gocui.Gui, v *gocui.View) error
return gui.createCustomCommandMenu(customCommands, commandObject)
}
func (gui *Gui) handleStopContainers() error {
return gui.createConfirmationPanel(gui.g, gui.getContainersView(), gui.Tr.Confirm, gui.Tr.ConfirmStopContainers, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.StoppingStatus, func() error {
for _, container := range gui.DockerCommand.Containers {
_ = container.Stop()
}
return nil
})
}, nil)
}
func (gui *Gui) handleRemoveContainers() error {
return gui.createConfirmationPanel(gui.g, gui.getContainersView(), gui.Tr.Confirm, gui.Tr.ConfirmRemoveContainers, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.RemovingStatus, func() error {
for _, container := range gui.DockerCommand.Containers {
_ = container.Remove(types.ContainerRemoveOptions{Force: true})
}
return nil
})
}, nil)
}
func (gui *Gui) handleContainersBulkCommand(g *gocui.Gui, v *gocui.View) error {
baseBulkCommands := []config.CustomCommand{
{
Name: gui.Tr.StopAllContainers,
InternalFunction: gui.handleStopContainers,
},
{
Name: gui.Tr.RemoveAllContainers,
InternalFunction: gui.handleRemoveContainers,
},
{
Name: gui.Tr.PruneContainers,
InternalFunction: gui.handlePruneContainers,
},
}
bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Containers...)
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{})
return gui.createBulkCommandMenu(bulkCommands, commandObject)
}

View file

@ -21,7 +21,7 @@ func (r *customCommandOption) GetDisplayStrings(isFocused bool) []string {
return []string{r.name, utils.ColoredString(r.description, color.FgCyan)}
}
func (gui *Gui) createCustomCommandMenu(customCommands []config.CustomCommand, commandObject commands.CommandObject) error {
func (gui *Gui) createCommandMenu(customCommands []config.CustomCommand, commandObject commands.CommandObject, title string, waitingStatus string) error {
options := make([]*customCommandOption, len(customCommands)+1)
for i, command := range customCommands {
resolvedCommand := utils.ApplyTemplate(command.Command, commandObject)
@ -46,6 +46,10 @@ func (gui *Gui) createCustomCommandMenu(customCommands []config.CustomCommand, c
return nil
}
if option.customCommand.InternalFunction != nil {
return option.customCommand.InternalFunction()
}
// if we have a command for attaching, we attach and return the subprocess error
if option.customCommand.Attach {
cmd := gui.OSCommand.ExecutableFromString(option.command)
@ -53,7 +57,7 @@ func (gui *Gui) createCustomCommandMenu(customCommands []config.CustomCommand, c
return gui.Errors.ErrSubProcess
}
return gui.WithWaitingStatus(gui.Tr.RunningCustomCommandStatus, func() error {
return gui.WithWaitingStatus(waitingStatus, func() error {
if err := gui.OSCommand.RunCommand(option.command); err != nil {
return gui.createErrorPanel(gui.g, err.Error())
}
@ -61,5 +65,13 @@ func (gui *Gui) createCustomCommandMenu(customCommands []config.CustomCommand, c
})
}
return gui.createMenu("", options, len(options), handleMenuPress)
return gui.createMenu(title, options, len(options), handleMenuPress)
}
func (gui *Gui) createCustomCommandMenu(customCommands []config.CustomCommand, commandObject commands.CommandObject) error {
return gui.createCommandMenu(customCommands, commandObject, gui.Tr.CustomCommandTitle, gui.Tr.RunningCustomCommandStatus)
}
func (gui *Gui) createBulkCommandMenu(customCommands []config.CustomCommand, commandObject commands.CommandObject) error {
return gui.createCommandMenu(customCommands, commandObject, gui.Tr.BulkCommandTitle, gui.Tr.RunningBulkCommandStatus)
}

View file

@ -225,6 +225,7 @@ func (gui *Gui) renderGlobalOptions() error {
"PgUp/PgDn": gui.Tr.Scroll,
"← → ↑ ↓": gui.Tr.Navigate,
"esc/q": gui.Tr.Close,
"b": gui.Tr.ViewBulkCommands,
"x": gui.Tr.Menu,
})
}
@ -282,6 +283,9 @@ func (gui *Gui) Run() error {
go func() {
for err := range gui.ErrorChan {
if err == nil {
continue
}
if strings.Contains(err.Error(), "No such container") {
// this happens all the time when e.g. restarting containers so we won't worry about it
gui.Log.Warn(err)

View file

@ -10,6 +10,7 @@ import (
"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"
)
@ -23,7 +24,7 @@ func (gui *Gui) getImageContextTitles() []string {
return []string{gui.Tr.ConfigTitle}
}
func (gui *Gui) getSelectedImage(g *gocui.Gui) (*commands.Image, error) {
func (gui *Gui) getSelectedImage() (*commands.Image, error) {
selectedLine := gui.State.Panels.Images.SelectedLine
if selectedLine == -1 {
return &commands.Image{}, gui.Errors.ErrNoImages
@ -41,7 +42,7 @@ func (gui *Gui) handleImagesClick(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleImageSelect(g *gocui.Gui, v *gocui.View) error {
Image, err := gui.getSelectedImage(g)
Image, err := gui.getSelectedImage()
if err != nil {
if err != gui.Errors.ErrNoImages {
return err
@ -207,7 +208,7 @@ func (r *removeImageOption) GetDisplayStrings(isFocused bool) []string {
}
func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
Image, err := gui.getSelectedImage(g)
Image, err := gui.getSelectedImage()
if err != nil {
return nil
}
@ -248,8 +249,8 @@ func (gui *Gui) handleImagesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
return gui.createMenu("", options, len(options), handleMenuPress)
}
func (gui *Gui) handlePruneImages(g *gocui.Gui, v *gocui.View) error {
return gui.createConfirmationPanel(gui.g, v, gui.Tr.Confirm, gui.Tr.ConfirmPruneImages, func(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handlePruneImages() error {
return gui.createConfirmationPanel(gui.g, gui.getImagesView(), gui.Tr.Confirm, gui.Tr.ConfirmPruneImages, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
err := gui.DockerCommand.PruneImages()
if err != nil {
@ -261,17 +262,30 @@ func (gui *Gui) handlePruneImages(g *gocui.Gui, v *gocui.View) error {
}
func (gui *Gui) handleImagesCustomCommand(g *gocui.Gui, v *gocui.View) error {
image, err := gui.getSelectedImage(g)
image, err := gui.getSelectedImage()
if err != nil {
return nil
}
commandObject := gui.DockerCommand.NewCommandObject(
commands.CommandObject{
Image: image,
})
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
Image: image,
})
customCommands := gui.Config.UserConfig.CustomCommands.Images
return gui.createCustomCommandMenu(customCommands, commandObject)
}
func (gui *Gui) handleImagesBulkCommand(g *gocui.Gui, v *gocui.View) error {
baseBulkCommands := []config.CustomCommand{
{
Name: gui.Tr.PruneImages,
InternalFunction: gui.handlePruneImages,
},
}
bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Images...)
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{})
return gui.createBulkCommandMenu(bulkCommands, commandObject)
}

View file

@ -239,13 +239,6 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleContainerAttach,
Description: gui.Tr.Attach,
},
{
ViewName: "containers",
Key: 'D',
Modifier: gocui.ModNone,
Handler: gui.handlePruneContainers,
Description: gui.Tr.PruneContainers,
},
{
ViewName: "containers",
Key: 'm',
@ -260,6 +253,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleContainersCustomCommand,
Description: gui.Tr.RunCustomCommand,
},
{
ViewName: "containers",
Key: 'b',
Modifier: gocui.ModNone,
Handler: gui.handleContainersBulkCommand,
Description: gui.Tr.ViewBulkCommands,
},
{
ViewName: "services",
Key: 'd',
@ -323,6 +323,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
Handler: gui.handleServicesCustomCommand,
Description: gui.Tr.RunCustomCommand,
},
{
ViewName: "services",
Key: 'b',
Modifier: gocui.ModNone,
Handler: gui.handleServicesBulkCommand,
Description: gui.Tr.ViewBulkCommands,
},
{
ViewName: "images",
Key: '[',
@ -353,10 +360,17 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
},
{
ViewName: "images",
Key: 'D',
Key: 'b',
Modifier: gocui.ModNone,
Handler: gui.handlePruneImages,
Description: gui.Tr.PruneImages,
Handler: gui.handleImagesBulkCommand,
Description: gui.Tr.ViewBulkCommands,
},
{
ViewName: "images",
Key: 'c',
Modifier: gocui.ModNone,
Handler: gui.handleImagesCustomCommand,
Description: gui.Tr.RunCustomCommand,
},
{
ViewName: "volumes",
@ -388,10 +402,17 @@ func (gui *Gui) GetInitialKeybindings() []*Binding {
},
{
ViewName: "volumes",
Key: 'D',
Key: 'b',
Modifier: gocui.ModNone,
Handler: gui.handlePruneVolumes,
Description: gui.Tr.PruneVolumes,
Handler: gui.handleVolumesBulkCommand,
Description: gui.Tr.ViewBulkCommands,
},
{
ViewName: "volumes",
Key: 'c',
Modifier: gocui.ModNone,
Handler: gui.handleVolumesCustomCommand,
Description: gui.Tr.RunCustomCommand,
},
{
ViewName: "main",

View file

@ -210,7 +210,7 @@ func (gui *Gui) handleServiceRemoveMenu(g *gocui.Gui, v *gocui.View) error {
},
}
return gui.createCommandMenu(options, gui.Tr.RemovingStatus)
return gui.createServiceCommandMenu(options, gui.Tr.RemovingStatus)
}
func (gui *Gui) handleServiceStop(g *gocui.Gui, v *gocui.View) error {
@ -349,7 +349,7 @@ func (gui *Gui) handleServiceRestartMenu(g *gocui.Gui, v *gocui.View) error {
return gui.createMenu("", options, len(options), handleMenuPress)
}
func (gui *Gui) createCommandMenu(options []*commandOption, status string) error {
func (gui *Gui) createServiceCommandMenu(options []*commandOption, status string) error {
handleMenuPress := func(index int) error {
if options[index].command == "" {
return nil
@ -402,3 +402,10 @@ L:
return gui.createCustomCommandMenu(customCommands, commandObject)
}
func (gui *Gui) handleServicesBulkCommand(g *gocui.Gui, v *gocui.View) error {
bulkCommands := gui.Config.UserConfig.BulkCommands.Services
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{})
return gui.createBulkCommandMenu(bulkCommands, commandObject)
}

View file

@ -7,6 +7,7 @@ import (
"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"
)
@ -239,8 +240,8 @@ func (gui *Gui) handleVolumesRemoveMenu(g *gocui.Gui, v *gocui.View) error {
return gui.createMenu("", options, len(options), handleMenuPress)
}
func (gui *Gui) handlePruneVolumes(g *gocui.Gui, v *gocui.View) error {
return gui.createConfirmationPanel(gui.g, v, gui.Tr.Confirm, gui.Tr.ConfirmPruneVolumes, func(g *gocui.Gui, v *gocui.View) error {
func (gui *Gui) handlePruneVolumes() error {
return gui.createConfirmationPanel(gui.g, gui.getVolumesView(), gui.Tr.Confirm, gui.Tr.ConfirmPruneVolumes, func(g *gocui.Gui, v *gocui.View) error {
return gui.WithWaitingStatus(gui.Tr.PruningStatus, func() error {
err := gui.DockerCommand.PruneVolumes()
if err != nil {
@ -257,12 +258,25 @@ func (gui *Gui) handleVolumesCustomCommand(g *gocui.Gui, v *gocui.View) error {
return nil
}
commandObject := gui.DockerCommand.NewCommandObject(
commands.CommandObject{
Volume: volume,
})
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{
Volume: volume,
})
customCommands := gui.Config.UserConfig.CustomCommands.Volumes
return gui.createCustomCommandMenu(customCommands, commandObject)
}
func (gui *Gui) handleVolumesBulkCommand(g *gocui.Gui, v *gocui.View) error {
baseBulkCommands := []config.CustomCommand{
{
Name: gui.Tr.PruneVolumes,
InternalFunction: gui.handlePruneVolumes,
},
}
bulkCommands := append(baseBulkCommands, gui.Config.UserConfig.BulkCommands.Volumes...)
commandObject := gui.DockerCommand.NewCommandObject(commands.CommandObject{})
return gui.createBulkCommandMenu(bulkCommands, commandObject)
}

View file

@ -28,6 +28,7 @@ type TranslationSet struct {
Donate string
Cancel string
CustomCommandTitle string
BulkCommandTitle string
Remove string
HideStopped string
ForceRemove string
@ -41,6 +42,7 @@ type TranslationSet struct {
StoppingStatus string
RemovingStatus string
RunningCustomCommandStatus string
RunningBulkCommandStatus string
RemoveService string
Stop string
Restart string
@ -67,13 +69,18 @@ type TranslationSet struct {
PruneContainers string
PruneVolumes string
ConfirmPruneContainers string
ConfirmStopContainers string
ConfirmRemoveContainers string
ConfirmPruneImages string
ConfirmPruneVolumes string
PruningStatus string
StopService string
PressEnterToReturn string
StopAllContainers string
RemoveAllContainers string
ViewRestartOptions string
RunCustomCommand string
ViewBulkCommands string
LogsTitle string
ConfigTitle string
@ -93,6 +100,7 @@ func englishSet() TranslationSet {
RestartingStatus: "restarting",
StoppingStatus: "stopping",
RunningCustomCommandStatus: "running custom command",
RunningBulkCommandStatus: "running bulk command",
RunningSubprocess: "running subprocess",
NoViewMachingNewLineFocusedSwitchStatement: "No view matching newLineFocused switch statement",
@ -106,37 +114,40 @@ func englishSet() TranslationSet {
Donate: "Donate",
Confirm: "Confirm",
Return: "return",
FocusMain: "focus main panel",
Navigate: "navigate",
Execute: "execute",
Close: "close",
Menu: "menu",
Scroll: "scroll",
OpenConfig: "open lazydocker config",
EditConfig: "edit lazydocker config",
Cancel: "cancel",
Remove: "remove",
HideStopped: "Hide/Show stopped containers",
ForceRemove: "force remove",
RemoveWithVolumes: "remove with volumes",
RemoveService: "remove containers",
Stop: "stop",
Restart: "restart",
Rebuild: "rebuild",
Recreate: "recreate",
PreviousContext: "previous tab",
NextContext: "next tab",
Attach: "attach",
ViewLogs: "view logs",
RemoveImage: "remove image",
RemoveVolume: "remove volume",
RemoveWithoutPrune: "remove without deleting untagged parents",
PruneContainers: "prune exited containers",
PruneVolumes: "prune unused volumes",
PruneImages: "prune unused images",
ViewRestartOptions: "view restart options",
RunCustomCommand: "run predefined custom command",
Return: "return",
FocusMain: "focus main panel",
Navigate: "navigate",
Execute: "execute",
Close: "close",
Menu: "menu",
Scroll: "scroll",
OpenConfig: "open lazydocker config",
EditConfig: "edit lazydocker config",
Cancel: "cancel",
Remove: "remove",
HideStopped: "Hide/Show stopped containers",
ForceRemove: "force remove",
RemoveWithVolumes: "remove with volumes",
RemoveService: "remove containers",
Stop: "stop",
Restart: "restart",
Rebuild: "rebuild",
Recreate: "recreate",
PreviousContext: "previous tab",
NextContext: "next tab",
Attach: "attach",
ViewLogs: "view logs",
RemoveImage: "remove image",
RemoveVolume: "remove volume",
RemoveWithoutPrune: "remove without deleting untagged parents",
PruneContainers: "prune exited containers",
PruneVolumes: "prune unused volumes",
PruneImages: "prune unused images",
StopAllContainers: "stop all containers",
RemoveAllContainers: "remove all containers (forced)",
ViewRestartOptions: "view restart options",
RunCustomCommand: "run predefined custom command",
ViewBulkCommands: "view bulk commands",
AnonymousReportingTitle: "Help make lazydocker better",
AnonymousReportingPrompt: "Would you like to enable anonymous reporting data to help improve lazydocker?",
@ -150,6 +161,7 @@ func englishSet() TranslationSet {
ImagesTitle: "Images",
VolumesTitle: "Volumes",
CustomCommandTitle: "Custom Command:",
BulkCommandTitle: "Bulk Command:",
ErrorTitle: "Error",
LogsTitle: "Logs",
ConfigTitle: "Config",
@ -169,6 +181,8 @@ func englishSet() TranslationSet {
NotEnoughSpace: "Not enough space to render panels",
ConfirmPruneImages: "Are you sure you want to prune all unused images?",
ConfirmPruneContainers: "Are you sure you want to prune all stopped containers?",
ConfirmStopContainers: "Are you sure you want to stop all containers?",
ConfirmRemoveContainers: "Are you sure you want to remove all containers?",
ConfirmPruneVolumes: "Are you sure you want to prune all unused volumes?",
StopService: "Are you sure you want to stop this service's containers?",
StopContainer: "Are you sure you want to stop this container?",