You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lazydocker/pkg/commands/container.go

525 lines
17 KiB
Go

package commands
import (
"context"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/term"
"io"
"os"
"os/exec"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/fatih/color"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazydocker/pkg/config"
"github.com/jesseduffield/lazydocker/pkg/i18n"
"github.com/jesseduffield/lazydocker/pkg/utils"
"github.com/sirupsen/logrus"
"golang.org/x/xerrors"
)
// Container : A docker Container
type Container struct {
Name string
ServiceName string
ContainerNumber string // might make this an int in the future if need be
// OneOff tells us if the container is just a job container or is actually bound to the service
OneOff bool
ProjectName string
ID string
Container types.Container
DisplayString string
Client *client.Client
OSCommand *OSCommand
Config *config.AppConfig
Log *logrus.Entry
CLIStats ContainerCliStat // for realtime we use the CLI, for long-term we use the client
StatHistory []RecordedStats
Details Details
MonitoringStats bool
DockerCommand LimitedDockerCommand
Tr *i18n.TranslationSet
}
// 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"`
} `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"`
}
// 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{c.GetDisplayStatus(), c.Name, c.GetDisplayCPUPerc()}
}
// GetDisplayStatus returns the colored status of the container
func (c *Container) GetDisplayStatus() string {
state := c.Container.State
if c.Container.State == "exited" {
state += " (" + strconv.Itoa(c.Details.State.ExitCode) + ")"
}
return utils.ColoredString(state, c.GetColor())
}
// GetDisplayCPUPerc colors the cpu percentage based on how extreme it is
func (c *Container) GetDisplayCPUPerc() string {
stats := c.CLIStats
if stats.CPUPerc == "" {
return ""
}
percentage, err := strconv.ParseFloat(strings.TrimSuffix(stats.CPUPerc, "%"), 32)
if err != nil {
// probably complaining about not being able to convert '--'
return ""
}
var clr color.Attribute
if percentage > 90 {
clr = color.FgRed
} else if percentage > 50 {
clr = color.FgYellow
} else {
clr = color.FgWhite
}
return utils.ColoredString(stats.CPUPerc, clr)
}
// 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")
}
// GetColor Container color
func (c *Container) GetColor() color.Attribute {
switch c.Container.State {
case "exited":
if c.Details.State.ExitCode == 0 {
return color.FgYellow
}
return color.FgRed
case "created":
return color.FgCyan
case "running":
return color.FgGreen
case "paused":
return color.FgYellow
case "dead":
return color.FgRed
case "restarting":
return color.FgBlue
case "removing":
return color.FgMagenta
default:
return color.FgWhite
}
}
// Remove removes the container
func (c *Container) Remove(options types.ContainerRemoveOptions) error {
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{
Code: MustStopContainer,
Message: err.Error(),
frame: xerrors.Caller(1),
}
}
return err
}
return nil
}
// Stop stops the container
func (c *Container) Stop() error {
return c.Client.ContainerStop(context.Background(), c.ID, nil)
}
// Restart restarts the container
func (c *Container) Restart() error {
return c.Client.ContainerRestart(context.Background(), c.ID, nil)
}
// Attach attaches the container
func (c *Container) Attach() error {
// verify that we can in fact attach to this container
if !c.Details.Config.OpenStdin {
return errors.New(c.Tr.UnattachableContainerError)
}
if c.Container.State == "exited" {
return errors.New(c.Tr.CannotAttachStoppedContainerError)
}
options := types.ContainerAttachOptions{
Stream: true,
Stdin: c.Details.Config.OpenStdin,
Stdout: true,
Stderr: true,
Logs: true,
}
hijack, err := c.Client.ContainerAttach(context.Background(), c.ID, options)
if err != nil {
return err
}
defer hijack.Close()
fd := os.Stdin.Fd()
oldState, err := term.MakeRaw(fd)
if err != nil {
return err
}
channel := make(chan os.Signal, 1)
signal.Notify(channel, syscall.SIGWINCH)
// initial resize
//
// without this user needs to manually resize terminal,
// to get the prompt under cursor
//
// terminal width and height is artificially incremented
// and then it returns to nominal size
channel <- syscall.SIGWINCH
err = c.Resize(fd, true)
if err != nil {
return err
}
// read output from container
go func() {
output := os.Stdout
clearScreen := "\033[2J"
showCursor := "\033[?25h"
noColor := "\033[0m"
_, _ = io.Copy(output, strings.NewReader(clearScreen+showCursor+noColor))
_, _ = io.Copy(output, hijack.Conn)
channel <- syscall.SIGINT
}()
// send input to container
if c.Details.Config.OpenStdin {
go func() {
// the default escape key sequence: ctrl-p, ctrl-q
//
// shamelessly taken from docker/cli
escapeKeys := []byte{16, 17}
// Stop reading if escape sequence had been entered
input := term.NewEscapeProxy(os.Stdin, escapeKeys)
_, _ = io.Copy(hijack.Conn, input)
channel <- syscall.SIGINT
}()
}
for {
sig := <-channel
if sig == syscall.SIGWINCH {
err := c.Resize(fd, false)
if err != nil {
return err
}
} else {
err = term.RestoreTerminal(fd, oldState)
if err != nil {
return err
}
return nil
}
}
}
// Resize gets current terminal size and sends it to Docker.
//
// Bool "fool" parameter if set to true increments height and width by 1.
func (c *Container) Resize(fd uintptr, fool bool) error {
size, err := term.GetWinsize(fd)
if err != nil {
return err
}
options := types.ResizeOptions{
Height: uint(size.Height),
Width: uint(size.Width),
}
if fool {
options.Width++
options.Height++
}
err = c.Client.ContainerResize(context.Background(), c.ID, options)
if err != nil {
return err
}
return nil
}
// Top returns process information
func (c *Container) Top() (container.ContainerTopOKBody, error) {
return c.Client.ContainerTop(context.Background(), c.ID, []string{})
}
// EraseOldHistory removes any history before the user-specified max duration
func (c *Container) EraseOldHistory() {
if c.Config.UserConfig.Stats.MaxDuration == 0 {
return
}
for i, stat := range c.StatHistory {
if time.Since(stat.RecordedAt) < c.Config.UserConfig.Stats.MaxDuration {
c.StatHistory = c.StatHistory[i:]
return
}
}
}
// 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.DockerCommand.NewCommandObject(CommandObject{Container: c}),
)
cmd := c.OSCommand.ExecutableFromString(command)
c.OSCommand.PrepareForChildren(cmd)
return cmd, nil
}
// PruneContainers prunes containers
func (c *DockerCommand) PruneContainers() error {
_, err := c.Client.ContainersPrune(context.Background(), filters.Args{})
return err
}
// Inspect returns details about the container
func (c *Container) Inspect() (types.ContainerJSON, error) {
return c.Client.ContainerInspect(context.Background(), c.ID)
}
// RenderTop returns details about the container
func (c *Container) RenderTop() (string, error) {
result, err := c.Client.ContainerTop(context.Background(), c.ID, []string{})
if err != nil {
return "", err
}
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
func (c *Container) DetailsLoaded() bool {
return c.Details.Image != ""
}