From a100caa0d95544e96f4d9bc178ab597743f378f1 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Wed, 11 May 2022 18:17:41 +1000 Subject: [PATCH] use sdk to inspect containers --- pkg/commands/container.go | 213 ++++-------------------------------- pkg/commands/docker.go | 27 ++--- pkg/gui/containers_panel.go | 16 ++- pkg/gui/project_panel.go | 5 +- pkg/i18n/english.go | 2 + 5 files changed, 48 insertions(+), 215 deletions(-) diff --git a/pkg/commands/container.go b/pkg/commands/container.go index b043c658..61201318 100644 --- a/pkg/commands/container.go +++ b/pkg/commands/container.go @@ -7,7 +7,6 @@ import ( "strconv" "strings" "sync" - "time" "github.com/docker/docker/api/types/container" @@ -40,7 +39,7 @@ type Container struct { Config *config.AppConfig Log *logrus.Entry StatHistory []*RecordedStats - Details Details + Details types.ContainerJSON MonitoringStats bool DockerCommand LimitedDockerCommand Tr *i18n.TranslationSet @@ -48,190 +47,6 @@ type Container struct { StatsMutex sync.Mutex } -// Details is a struct containing what we get back from `docker inspect` on a container -type Details struct { - ID string `json:"Id"` - Created time.Time `json:"Created"` - Path string `json:"Path"` - Args []string `json:"Args"` - State struct { - Status string `json:"Status"` - Running bool `json:"Running"` - Paused bool `json:"Paused"` - Restarting bool `json:"Restarting"` - OOMKilled bool `json:"OOMKilled"` - Dead bool `json:"Dead"` - Pid int `json:"Pid"` - ExitCode int `json:"ExitCode"` - Error string `json:"Error"` - StartedAt time.Time `json:"StartedAt"` - FinishedAt time.Time `json:"FinishedAt"` - Health types.Health `json:"Health"` - } `json:"State"` - Image string `json:"Image"` - ResolvConfPath string `json:"ResolvConfPath"` - HostnamePath string `json:"HostnamePath"` - HostsPath string `json:"HostsPath"` - LogPath string `json:"LogPath"` - Name string `json:"Name"` - RestartCount int `json:"RestartCount"` - Driver string `json:"Driver"` - Platform string `json:"Platform"` - MountLabel string `json:"MountLabel"` - ProcessLabel string `json:"ProcessLabel"` - AppArmorProfile string `json:"AppArmorProfile"` - ExecIDs interface{} `json:"ExecIDs"` - HostConfig struct { - Binds []string `json:"Binds"` - ContainerIDFile string `json:"ContainerIDFile"` - LogConfig struct { - Type string `json:"Type"` - Config struct{} `json:"Config"` - } `json:"LogConfig"` - NetworkMode string `json:"NetworkMode"` - PortBindings struct{} `json:"PortBindings"` - RestartPolicy struct { - Name string `json:"Name"` - MaximumRetryCount int `json:"MaximumRetryCount"` - } `json:"RestartPolicy"` - AutoRemove bool `json:"AutoRemove"` - VolumeDriver string `json:"VolumeDriver"` - VolumesFrom []interface{} `json:"VolumesFrom"` - CapAdd interface{} `json:"CapAdd"` - CapDrop interface{} `json:"CapDrop"` - DNS interface{} `json:"Dns"` - DNSOptions interface{} `json:"DnsOptions"` - DNSSearch interface{} `json:"DnsSearch"` - ExtraHosts interface{} `json:"ExtraHosts"` - GroupAdd interface{} `json:"GroupAdd"` - IpcMode string `json:"IpcMode"` - Cgroup string `json:"Cgroup"` - Links interface{} `json:"Links"` - OomScoreAdj int `json:"OomScoreAdj"` - PidMode string `json:"PidMode"` - Privileged bool `json:"Privileged"` - PublishAllPorts bool `json:"PublishAllPorts"` - ReadonlyRootfs bool `json:"ReadonlyRootfs"` - SecurityOpt interface{} `json:"SecurityOpt"` - UTSMode string `json:"UTSMode"` - UsernsMode string `json:"UsernsMode"` - ShmSize int `json:"ShmSize"` - Runtime string `json:"Runtime"` - ConsoleSize []int `json:"ConsoleSize"` - Isolation string `json:"Isolation"` - CPUShares int `json:"CpuShares"` - Memory int `json:"Memory"` - NanoCpus int `json:"NanoCpus"` - CgroupParent string `json:"CgroupParent"` - BlkioWeight int `json:"BlkioWeight"` - BlkioWeightDevice interface{} `json:"BlkioWeightDevice"` - BlkioDeviceReadBps interface{} `json:"BlkioDeviceReadBps"` - BlkioDeviceWriteBps interface{} `json:"BlkioDeviceWriteBps"` - BlkioDeviceReadIOps interface{} `json:"BlkioDeviceReadIOps"` - BlkioDeviceWriteIOps interface{} `json:"BlkioDeviceWriteIOps"` - CPUPeriod int `json:"CpuPeriod"` - CPUQuota int `json:"CpuQuota"` - CPURealtimePeriod int `json:"CpuRealtimePeriod"` - CPURealtimeRuntime int `json:"CpuRealtimeRuntime"` - CpusetCpus string `json:"CpusetCpus"` - CpusetMems string `json:"CpusetMems"` - Devices interface{} `json:"Devices"` - DeviceCgroupRules interface{} `json:"DeviceCgroupRules"` - DiskQuota int `json:"DiskQuota"` - KernelMemory int `json:"KernelMemory"` - MemoryReservation int `json:"MemoryReservation"` - MemorySwap int `json:"MemorySwap"` - MemorySwappiness interface{} `json:"MemorySwappiness"` - OomKillDisable bool `json:"OomKillDisable"` - PidsLimit int `json:"PidsLimit"` - Ulimits interface{} `json:"Ulimits"` - CPUCount int `json:"CpuCount"` - CPUPercent int `json:"CpuPercent"` - IOMaximumIOps int `json:"IOMaximumIOps"` - IOMaximumBandwidth int `json:"IOMaximumBandwidth"` - MaskedPaths []string `json:"MaskedPaths"` - ReadonlyPaths []string `json:"ReadonlyPaths"` - } `json:"HostConfig"` - GraphDriver struct { - Data struct { - LowerDir string `json:"LowerDir"` - MergedDir string `json:"MergedDir"` - UpperDir string `json:"UpperDir"` - WorkDir string `json:"WorkDir"` - } `json:"Data"` - Name string `json:"Name"` - } `json:"GraphDriver"` - Mounts []struct { - Type string `json:"Type"` - Name string `json:"Name,omitempty"` - Source string `json:"Source"` - Destination string `json:"Destination"` - Driver string `json:"Driver,omitempty"` - Mode string `json:"Mode"` - RW bool `json:"RW"` - Propagation string `json:"Propagation"` - } `json:"Mounts"` - Config struct { - Hostname string `json:"Hostname"` - Domainname string `json:"Domainname"` - User string `json:"User"` - AttachStdin bool `json:"AttachStdin"` - AttachStdout bool `json:"AttachStdout"` - AttachStderr bool `json:"AttachStderr"` - Tty bool `json:"Tty"` - OpenStdin bool `json:"OpenStdin"` - StdinOnce bool `json:"StdinOnce"` - Env []string `json:"Env"` - Cmd []string `json:"Cmd"` - Image string `json:"Image"` - Volumes struct { - APIBundle struct{} `json:"/api-bundle"` - App struct{} `json:"/app"` - } `json:"Volumes"` - WorkingDir string `json:"WorkingDir"` - Entrypoint interface{} `json:"Entrypoint"` - OnBuild interface{} `json:"OnBuild"` - Labels map[string]string `json:"Labels"` - } `json:"Config"` - NetworkSettings struct { - Bridge string `json:"Bridge"` - SandboxID string `json:"SandboxID"` - HairpinMode bool `json:"HairpinMode"` - LinkLocalIPv6Address string `json:"LinkLocalIPv6Address"` - LinkLocalIPv6PrefixLen int `json:"LinkLocalIPv6PrefixLen"` - Ports map[string][]struct { - HostIP string `json:"HostIP"` - HostPort string `json:"HostPort"` - } `json:"Ports"` - SandboxKey string `json:"SandboxKey"` - SecondaryIPAddresses interface{} `json:"SecondaryIPAddresses"` - SecondaryIPv6Addresses interface{} `json:"SecondaryIPv6Addresses"` - EndpointID string `json:"EndpointID"` - Gateway string `json:"Gateway"` - GlobalIPv6Address string `json:"GlobalIPv6Address"` - GlobalIPv6PrefixLen int `json:"GlobalIPv6PrefixLen"` - IPAddress string `json:"IPAddress"` - IPPrefixLen int `json:"IPPrefixLen"` - IPv6Gateway string `json:"IPv6Gateway"` - MacAddress string `json:"MacAddress"` - Networks map[string]struct { - IPAMConfig interface{} `json:"IPAMConfig"` - Links interface{} `json:"Links"` - Aliases []string `json:"Aliases"` - NetworkID string `json:"NetworkID"` - EndpointID string `json:"EndpointID"` - Gateway string `json:"Gateway"` - IPAddress string `json:"IPAddress"` - IPPrefixLen int `json:"IPPrefixLen"` - IPv6Gateway string `json:"IPv6Gateway"` - GlobalIPv6Address string `json:"GlobalIPv6Address"` - GlobalIPv6PrefixLen int `json:"GlobalIPv6PrefixLen"` - MacAddress string `json:"MacAddress"` - DriverOpts interface{} `json:"DriverOpts"` - } `json:"Networks"` - } `json:"NetworkSettings"` -} - // GetDisplayStrings returns the dispaly string of Container func (c *Container) GetDisplayStrings(isFocused bool) []string { image := strings.TrimPrefix(c.Container.Image, "sha256:") @@ -246,6 +61,10 @@ func (c *Container) GetDisplayStatus() string { // GetDisplayStatus returns the exit code if the container has exited, and the health status if the container is running (and has a health check) func (c *Container) GetDisplaySubstatus() string { + if !c.DetailsLoaded() { + return "" + } + switch c.Container.State { case "exited": return utils.ColoredString( @@ -259,12 +78,19 @@ func (c *Container) GetDisplaySubstatus() string { } func (c *Container) getHealthStatus() string { + if !c.DetailsLoaded() { + return "" + } + healthStatusColorMap := map[string]color.Attribute{ "healthy": color.FgGreen, "unhealthy": color.FgRed, "starting": color.FgYellow, } + if c.Details.State.Health == nil { + return "" + } healthStatus := c.Details.State.Health.Status if healthStatusColor, ok := healthStatusColorMap[healthStatus]; ok { return utils.ColoredString(fmt.Sprintf("(%s)", healthStatus), healthStatusColor) @@ -296,14 +122,16 @@ func (c *Container) GetDisplayCPUPerc() string { // ProducingLogs tells us whether we should bother checking a container's logs func (c *Container) ProducingLogs() bool { - return c.Container.State == "running" && !(c.Details.HostConfig.LogConfig.Type == "none") + return c.Container.State == "running" && c.DetailsLoaded() && c.Details.HostConfig.LogConfig.Type != "none" } // GetColor Container color func (c *Container) GetColor() color.Attribute { switch c.Container.State { case "exited": - if c.Details.State.ExitCode == 0 { + // This means the colour may be briefly yellow and then switch to red upon starting + // Not sure what a better alternative is. + if !c.DetailsLoaded() || c.Details.State.ExitCode == 0 { return color.FgYellow } return color.FgRed @@ -355,7 +183,9 @@ func (c *Container) Restart() error { // Attach attaches the container func (c *Container) Attach() (*exec.Cmd, error) { - c.Log.Warn(fmt.Sprintf("attaching to container %s", c.Name)) + if !c.DetailsLoaded() { + return nil, errors.New(c.Tr.WaitingForContainerInfo) + } // verify that we can in fact attach to this container if !c.Details.Config.OpenStdin { @@ -366,6 +196,7 @@ func (c *Container) Attach() (*exec.Cmd, error) { return nil, errors.New(c.Tr.CannotAttachStoppedContainerError) } + c.Log.Warn(fmt.Sprintf("attaching to container %s", c.Name)) cmd := c.OSCommand.PrepareSubProcess("docker", "attach", "--sig-proxy=false", c.ID) return cmd, nil } @@ -420,7 +251,7 @@ func (c *Container) RenderTop() (string, error) { return utils.RenderTable(append([][]string{result.Titles}, result.Processes...)) } -// DetailsLoaded tells us whether we have yet loaded the details for a container. Because this is an asynchronous operation, sometimes we have the container before we have its details. Details is a struct, not a pointer to a struct, so it starts off with heaps of zero values. One of which is the container Image, which starts as a blank string. Given that every container should have an image, this is a good proxy to use +// DetailsLoaded tells us whether we have yet loaded the details for a container. Because this is an asynchronous operation, sometimes we have the container before we have its details. func (c *Container) DetailsLoaded() bool { - return c.Details.Image != "" + return c.Details.ContainerJSONBase != nil } diff --git a/pkg/commands/docker.go b/pkg/commands/docker.go index b6848395..8b2825c4 100644 --- a/pkg/commands/docker.go +++ b/pkg/commands/docker.go @@ -382,26 +382,13 @@ func (c *DockerCommand) UpdateContainerDetails() error { c.ContainerMutex.Lock() defer c.ContainerMutex.Unlock() - containers := c.Containers - - ids := make([]string, len(containers)) - for i, container := range containers { - ids[i] = container.ID - } - - cmd := c.OSCommand.RunCustomCommand("docker inspect " + strings.Join(ids, " ")) - output, err := cmd.CombinedOutput() - if err != nil { - return err - } - - var details []*Details - if err := json.Unmarshal(output, &details); err != nil { - return err - } - - for i, container := range containers { - container.Details = *details[i] + for _, container := range c.Containers { + details, err := c.Client.ContainerInspect(context.Background(), container.ID) + if err != nil { + c.Log.Error(err) + } else { + container.Details = details + } } return nil diff --git a/pkg/gui/containers_panel.go b/pkg/gui/containers_panel.go index f45e1dda..7c227cf3 100644 --- a/pkg/gui/containers_panel.go +++ b/pkg/gui/containers_panel.go @@ -93,6 +93,12 @@ func (gui *Gui) handleContainerSelect(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) renderContainerEnv(container *commands.Container) error { + if !container.DetailsLoaded() { + return gui.T.NewTask(func(stop chan struct{}) { + _ = gui.renderString(gui.g, "main", gui.Tr.WaitingForContainerInfo) + }) + } + mainView := gui.getMainView() mainView.Autoscroll = false mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel @@ -125,6 +131,12 @@ func (gui *Gui) renderContainerEnv(container *commands.Container) error { } func (gui *Gui) renderContainerConfig(container *commands.Container) error { + if !container.DetailsLoaded() { + return gui.T.NewTask(func(stop chan struct{}) { + _ = gui.renderString(gui.g, "main", gui.Tr.WaitingForContainerInfo) + }) + } + mainView := gui.getMainView() mainView.Autoscroll = false mainView.Wrap = gui.Config.UserConfig.Gui.WrapMainPanel @@ -142,9 +154,9 @@ func (gui *Gui) renderContainerConfig(container *commands.Container) error { output += "\n" for _, mount := range container.Details.Mounts { if mount.Type == "volume" { - output += fmt.Sprintf("%s%s %s\n", strings.Repeat(" ", padding), utils.ColoredString(mount.Type+":", color.FgYellow), mount.Name) + output += fmt.Sprintf("%s%s %s\n", strings.Repeat(" ", padding), utils.ColoredString(string(mount.Type)+":", color.FgYellow), mount.Name) } else { - output += fmt.Sprintf("%s%s %s:%s\n", strings.Repeat(" ", padding), utils.ColoredString(mount.Type+":", color.FgYellow), mount.Source, mount.Destination) + output += fmt.Sprintf("%s%s %s:%s\n", strings.Repeat(" ", padding), utils.ColoredString(string(mount.Type)+":", color.FgYellow), mount.Source, mount.Destination) } } } else { diff --git a/pkg/gui/project_panel.go b/pkg/gui/project_panel.go index 2e3d7b5c..743c6825 100644 --- a/pkg/gui/project_panel.go +++ b/pkg/gui/project_panel.go @@ -44,8 +44,9 @@ func (gui *Gui) getProjectName() string { projectName := path.Base(gui.Config.ProjectDir) if gui.DockerCommand.InDockerComposeProject { for _, service := range gui.DockerCommand.Services { - if service.Container != nil { - return service.Container.Details.Config.Labels["com.docker.compose.project"] + container := service.Container + if container != nil && container.DetailsLoaded() { + return container.Details.Config.Labels["com.docker.compose.project"] } } } diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 3ae63d04..608e384a 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -21,6 +21,7 @@ type TranslationSet struct { ErrorOccurred string ConnectionFailed string UnattachableContainerError string + WaitingForContainerInfo string CannotAttachStoppedContainerError string CannotAccessDockerSocketError string CannotKillChildError string @@ -115,6 +116,7 @@ func englishSet() TranslationSet { ErrorOccurred: "An error occurred! Please create an issue at https://github.com/jesseduffield/lazydocker/issues", ConnectionFailed: "connection to docker client failed. You may need to restart the docker client", UnattachableContainerError: "Container does not support attaching. You must either run the service with the '-it' flag or use `stdin_open: true, tty: true` in the docker-compose.yml file", + WaitingForContainerInfo: "Cannot proceed until docker gives us more information about the container. Please retry in a few moments.", CannotAttachStoppedContainerError: "You cannot attach to a stopped container, you need to start it first (which you can actually do with the 'r' key) (yes I'm too lazy to do this automatically for you) (pretty cool that I get to communicate one-on-one with you in the form of an error message though)", CannotAccessDockerSocketError: "Can't access docker socket at: unix:///var/run/docker.sock\nRun lazydocker as root or read https://docs.docker.com/install/linux/linux-postinstall/", CannotKillChildError: "Waited three seconds for child process to stop. There may be an orphan process that continues to run on your system.",