Compare commits

..

6 Commits
v0.7.6 ... k8s

Author SHA1 Message Date
Bradley Cicenas
ae62c388bc move flags to FlagSet 2018-12-02 12:50:49 +00:00
Bradley Cicenas
523d6c7277 update for latest k8s libs 2018-12-02 12:27:47 +00:00
Bradley Cicenas
47a0d7e495 fix namespace flag help text 2018-12-02 00:46:39 +00:00
Alexandr Kozlenkov
9dec6b4c67 Added loading CPU and Mem for containers in pod 2018-12-01 19:44:34 -05:00
Alexandr Kozlenkov
7f6ff0b599 Added draft for collector metrics of k8s 2018-12-01 19:44:01 -05:00
Alexandr Kozlenkov
187adf0540 A draft of connector to kubernetse 2018-12-01 19:43:41 -05:00
67 changed files with 1255 additions and 2220 deletions

View File

@@ -7,7 +7,7 @@ jobs:
steps: steps:
- checkout - checkout
- setup_remote_docker: - setup_remote_docker:
version: 19.03.13 version: 17.05.0-ce
- run: make image - run: make image
- deploy: - deploy:
command: | command: |

3
.gitignore vendored
View File

@@ -1,4 +1,3 @@
ctop ctop
.idea .idea
/vendor/ /vendor/
*.log

View File

@@ -1,4 +1,4 @@
FROM quay.io/vektorcloud/go:1.15 FROM quay.io/vektorcloud/go:1.11
RUN apk add --no-cache make RUN apk add --no-cache make

View File

@@ -1,7 +1,8 @@
NAME=ctop NAME=ctop
VERSION=$(shell cat VERSION) VERSION=$(shell cat VERSION)
BUILD=$(shell git rev-parse --short HEAD) BUILD=$(shell git rev-parse --short HEAD)
LD_FLAGS="-w -X main.version=$(VERSION) -X main.build=$(BUILD)" EXT_LD_FLAGS="-Wl,--allow-multiple-definition"
LD_FLAGS="-w -X main.version=$(VERSION) -X main.build=$(BUILD) -extldflags=$(EXT_LD_FLAGS)"
clean: clean:
rm -rf _build/ release/ rm -rf _build/ release/
@@ -10,27 +11,27 @@ build:
go mod download go mod download
CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o ctop CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o ctop
build-dev:
go build -ldflags "-w -X main.version=$(VERSION)-dev -X main.build=$(BUILD) -extldflags=$(EXT_LD_FLAGS)"
build-all: build-all:
mkdir -p _build mkdir -p _build
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-darwin-amd64 GOOS=darwin GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-darwin-amd64
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-amd64 GOOS=linux GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-amd64
GOOS=linux GOARCH=arm CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm GOOS=linux GOARCH=arm go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm64 GOOS=linux GOARCH=arm64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm64
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-windows-amd64 GOOS=windows GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-windows-amd64
cd _build; sha256sum * > sha256sums.txt cd _build; sha256sum * > sha256sums.txt
run-dev:
rm -f ctop.sock ctop
go build -ldflags $(LD_FLAGS) -o ctop
CTOP_DEBUG=1 ./ctop
image: image:
docker build -t ctop -f Dockerfile . docker build -t ctop -f Dockerfile .
release: release:
mkdir release mkdir release
go get github.com/progrium/gh-release/...
cp _build/* release cp _build/* release
cd release; sha256sum --quiet --check sha256sums.txt && \ cd release; sha256sum --quiet --check sha256sums.txt
gh release create $(VERSION) -d -t v$(VERSION) * gh-release create bcicen/$(NAME) $(VERSION) \
$(shell git rev-parse --abbrev-ref HEAD) $(VERSION)
.PHONY: build .PHONY: build

View File

@@ -2,14 +2,14 @@
# #
![release][release] ![homebrew][homebrew] ![macports][macports] ![release][release] ![homebrew][homebrew]
Top-like interface for container metrics Top-like interface for container metrics
`ctop` provides a concise and condensed overview of real-time metrics for multiple containers: `ctop` provides a concise and condensed overview of real-time metrics for multiple containers:
<p align="center"><img src="_docs/img/grid.gif" alt="ctop"/></p> <p align="center"><img src="_docs/img/grid.gif" alt="ctop"/></p>
as well as a [single container view][single_view] for inspecting a specific container. as well as an [single container view][single_view] for inspecting a specific container.
`ctop` comes with built-in support for Docker and runC; connectors for other container and cluster systems are planned for future releases. `ctop` comes with built-in support for Docker and runC; connectors for other container and cluster systems are planned for future releases.
@@ -17,24 +17,10 @@ as well as a [single container view][single_view] for inspecting a specific cont
Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your platform: Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your platform:
#### Debian/Ubuntu #### Linux
Maintained by a [third party](https://packages.azlux.fr/)
```bash
echo "deb http://packages.azlux.fr/debian/ buster main" | sudo tee /etc/apt/sources.list.d/azlux.list
wget -qO - https://azlux.fr/repo.gpg.key | sudo apt-key add -
sudo apt update
sudo apt install docker-ctop
```
#### Arch
`ctop` is available for Arch in the [AUR](https://aur.archlinux.org/packages/ctop-bin/)
#### Linux (Generic)
```bash ```bash
sudo wget https://github.com/bcicen/ctop/releases/download/v0.7.6/ctop-0.7.6-linux-amd64 -O /usr/local/bin/ctop sudo wget https://github.com/bcicen/ctop/releases/download/v0.7.1/ctop-0.7.1-linux-amd64 -O /usr/local/bin/ctop
sudo chmod +x /usr/local/bin/ctop sudo chmod +x /usr/local/bin/ctop
``` ```
@@ -45,11 +31,7 @@ brew install ctop
``` ```
or or
```bash ```bash
sudo port install ctop sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.7.1/ctop-0.7.1-darwin-amd64
```
or
```bash
sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.7.6/ctop-0.7.6-darwin-amd64
sudo chmod +x /usr/local/bin/ctop sudo chmod +x /usr/local/bin/ctop
``` ```
@@ -58,10 +40,12 @@ sudo chmod +x /usr/local/bin/ctop
```bash ```bash
docker run --rm -ti \ docker run --rm -ti \
--name=ctop \ --name=ctop \
--volume /var/run/docker.sock:/var/run/docker.sock:ro \ -v /var/run/docker.sock:/var/run/docker.sock \
quay.io/vektorlab/ctop:latest quay.io/vektorlab/ctop:latest
``` ```
`ctop` is also available for Arch in the [AUR](https://aur.archlinux.org/packages/ctop-bin/)
## Building ## Building
Build steps can be found [here][build]. Build steps can be found [here][build].
@@ -72,47 +56,39 @@ Build steps can be found [here][build].
### Config file ### Config file
While running, use `S` to save the current filters, sort field, and other options to a default config path (`~/.config/ctop/config` on XDG systems, else `~/.ctop`). While running, use `S` to save the current filters, sort field, and other options to a default config path. These settings will be loaded and applied the next time `ctop` is started.
Config file values will be loaded and applied the next time `ctop` is started.
### Options ### Options
Option | Description Option | Description
--- | --- --- | ---
`-a` | show active containers only -a | show active containers only
`-f <string>` | set an initial filter string -f \<string\> | set an initial filter string
`-h` | display help dialog -h | display help dialog
`-i` | invert default colors -i | invert default colors
`-r` | reverse container sort order -r | reverse container sort order
`-s` | select initial container sort field -s | select initial container sort field
`-v` | output version information and exit -scale-cpu | show cpu as % of system total
-v | output version information and exit
### Keybindings ### Keybindings
| Key | Action | Key | Action
| :----------------------: | ---------------------------------------------------------- | --- | ---
| <kbd>&lt;ENTER&gt;</kbd> | Open container menu | \<enter\> | Open container menu
| <kbd>a</kbd> | Toggle display of all (running and non-running) containers | a | Toggle display of all (running and non-running) containers
| <kbd>f</kbd> | Filter displayed containers (`esc` to clear when open) | f | Filter displayed containers (`esc` to clear when open)
| <kbd>H</kbd> | Toggle ctop header | H | Toggle ctop header
| <kbd>h</kbd> | Open help dialog | h | Open help dialog
| <kbd>s</kbd> | Select container sort field | s | Select container sort field
| <kbd>r</kbd> | Reverse container sort order | r | Reverse container sort order
| <kbd>o</kbd> | Open single view | o | Open single view
| <kbd>l</kbd> | View container logs (`t` to toggle timestamp when open) | l | View container logs (`t` to toggle timestamp when open)
| <kbd>e</kbd> | Exec Shell | S | Save current configuration to file
| <kbd>c</kbd> | Configure columns | q | Quit ctop
| <kbd>S</kbd> | Save current configuration to file |
| <kbd>q</kbd> | Quit ctop |
[build]: _docs/build.md [build]: _docs/build.md
[connectors]: _docs/connectors.md [connectors]: _docs/connectors.md
[single_view]: _docs/single.md [single_view]: _docs/single.md
[release]: https://img.shields.io/github/release/bcicen/ctop.svg "ctop" [release]: https://img.shields.io/github/release/bcicen/ctop.svg "ctop"
[homebrew]: https://img.shields.io/homebrew/v/ctop.svg "ctop" [homebrew]: https://img.shields.io/homebrew/v/ctop.svg "ctop"
[macports]: https://repology.org/badge/version-for-repo/macports/ctop.svg?header=macports "ctop"
## Alternatives
See [Awesome Docker list](https://github.com/veggiemonk/awesome-docker/blob/master/README.md#terminal) for similar tools to work with Docker.

View File

@@ -1 +1 @@
0.7.6 0.7.1

View File

@@ -1,8 +1,10 @@
# Build # Build
To build `ctop` from source, simply clone the repo and run: To build `ctop` from source, ensure you have [dep](https://github.com/golang/dep) installed and run:
```bash ```bash
go get github.com/bcicen/ctop && \
cd $GOPATH/src/github.com/bcicen/ctop && \
make build make build
``` ```
@@ -14,8 +16,5 @@ make image
Now you can run your local image: Now you can run your local image:
```bash ```bash
docker run --rm -ti \ docker run -ti --name ctop --rm -v /var/run/docker.sock:/var/run/docker.sock ctop
--name ctop \
-v /var/run/docker.sock:/var/run/docker.sock \
ctop:latest
``` ```

View File

@@ -1,4 +1,4 @@
# Connectors # connectors
`ctop` comes with the below native connectors, enabled via the `--connector` option. `ctop` comes with the below native connectors, enabled via the `--connector` option.

View File

@@ -54,18 +54,3 @@ CTOP_DEBUG=1 CTOP_DEBUG_TCP=1 ./ctop
``` ```
A TCP listener for streaming log messages will be started on the default listen address(`0.0.0.0:9000`) A TCP listener for streaming log messages will be started on the default listen address(`0.0.0.0:9000`)
## Log to file
You can also log to a file by specifying `CTOP_DEBUG_FILE=/path/to/ctop.log` environment variable:
```sh
CTOP_DEBUG=1 CTOP_DEBUG_FILE=ctop.log ./ctop
```
This is useful for GoLand to see logs right in debug panel:
* Edit Run configuration
* Go to Logs tab
* Specify this log file in "Log file to be shown in console".
Then during debugging you'll see the log tab in debug panel:
![Debug in GoLand](img/goland_debug.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -1,30 +0,0 @@
# Status Indicator
The `ctop` grid view provides a compact status indicator to convey container state
<img width="200px" src="img/status.png" alt="ctop"/>
### Status
<span align="center">
Appearance | Description
--- | ---
red | container is stopped
green | container is running
▮▮ | container is paused
</span>
### Health
If the container is configured with a health check, a `+` will appear next to the indicator
<span align="center">
Appearance | Description
--- | ---
red | health check in failed state
yellow | health check in starting state
green | health check in OK state
</span>

View File

@@ -1,170 +0,0 @@
package config
import (
"strings"
)
// defaults
var defaultColumns = []Column{
{
Name: "status",
Label: "Status Indicator",
Enabled: true,
},
{
Name: "name",
Label: "Container Name",
Enabled: true,
},
{
Name: "id",
Label: "Container ID",
Enabled: true,
},
{
Name: "image",
Label: "Image name",
Enabled: false,
},
{
Name: "ports",
Label: "Exposed ports",
Enabled: false,
},
{
Name: "IPs",
Label: "Exposed IPs",
Enabled: false,
},
{
Name: "created",
Label: "Date created",
Enabled: false,
},
{
Name: "cpu",
Label: "CPU Usage",
Enabled: true,
},
{
Name: "cpus",
Label: "CPU Usage (% of system total)",
Enabled: false,
},
{
Name: "mem",
Label: "Memory Usage",
Enabled: true,
},
{
Name: "net",
Label: "Network RX/TX",
Enabled: true,
},
{
Name: "io",
Label: "Disk IO Read/Write",
Enabled: true,
},
{
Name: "pids",
Label: "Container PID Count",
Enabled: true,
},
}
type Column struct {
Name string
Label string
Enabled bool
}
// ColumnsString returns an ordered and comma-delimited string of currently enabled Columns
func ColumnsString() string { return strings.Join(EnabledColumns(), ",") }
// EnabledColumns returns an ordered array of enabled column names
func EnabledColumns() (a []string) {
lock.RLock()
defer lock.RUnlock()
for _, col := range GlobalColumns {
if col.Enabled {
a = append(a, col.Name)
}
}
return a
}
// ColumnToggle toggles the enabled status of a given column name
func ColumnToggle(name string) {
col := GlobalColumns[colIndex(name)]
col.Enabled = !col.Enabled
log.Noticef("config change [column-%s]: %t -> %t", col.Name, !col.Enabled, col.Enabled)
}
// ColumnLeft moves the column with given name up one position, if possible
func ColumnLeft(name string) {
idx := colIndex(name)
if idx > 0 {
swapCols(idx, idx-1)
}
}
// ColumnRight moves the column with given name up one position, if possible
func ColumnRight(name string) {
idx := colIndex(name)
if idx < len(GlobalColumns)-1 {
swapCols(idx, idx+1)
}
}
// Set Column order and enabled status from one or more provided Column names
func SetColumns(names []string) {
var (
n int
curColStr = ColumnsString()
newColumns = make([]*Column, len(GlobalColumns))
)
lock.Lock()
// add enabled columns by name
for _, name := range names {
newColumns[n] = popColumn(name)
newColumns[n].Enabled = true
n++
}
// extend with omitted columns as disabled
for _, col := range GlobalColumns {
newColumns[n] = col
newColumns[n].Enabled = false
n++
}
GlobalColumns = newColumns
lock.Unlock()
log.Noticef("config change [columns]: %s -> %s", curColStr, ColumnsString())
}
func swapCols(i, j int) { GlobalColumns[i], GlobalColumns[j] = GlobalColumns[j], GlobalColumns[i] }
func popColumn(name string) *Column {
idx := colIndex(name)
if idx < 0 {
panic("no such column name: " + name)
}
col := GlobalColumns[idx]
GlobalColumns = append(GlobalColumns[:idx], GlobalColumns[idx+1:]...)
return col
}
// return index of column with given name, if any
func colIndex(name string) int {
for n, c := range GlobalColumns {
if c.Name == name {
return n
}
}
return -1
}

View File

@@ -3,7 +3,6 @@ package config
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"regexp" "regexp"
"strings" "strings"
@@ -20,28 +19,19 @@ type File struct {
} }
func exportConfig() File { func exportConfig() File {
// update columns param from working config
Update("columns", ColumnsString())
lock.RLock()
defer lock.RUnlock()
c := File{ c := File{
Options: make(map[string]string), Options: make(map[string]string),
Toggles: make(map[string]bool), Toggles: make(map[string]bool),
} }
for _, p := range GlobalParams { for _, p := range GlobalParams {
c.Options[p.Key] = p.Val c.Options[p.Key] = p.Val
} }
for _, sw := range GlobalSwitches { for _, sw := range GlobalSwitches {
c.Toggles[sw.Key] = sw.Val c.Toggles[sw.Key] = sw.Val
} }
return c return c
} }
//
func Read() error { func Read() error {
var config File var config File
@@ -53,26 +43,13 @@ func Read() error {
if _, err := toml.DecodeFile(path, &config); err != nil { if _, err := toml.DecodeFile(path, &config); err != nil {
return err return err
} }
for k, v := range config.Options { for k, v := range config.Options {
Update(k, v) Update(k, v)
} }
for k, v := range config.Toggles { for k, v := range config.Toggles {
UpdateSwitch(k, v) UpdateSwitch(k, v)
} }
// set working column config, if provided
colStr := GetVal("columns")
if len(colStr) > 0 {
var colNames []string
for _, s := range strings.Split(colStr, ",") {
s = strings.TrimSpace(s)
if s != "" {
colNames = append(colNames, s)
}
}
SetColumns(colNames)
}
return nil return nil
} }
@@ -82,7 +59,7 @@ func Write() (path string, err error) {
return path, err return path, err
} }
cfgdir := filepath.Dir(path) cfgdir := basedir(path)
// create config dir if not exist // create config dir if not exist
if _, err := os.Stat(cfgdir); err != nil { if _, err := os.Stat(cfgdir); err != nil {
err = os.MkdirAll(cfgdir, 0755) err = os.MkdirAll(cfgdir, 0755)
@@ -91,13 +68,6 @@ func Write() (path string, err error) {
} }
} }
// remove prior to writing new file
if err := os.Remove(path); err != nil {
if !os.IsNotExist(err) {
return path, err
}
}
file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644) file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
if err != nil { if err != nil {
return path, fmt.Errorf("failed to open config for writing: %s", err) return path, fmt.Errorf("failed to open config for writing: %s", err)
@@ -142,3 +112,8 @@ func xdgSupport() bool {
} }
return false return false
} }
func basedir(path string) string {
parts := strings.Split(path, "/")
return strings.Join((parts[0 : len(parts)-1]), "/")
}

View File

@@ -3,7 +3,6 @@ package config
import ( import (
"fmt" "fmt"
"os" "os"
"sync"
"github.com/bcicen/ctop/logging" "github.com/bcicen/ctop/logging"
) )
@@ -11,24 +10,17 @@ import (
var ( var (
GlobalParams []*Param GlobalParams []*Param
GlobalSwitches []*Switch GlobalSwitches []*Switch
GlobalColumns []*Column
lock sync.RWMutex
log = logging.Init() log = logging.Init()
) )
func Init() { func Init() {
for _, p := range defaultParams { for _, p := range params {
GlobalParams = append(GlobalParams, p) GlobalParams = append(GlobalParams, p)
log.Infof("loaded default config param [%s]: %s", quote(p.Key), quote(p.Val)) log.Infof("loaded config param: %s: %s", quote(p.Key), quote(p.Val))
} }
for _, s := range defaultSwitches { for _, s := range switches {
GlobalSwitches = append(GlobalSwitches, s) GlobalSwitches = append(GlobalSwitches, s)
log.Infof("loaded default config switch [%s]: %t", quote(s.Key), s.Val) log.Infof("loaded config switch: %s: %t", quote(s.Key), s.Val)
}
for _, c := range defaultColumns {
x := c
GlobalColumns = append(GlobalColumns, &x)
log.Infof("loaded default widget config [%s]: %t", quote(x.Name), x.Enabled)
} }
} }

View File

@@ -1,7 +1,7 @@
package config package config
// defaults // defaults
var defaultParams = []*Param{ var params = []*Param{
&Param{ &Param{
Key: "filterStr", Key: "filterStr",
Val: "", Val: "",
@@ -13,9 +13,9 @@ var defaultParams = []*Param{
Label: "Container Sort Field", Label: "Container Sort Field",
}, },
&Param{ &Param{
Key: "columns", Key: "namespace",
Val: "status,name,id,cpu,mem,net,io,pids", Val: "state",
Label: "Enabled Columns", Label: "Kubernetes namespace for monitoring",
}, },
} }
@@ -27,9 +27,6 @@ type Param struct {
// Get Param by key // Get Param by key
func Get(k string) *Param { func Get(k string) *Param {
lock.RLock()
defer lock.RUnlock()
for _, p := range GlobalParams { for _, p := range GlobalParams {
if p.Key == k { if p.Key == k {
return p return p
@@ -38,7 +35,7 @@ func Get(k string) *Param {
return &Param{} // default return &Param{} // default
} }
// GetVal gets Param value by key // Get Param value by key
func GetVal(k string) string { func GetVal(k string) string {
return Get(k).Val return Get(k).Val
} }
@@ -46,10 +43,7 @@ func GetVal(k string) string {
// Set param value // Set param value
func Update(k, v string) { func Update(k, v string) {
p := Get(k) p := Get(k)
log.Noticef("config change [%s]: %s -> %s", k, quote(p.Val), quote(v)) log.Noticef("config change: %s: %s -> %s", k, quote(p.Val), quote(v))
lock.Lock()
defer lock.Unlock()
p.Val = v p.Val = v
// log.Errorf("ignoring update for non-existant parameter: %s", k) // log.Errorf("ignoring update for non-existant parameter: %s", k)
} }

View File

@@ -1,7 +1,7 @@
package config package config
// defaults // defaults
var defaultSwitches = []*Switch{ var switches = []*Switch{
&Switch{ &Switch{
Key: "sortReversed", Key: "sortReversed",
Val: false, Val: false,
@@ -22,6 +22,11 @@ var defaultSwitches = []*Switch{
Val: true, Val: true,
Label: "Enable status header", Label: "Enable status header",
}, },
&Switch{
Key: "scaleCpu",
Val: false,
Label: "Show CPU as %% of system total",
},
} }
type Switch struct { type Switch struct {
@@ -30,11 +35,8 @@ type Switch struct {
Label string Label string
} }
// GetSwitch returns Switch by key // Return Switch by key
func GetSwitch(k string) *Switch { func GetSwitch(k string) *Switch {
lock.RLock()
defer lock.RUnlock()
for _, sw := range GlobalSwitches { for _, sw := range GlobalSwitches {
if sw.Key == k { if sw.Key == k {
return sw return sw
@@ -43,19 +45,15 @@ func GetSwitch(k string) *Switch {
return &Switch{} // default return &Switch{} // default
} }
// GetSwitchVal returns Switch value by key // Return Switch value by key
func GetSwitchVal(k string) bool { func GetSwitchVal(k string) bool {
return GetSwitch(k).Val return GetSwitch(k).Val
} }
func UpdateSwitch(k string, val bool) { func UpdateSwitch(k string, val bool) {
sw := GetSwitch(k) sw := GetSwitch(k)
lock.Lock()
defer lock.Unlock()
if sw.Val != val { if sw.Val != val {
log.Noticef("config change [%s]: %t -> %t", k, sw.Val, val) log.Noticef("config change: %s: %t -> %t", k, sw.Val, val)
sw.Val = val sw.Val = val
} }
} }
@@ -63,11 +61,8 @@ func UpdateSwitch(k string, val bool) {
// Toggle a boolean switch // Toggle a boolean switch
func Toggle(k string) { func Toggle(k string) {
sw := GetSwitch(k) sw := GetSwitch(k)
newVal := !sw.Val
lock.Lock() log.Noticef("config change: %s: %t -> %t", k, sw.Val, newVal)
defer lock.Unlock() sw.Val = newVal
sw.Val = !sw.Val
log.Noticef("config change [%s]: %t -> %t", k, !sw.Val, sw.Val)
//log.Errorf("ignoring toggle for non-existant switch: %s", k) //log.Errorf("ignoring toggle for non-existant switch: %s", k)
} }

View File

@@ -1,6 +1,7 @@
package collector package collector
import ( import (
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/models" "github.com/bcicen/ctop/models"
api "github.com/fsouza/go-dockerclient" api "github.com/fsouza/go-dockerclient"
) )
@@ -15,13 +16,15 @@ type Docker struct {
done chan bool done chan bool
lastCpu float64 lastCpu float64
lastSysCpu float64 lastSysCpu float64
scaleCpu bool
} }
func NewDocker(client *api.Client, id string) *Docker { func NewDocker(client *api.Client, id string) *Docker {
return &Docker{ return &Docker{
Metrics: models.Metrics{}, Metrics: models.Metrics{},
id: id, id: id,
client: client, client: client,
scaleCpu: config.GetSwitchVal("scaleCpu"),
} }
} }
@@ -71,20 +74,22 @@ func (c *Docker) Logs() LogCollector {
// Stop collector // Stop collector
func (c *Docker) Stop() { func (c *Docker) Stop() {
c.running = false
c.done <- true c.done <- true
} }
func (c *Docker) ReadCPU(stats *api.Stats) { func (c *Docker) ReadCPU(stats *api.Stats) {
ncpus := uint8(len(stats.CPUStats.CPUUsage.PercpuUsage)) ncpus := float64(len(stats.CPUStats.CPUUsage.PercpuUsage))
total := float64(stats.CPUStats.CPUUsage.TotalUsage) total := float64(stats.CPUStats.CPUUsage.TotalUsage)
system := float64(stats.CPUStats.SystemCPUUsage) system := float64(stats.CPUStats.SystemCPUUsage)
cpudiff := total - c.lastCpu cpudiff := total - c.lastCpu
syscpudiff := system - c.lastSysCpu syscpudiff := system - c.lastSysCpu
c.NCpus = ncpus if c.scaleCpu {
c.CPUUtil = percent(cpudiff, syscpudiff) c.CPUUtil = round((cpudiff / syscpudiff * 100))
} else {
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
}
c.lastCpu = total c.lastCpu = total
c.lastSysCpu = system c.lastSysCpu = system
c.Pids = int(stats.PidsStats.Current) c.Pids = int(stats.PidsStats.Current)
@@ -109,10 +114,10 @@ func (c *Docker) ReadIO(stats *api.Stats) {
var read, write int64 var read, write int64
for _, blk := range stats.BlkioStats.IOServiceBytesRecursive { for _, blk := range stats.BlkioStats.IOServiceBytesRecursive {
if blk.Op == "Read" { if blk.Op == "Read" {
read += int64(blk.Value) read = int64(blk.Value)
} }
if blk.Op == "Write" { if blk.Op == "Write" {
write += int64(blk.Value) write = int64(blk.Value)
} }
} }
c.IOBytesRead, c.IOBytesWrite = read, write c.IOBytesRead, c.IOBytesWrite = read, write

View File

@@ -34,22 +34,21 @@ func (l *DockerLogs) Stream() chan models.Log {
Context: ctx, Context: ctx,
Container: l.id, Container: l.id,
OutputStream: w, OutputStream: w,
//ErrorStream: w, ErrorStream: w,
Stdout: true, Stdout: true,
Stderr: true, Stderr: true,
Tail: "20", Tail: "10",
Follow: true, Follow: true,
Timestamps: true, Timestamps: true,
RawTerminal: true,
} }
// read io pipe into channel // read io pipe into channel
go func() { go func() {
scanner := bufio.NewScanner(r) scanner := bufio.NewScanner(r)
for scanner.Scan() { for scanner.Scan() {
parts := strings.SplitN(scanner.Text(), " ", 2) parts := strings.Split(scanner.Text(), " ")
ts := l.parseTime(parts[0]) ts := l.parseTime(parts[0])
logCh <- models.Log{Timestamp: ts, Message: parts[1]} logCh <- models.Log{Timestamp: ts, Message: strings.Join(parts[1:], " ")}
} }
}() }()
@@ -74,26 +73,10 @@ func (l *DockerLogs) Stream() chan models.Log {
func (l *DockerLogs) Stop() { l.done <- true } func (l *DockerLogs) Stop() { l.done <- true }
func (l *DockerLogs) parseTime(s string) time.Time { func (l *DockerLogs) parseTime(s string) time.Time {
ts, err := time.Parse(time.RFC3339Nano, s) ts, err := time.Parse("2006-01-02T15:04:05.000000000Z", s)
if err == nil { if err != nil {
return ts log.Errorf("failed to parse container log: %s", err)
ts = time.Now()
} }
return ts
ts, err2 := time.Parse(time.RFC3339Nano, l.stripPfx(s))
if err2 == nil {
return ts
}
log.Errorf("failed to parse container log: %s", err)
log.Errorf("failed to parse container log2: %s", err2)
return time.Now()
}
// attempt to strip message header prefix from a given raw docker log string
func (l *DockerLogs) stripPfx(s string) string {
b := []byte(s)
if len(b) > 8 {
return string(b[8:])
}
return s
} }

View File

@@ -0,0 +1,128 @@
package collector
import (
"time"
"k8s.io/metrics/pkg/apis/metrics/v1alpha1"
clientset "k8s.io/metrics/pkg/client/clientset/versioned"
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/models"
"k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
)
// Kubernetes collector
type Kubernetes struct {
models.Metrics
name string
client clientset.Interface
clientset *kubernetes.Clientset
running bool
stream chan models.Metrics
done chan bool
lastCpu float64
lastSysCpu float64
scaleCpu bool
}
func NewKubernetes(client *kubernetes.Clientset, name string) *Kubernetes {
return &Kubernetes{
Metrics: models.Metrics{},
name: name,
client: clientset.New(client.RESTClient()),
clientset: client,
scaleCpu: config.GetSwitchVal("scaleCpu"),
}
}
func (k *Kubernetes) Start() {
k.done = make(chan bool)
k.stream = make(chan models.Metrics)
go func() {
k.running = false
for {
result := &v1alpha1.PodMetrics{}
err := k.clientset.RESTClient().Get().AbsPath("/api/v1/namespaces/kube-system/services/http:heapster:/proxy/apis/metrics/v1alpha1/namespaces/" + config.GetVal("namespace") + "/pods/" + k.name).Do().Into(result)
if err != nil {
log.Errorf("has error %s here %s", k.name, err.Error())
time.Sleep(1 * time.Second)
continue
}
k.ReadCPU(result)
k.ReadMem(result)
k.stream <- k.Metrics
}
}()
k.running = true
log.Infof("collector started for container: %s", k.name)
}
func (c *Kubernetes) Running() bool {
return c.running
}
func (c *Kubernetes) Stream() chan models.Metrics {
return c.stream
}
func (c *Kubernetes) Logs() LogCollector {
return NewKubernetesLogs(c.name, c.clientset)
}
// Stop collector
func (c *Kubernetes) Stop() {
c.done <- true
}
func (k *Kubernetes) ReadCPU(metrics *v1alpha1.PodMetrics) {
all := int64(0)
for _, c := range metrics.Containers {
v := c.Usage[v1.ResourceCPU]
all += v.Value()
}
if all != 0 {
k.CPUUtil = round(float64(all))
}
}
func (k *Kubernetes) ReadMem(metrics *v1alpha1.PodMetrics) {
all := int64(0)
for _, c := range metrics.Containers {
v := c.Usage[v1.ResourceMemory]
a, ok := v.AsInt64()
if ok {
all += a
}
}
k.MemUsage = all
k.MemLimit = int64(0)
//k.MemPercent = percent(float64(k.MemUsage), float64(k.MemLimit))
}
//func (c *Kubernetes) ReadNet(stats *api.Stats) {
// var rx, tx int64
// for _, network := range stats.Networks {
// rx += int64(network.RxBytes)
// tx += int64(network.TxBytes)
// }
// c.NetRx, c.NetTx = rx, tx
//}
//
//func (c *Kubernetes) ReadIO(stats *api.Stats) {
// var read, write int64
// for _, blk := range stats.BlkioStats.IOServiceBytesRecursive {
// if blk.Op == "Read" {
// read = int64(blk.Value)
// }
// if blk.Op == "Write" {
// write = int64(blk.Value)
// }
// }
// c.IOBytesRead, c.IOBytesWrite = read, write
//}

View File

@@ -0,0 +1,78 @@
package collector
import (
"time"
"github.com/bcicen/ctop/models"
"k8s.io/client-go/kubernetes"
)
type KubernetesLogs struct {
id string
client *kubernetes.Clientset
done chan bool
}
func NewKubernetesLogs(id string, client *kubernetes.Clientset) *KubernetesLogs {
return &KubernetesLogs{
id: id,
client: client,
done: make(chan bool),
}
}
func (l *KubernetesLogs) Stream() chan models.Log {
//r, w := io.Pipe()
logCh := make(chan models.Log)
//ctx, cancel := context.WithCancel(context.Background())
//opts := api.LogsOptions{
// Context: ctx,
// Container: l.id,
// OutputStream: w,
// ErrorStream: w,
// Stdout: true,
// Stderr: true,
// Tail: "10",
// Follow: true,
// Timestamps: true,
//}
//// read io pipe into channel
//go func() {
// scanner := bufio.NewScanner(r)
// for scanner.Scan() {
// parts := strings.Split(scanner.Text(), " ")
// ts := l.parseTime(parts[0])
// logCh <- models.Log{Timestamp: ts, Message: strings.Join(parts[1:], " ")}
// }
//}()
//// connect to container log stream
//go func() {
// err := l.client.Logs(opts)
// if err != nil {
// log.Errorf("error reading container logs: %s", err)
// }
// log.Infof("log reader stopped for container: %s", l.id)
//}()
//go func() {
// <-l.done
// cancel()
//}()
log.Infof("log reader started for container: %s", l.id)
return logCh
}
func (l *KubernetesLogs) Stop() { l.done <- true }
func (l *KubernetesLogs) parseTime(s string) time.Time {
ts, err := time.Parse("2006-01-02T15:04:05.000000000Z", s)
if err != nil {
log.Errorf("failed to parse container log: %s", err)
ts = time.Now()
}
return ts
}

View File

@@ -38,7 +38,6 @@ func (c *Mock) Start() {
} }
func (c *Mock) Stop() { func (c *Mock) Stop() {
c.running = false
c.done = true c.done = true
} }

View File

@@ -4,14 +4,13 @@ package collector
import ( import (
linuxproc "github.com/c9s/goprocinfo/linux" linuxproc "github.com/c9s/goprocinfo/linux"
"github.com/opencontainers/runc/libcontainer/system"
) )
var sysMemTotal = getSysMemTotal() var sysMemTotal = getSysMemTotal()
var clockTicksPerSecond = uint64(system.GetClockTicks())
const ( const nanoSecondsPerSecond = 1e9
clockTicksPerSecond uint64 = 100
nanoSecondsPerSecond = 1e9
)
func getSysMemTotal() int64 { func getSysMemTotal() int64 {
stat, err := linuxproc.ReadMemInfo("/proc/meminfo") stat, err := linuxproc.ReadMemInfo("/proc/meminfo")

View File

@@ -5,11 +5,10 @@ package collector
import ( import (
"time" "time"
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/models"
"github.com/opencontainers/runc/libcontainer" "github.com/opencontainers/runc/libcontainer"
"github.com/opencontainers/runc/libcontainer/cgroups" "github.com/opencontainers/runc/libcontainer/cgroups"
"github.com/opencontainers/runc/types"
"github.com/bcicen/ctop/models"
) )
// Runc collector // Runc collector
@@ -23,6 +22,7 @@ type Runc struct {
interval int // collection interval, in seconds interval int // collection interval, in seconds
lastCpu float64 lastCpu float64
lastSysCpu float64 lastSysCpu float64
scaleCpu bool
} }
func NewRunc(libc libcontainer.Container) *Runc { func NewRunc(libc libcontainer.Container) *Runc {
@@ -31,6 +31,7 @@ func NewRunc(libc libcontainer.Container) *Runc {
id: libc.ID(), id: libc.ID(),
libc: libc, libc: libc,
interval: 1, interval: 1,
scaleCpu: config.GetSwitchVal("scaleCpu"),
} }
return c return c
} }
@@ -46,7 +47,6 @@ func (c *Runc) Start() {
} }
func (c *Runc) Stop() { func (c *Runc) Stop() {
c.running = false
c.done = true c.done = true
} }
@@ -86,15 +86,18 @@ func (c *Runc) run() {
func (c *Runc) ReadCPU(stats *cgroups.Stats) { func (c *Runc) ReadCPU(stats *cgroups.Stats) {
u := stats.CpuStats.CpuUsage u := stats.CpuStats.CpuUsage
ncpus := uint8(len(u.PercpuUsage)) ncpus := float64(len(u.PercpuUsage))
total := float64(u.TotalUsage) total := float64(u.TotalUsage)
system := float64(getSysCPUUsage()) system := float64(getSysCPUUsage())
cpudiff := total - c.lastCpu cpudiff := total - c.lastCpu
syscpudiff := system - c.lastSysCpu syscpudiff := system - c.lastSysCpu
c.NCpus = ncpus if c.scaleCpu {
c.CPUUtil = percent(cpudiff, syscpudiff) c.CPUUtil = round((cpudiff / syscpudiff * 100))
} else {
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
}
c.lastCpu = total c.lastCpu = total
c.lastSysCpu = system c.lastSysCpu = system
c.Pids = int(stats.PidsStats.Current) c.Pids = int(stats.PidsStats.Current)
@@ -109,7 +112,7 @@ func (c *Runc) ReadMem(stats *cgroups.Stats) {
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit)) c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
} }
func (c *Runc) ReadNet(interfaces []*types.NetworkInterface) { func (c *Runc) ReadNet(interfaces []*libcontainer.NetworkInterface) {
var rx, tx int64 var rx, tx int64
for _, network := range interfaces { for _, network := range interfaces {
rx += int64(network.RxBytes) rx += int64(network.RxBytes)

View File

@@ -2,7 +2,6 @@ package connector
import ( import (
"fmt" "fmt"
"github.com/op/go-logging"
"strings" "strings"
"sync" "sync"
@@ -14,66 +13,31 @@ import (
func init() { enabled["docker"] = NewDocker } func init() { enabled["docker"] = NewDocker }
var actionToStatus = map[string]string{
"start": "running",
"die": "exited",
"stop": "exited",
"pause": "paused",
"unpause": "running",
}
type StatusUpdate struct {
Cid string
Field string // "status" or "health"
Status string
}
type Docker struct { type Docker struct {
client *api.Client client *api.Client
containers map[string]*container.Container containers map[string]*container.Container
needsRefresh chan string // container IDs requiring refresh needsRefresh chan string // container IDs requiring refresh
statuses chan StatusUpdate
closed chan struct{}
lock sync.RWMutex lock sync.RWMutex
} }
func NewDocker() (Connector, error) { func NewDocker() Connector {
// init docker client // init docker client
client, err := api.NewClientFromEnv() client, err := api.NewClientFromEnv()
if err != nil { if err != nil {
return nil, err panic(err)
} }
cm := &Docker{ cm := &Docker{
client: client, client: client,
containers: make(map[string]*container.Container), containers: make(map[string]*container.Container),
needsRefresh: make(chan string, 60), needsRefresh: make(chan string, 60),
statuses: make(chan StatusUpdate, 60),
closed: make(chan struct{}),
lock: sync.RWMutex{}, lock: sync.RWMutex{},
} }
// query info as pre-flight healthcheck
info, err := client.Info()
if err != nil {
return nil, err
}
log.Debugf("docker-connector ID: %s", info.ID)
log.Debugf("docker-connector Driver: %s", info.Driver)
log.Debugf("docker-connector Images: %d", info.Images)
log.Debugf("docker-connector Name: %s", info.Name)
log.Debugf("docker-connector ServerVersion: %s", info.ServerVersion)
go cm.Loop() go cm.Loop()
go cm.LoopStatuses()
cm.refreshAll() cm.refreshAll()
go cm.watchEvents() go cm.watchEvents()
return cm, nil return cm
} }
// Docker implements Connector
func (cm *Docker) Wait() struct{} { return <-cm.closed }
// Docker events watcher // Docker events watcher
func (cm *Docker) watchEvents() { func (cm *Docker) watchEvents() {
log.Info("docker event listener starting") log.Info("docker event listener starting")
@@ -85,49 +49,17 @@ func (cm *Docker) watchEvents() {
continue continue
} }
actionName := e.Action actionName := strings.Split(e.Action, ":")[0]
// fast skip all exec_* events: exec_create, exec_start, exec_die
if strings.HasPrefix(actionName, "exec_") {
continue
}
// Action may have additional param i.e. "health_status: healthy"
// We need to strip to have only action name
sepIdx := strings.Index(actionName, ": ")
if sepIdx != -1 {
actionName = actionName[:sepIdx]
}
switch actionName { switch actionName {
// most frequent event is a health checks case "start", "die", "pause", "unpause", "health_status":
case "health_status": log.Debugf("handling docker event: action=%s id=%s", e.Action, e.ID)
healthStatus := e.Action[sepIdx+2:]
if log.IsEnabledFor(logging.DEBUG) {
log.Debugf("handling docker event: action=health_status id=%s %s", e.ID, healthStatus)
}
cm.statuses <- StatusUpdate{e.ID, "health", healthStatus}
case "create":
if log.IsEnabledFor(logging.DEBUG) {
log.Debugf("handling docker event: action=create id=%s", e.ID)
}
cm.needsRefresh <- e.ID cm.needsRefresh <- e.ID
case "destroy": case "destroy":
if log.IsEnabledFor(logging.DEBUG) { log.Debugf("handling docker event: action=%s id=%s", e.Action, e.ID)
log.Debugf("handling docker event: action=destroy id=%s", e.ID)
}
cm.delByID(e.ID) cm.delByID(e.ID)
default:
// check if this action changes status e.g. start -> running
status := actionToStatus[actionName]
if status != "" {
if log.IsEnabledFor(logging.DEBUG) {
log.Debugf("handling docker event: action=%s id=%s %s", actionName, e.ID, status)
}
cm.statuses <- StatusUpdate{e.ID, "status", status}
}
} }
} }
log.Info("docker event listener exited")
close(cm.closed)
} }
func portsFormat(ports map[api.Port][]api.PortBinding) string { func portsFormat(ports map[api.Port][]api.PortBinding) string {
@@ -160,12 +92,9 @@ func ipsFormat(networks map[string]api.ContainerNetwork) string {
} }
func (cm *Docker) refresh(c *container.Container) { func (cm *Docker) refresh(c *container.Container) {
insp, found, failed := cm.inspect(c.Id) insp := cm.inspect(c.Id)
if failed {
return
}
// remove container if no longer exists // remove container if no longer exists
if !found { if insp == nil {
cm.delByID(c.Id) cm.delByID(c.Id)
return return
} }
@@ -175,21 +104,20 @@ func (cm *Docker) refresh(c *container.Container) {
c.SetMeta("ports", portsFormat(insp.NetworkSettings.Ports)) c.SetMeta("ports", portsFormat(insp.NetworkSettings.Ports))
c.SetMeta("created", insp.Created.Format("Mon Jan 2 15:04:05 2006")) c.SetMeta("created", insp.Created.Format("Mon Jan 2 15:04:05 2006"))
c.SetMeta("health", insp.State.Health.Status) c.SetMeta("health", insp.State.Health.Status)
c.SetMeta("[ENV-VAR]", strings.Join(insp.Config.Env, ";")) for _, env := range insp.Config.Env {
c.SetMeta("[ENV-VAR]", env)
}
c.SetState(insp.State.Status) c.SetState(insp.State.Status)
} }
func (cm *Docker) inspect(id string) (insp *api.Container, found bool, failed bool) { func (cm *Docker) inspect(id string) *api.Container {
c, err := cm.client.InspectContainer(id) c, err := cm.client.InspectContainer(id)
if err != nil { if err != nil {
if _, notFound := err.(*api.NoSuchContainer); notFound { if _, ok := err.(*api.NoSuchContainer); !ok {
return c, false, false log.Errorf(err.Error())
} }
// other error e.g. connection failed
log.Errorf("%s (%T)", err.Error(), err)
return c, false, true
} }
return c, true, false return c
} }
// Mark all container IDs for refresh // Mark all container IDs for refresh
@@ -197,8 +125,7 @@ func (cm *Docker) refreshAll() {
opts := api.ListContainersOptions{All: true} opts := api.ListContainersOptions{All: true}
allContainers, err := cm.client.ListContainers(opts) allContainers, err := cm.client.ListContainers(opts)
if err != nil { if err != nil {
log.Errorf("%s (%T)", err.Error(), err) panic(err)
return
} }
for _, i := range allContainers { for _, i := range allContainers {
@@ -210,36 +137,13 @@ func (cm *Docker) refreshAll() {
} }
func (cm *Docker) Loop() { func (cm *Docker) Loop() {
for { for id := range cm.needsRefresh {
select { c := cm.MustGet(id)
case id := <-cm.needsRefresh: cm.refresh(c)
c := cm.MustGet(id)
cm.refresh(c)
case <-cm.closed:
return
}
} }
} }
func (cm *Docker) LoopStatuses() { // Get a single container, creating one anew if not existing
for {
select {
case statusUpdate := <-cm.statuses:
c, _ := cm.Get(statusUpdate.Cid)
if c != nil {
if statusUpdate.Field == "health" {
c.SetMeta("health", statusUpdate.Status)
} else {
c.SetState(statusUpdate.Status)
}
}
case <-cm.closed:
return
}
}
}
// MustGet gets a single container, creating one anew if not existing
func (cm *Docker) MustGet(id string) *container.Container { func (cm *Docker) MustGet(id string) *container.Container {
c, ok := cm.Get(id) c, ok := cm.Get(id)
// append container struct for new containers // append container struct for new containers
@@ -257,7 +161,7 @@ func (cm *Docker) MustGet(id string) *container.Container {
return c return c
} }
// Docker implements Connector // Get a single container, by ID
func (cm *Docker) Get(id string) (*container.Container, bool) { func (cm *Docker) Get(id string) (*container.Container, bool) {
cm.lock.Lock() cm.lock.Lock()
c, ok := cm.containers[id] c, ok := cm.containers[id]
@@ -273,7 +177,7 @@ func (cm *Docker) delByID(id string) {
log.Infof("removed dead container: %s", id) log.Infof("removed dead container: %s", id)
} }
// Docker implements Connector // Return array of all containers, sorted by field
func (cm *Docker) All() (containers container.Containers) { func (cm *Docker) All() (containers container.Containers) {
cm.lock.Lock() cm.lock.Lock()
for _, c := range cm.containers { for _, c := range cm.containers {
@@ -288,5 +192,5 @@ func (cm *Docker) All() (containers container.Containers) {
// use primary container name // use primary container name
func shortName(name string) string { func shortName(name string) string {
return strings.TrimPrefix(name, "/") return strings.Replace(name, "/", "", 1)
} }

218
connector/kubernetes.go Normal file
View File

@@ -0,0 +1,218 @@
package connector
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
bcfg "github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/connector/collector"
"github.com/bcicen/ctop/connector/manager"
"github.com/bcicen/ctop/container"
api "github.com/fsouza/go-dockerclient"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
func init() { enabled["kubernetes"] = NewKubernetes }
type Kubernetes struct {
namespace string
clientset *kubernetes.Clientset
containers map[string]*container.Container
needsRefresh chan string // container IDs requiring refresh
lock sync.RWMutex
}
func NewKubernetes() Connector {
var kubeconfig string
//if home := homeDir(); home != "" {
// kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
//} else {
// kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
//}
//flag.Parse()
kubeconfig = filepath.Join(homeDir(), ".kube", "config")
// use the current context in kubeconfig
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
log.Error(err.Error())
return nil
}
// create the clientset
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
log.Error(err.Error())
return nil
}
// init docker client
k := &Kubernetes{
clientset: clientset,
containers: make(map[string]*container.Container),
needsRefresh: make(chan string, 60),
lock: sync.RWMutex{},
namespace: bcfg.GetVal("namespace"),
}
go k.Loop()
k.refreshAll()
go k.watchEvents()
return k
}
func (k *Kubernetes) watchEvents() {
for {
log.Info("kubernetes event listener starting")
allEvents, err := k.clientset.CoreV1().Events(k.namespace).List(metav1.ListOptions{})
if err != nil {
log.Error(err.Error())
return
}
for _, e := range allEvents.Items {
if e.Kind != "pod" {
continue
}
actionName := strings.Split(e.Action, ":")[0]
switch actionName {
case "start", "die", "pause", "unpause", "health_status":
log.Debugf("handling docker event: action=%s id=%s", e.Action, e.UID)
k.needsRefresh <- e.Name
case "destroy":
log.Debugf("handling docker event: action=%s id=%s", e.Action, e.UID)
k.delByID(e.Name)
default:
log.Debugf("handling docker event: %v", e)
k.needsRefresh <- e.Name
}
}
time.Sleep(1 * time.Second)
}
}
func (k *Kubernetes) Loop() {
for id := range k.needsRefresh {
c := k.MustGet(id)
k.refresh(c)
}
}
// Get a single container, creating one anew if not existing
func (k *Kubernetes) MustGet(name string) *container.Container {
c, ok := k.Get(name)
// append container struct for new containers
if !ok {
// create collector
collector := collector.NewKubernetes(k.clientset, name)
// create manager
manager := manager.NewKubernetes(k.clientset, name)
// create container
c = container.New(name, collector, manager)
k.lock.Lock()
k.containers[name] = c
k.lock.Unlock()
}
return c
}
func (k *Kubernetes) refresh(c *container.Container) {
insp := k.inspect(c.Id)
// remove container if no longer exists
if insp == nil {
k.delByID(c.Id)
return
}
c.SetMeta("name", insp.Name)
if len(insp.Spec.Containers) >= 1 {
c.SetMeta("image", insp.Spec.Containers[0].Image)
c.SetMeta("ports", k8sPort(insp.Spec.Containers[0].Ports))
for _, env := range insp.Spec.Containers[0].Env {
c.SetMeta("[ENV-VAR]", env.Name+"="+env.Value)
}
}
c.SetMeta("IPs", insp.Status.PodIP)
c.SetMeta("created", insp.CreationTimestamp.Format("Mon Jan 2 15:04:05 2006"))
c.SetMeta("health", string(insp.Status.Phase))
c.SetState("running")
}
func k8sPort(ports []v1.ContainerPort) string {
str := []string{}
for _, p := range ports {
str = append(str, fmt.Sprintf("%s:%d -> %d", p.HostIP, p.HostPort, p.ContainerPort))
}
return strings.Join(str, "\n")
}
func (k *Kubernetes) inspect(id string) *v1.Pod {
p, err := k.clientset.CoreV1().Pods(k.namespace).Get(id, metav1.GetOptions{})
if err != nil {
if _, ok := err.(*api.NoSuchContainer); !ok {
log.Errorf(err.Error())
}
}
return p
}
// Remove containers by ID
func (k *Kubernetes) delByID(name string) {
k.lock.Lock()
delete(k.containers, name)
k.lock.Unlock()
log.Infof("removed dead container: %s", name)
}
func (k *Kubernetes) Get(name string) (c *container.Container, ok bool) {
k.lock.Lock()
c, ok = k.containers[name]
k.lock.Unlock()
return
}
// Mark all container IDs for refresh
func (k *Kubernetes) refreshAll() {
allPods, err := k.clientset.CoreV1().Pods(k.namespace).List(metav1.ListOptions{})
if err != nil {
log.Error(err.Error())
return
}
for _, pod := range allPods.Items {
c := k.MustGet(pod.Name)
c.SetMeta("uid", string(pod.UID))
c.SetMeta("name", pod.Name)
if pod.Initializers != nil && pod.Initializers.Result != nil {
c.SetState(pod.Initializers.Result.Status)
} else {
c.SetState(string(pod.Status.Phase))
}
k.needsRefresh <- c.Id
}
}
func (k *Kubernetes) All() (containers container.Containers) {
k.lock.Lock()
for _, c := range k.containers {
containers = append(containers, c)
}
containers.Sort()
containers.Filter()
k.lock.Unlock()
return containers
}
func homeDir() string {
if h := os.Getenv("HOME"); h != "" {
return h
}
return os.Getenv("USERPROFILE") // windows
}

View File

@@ -3,8 +3,6 @@ package connector
import ( import (
"fmt" "fmt"
"sort" "sort"
"sync"
"time"
"github.com/bcicen/ctop/container" "github.com/bcicen/ctop/container"
"github.com/bcicen/ctop/logging" "github.com/bcicen/ctop/logging"
@@ -12,80 +10,10 @@ import (
var ( var (
log = logging.Init() log = logging.Init()
enabled = make(map[string]ConnectorFn) enabled = make(map[string]func() Connector)
) )
type ConnectorFn func() (Connector, error) // return names for all enabled connectors on the current platform
type Connector interface {
// All returns a pre-sorted container.Containers of all discovered containers
All() container.Containers
// Get returns a single container.Container by ID
Get(string) (*container.Container, bool)
// Wait blocks until the underlying connection is lost
Wait() struct{}
}
// ConnectorSuper provides initial connection and retry on failure for
// an undlerying Connector type
type ConnectorSuper struct {
conn Connector
connFn ConnectorFn
err error
lock sync.RWMutex
}
func NewConnectorSuper(connFn ConnectorFn) *ConnectorSuper {
cs := &ConnectorSuper{
connFn: connFn,
err: fmt.Errorf("connecting..."),
}
go cs.loop()
return cs
}
// Get returns the underlying Connector, or nil and an error
// if the Connector is not yet initialized or is disconnected.
func (cs *ConnectorSuper) Get() (Connector, error) {
cs.lock.RLock()
defer cs.lock.RUnlock()
if cs.err != nil {
return nil, cs.err
}
return cs.conn, nil
}
func (cs *ConnectorSuper) setError(err error) {
cs.lock.Lock()
defer cs.lock.Unlock()
cs.err = err
}
func (cs *ConnectorSuper) loop() {
const interval = 3
for {
log.Infof("initializing connector")
conn, err := cs.connFn()
if err != nil {
cs.setError(err)
log.Errorf("failed to initialize connector: %s (%T)", err, err)
log.Errorf("retrying in %ds", interval)
time.Sleep(interval * time.Second)
} else {
cs.conn = conn
cs.setError(nil)
log.Infof("successfully initialized connector")
// wait until connection closed
cs.conn.Wait()
cs.setError(fmt.Errorf("attempting to reconnect..."))
log.Infof("connector closed")
}
}
}
// Enabled returns names for all enabled connectors on the current platform
func Enabled() (a []string) { func Enabled() (a []string) {
for k, _ := range enabled { for k, _ := range enabled {
a = append(a, k) a = append(a, k)
@@ -94,11 +22,14 @@ func Enabled() (a []string) {
return a return a
} }
// ByName returns a ConnectorSuper for a given name, or error if the connector func ByName(s string) (Connector, error) {
// does not exists on the current platform
func ByName(s string) (*ConnectorSuper, error) {
if cfn, ok := enabled[s]; ok { if cfn, ok := enabled[s]; ok {
return NewConnectorSuper(cfn), nil return cfn(), nil
} }
return nil, fmt.Errorf("invalid connector type \"%s\"", s) return nil, fmt.Errorf("invalid connector type \"%s\"", s)
} }
type Connector interface {
All() container.Containers
Get(string) (*container.Container, bool)
}

View File

@@ -3,9 +3,6 @@ package manager
import ( import (
"fmt" "fmt"
api "github.com/fsouza/go-dockerclient" api "github.com/fsouza/go-dockerclient"
"github.com/pkg/errors"
"io"
"os"
) )
type Docker struct { type Docker struct {
@@ -20,88 +17,6 @@ func NewDocker(client *api.Client, id string) *Docker {
} }
} }
// Do not allow to close reader (i.e. /dev/stdin which docker client tries to close after command execution)
type noClosableReader struct {
io.Reader
}
func (w *noClosableReader) Read(p []byte) (n int, err error) {
return w.Reader.Read(p)
}
const (
STDIN = 0
STDOUT = 1
STDERR = 2
)
var wrongFrameFormat = errors.New("Wrong frame format")
// A frame has a Header and a Payload
// Header: [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4}
// STREAM_TYPE can be:
// 0: stdin (is written on stdout)
// 1: stdout
// 2: stderr
// SIZE1, SIZE2, SIZE3, SIZE4 are the four bytes of the uint32 size encoded as big endian.
// But we don't use size, because we don't need to find the end of frame.
type frameWriter struct {
stdout io.Writer
stderr io.Writer
stdin io.Writer
}
func (w *frameWriter) Write(p []byte) (n int, err error) {
// drop initial empty frames
if len(p) == 0 {
return 0, nil
}
if len(p) > 8 {
var targetWriter io.Writer
switch p[0] {
case STDIN:
targetWriter = w.stdin
break
case STDOUT:
targetWriter = w.stdout
break
case STDERR:
targetWriter = w.stderr
break
default:
return 0, wrongFrameFormat
}
n, err := targetWriter.Write(p[8:])
return n + 8, err
}
return 0, wrongFrameFormat
}
func (dc *Docker) Exec(cmd []string) error {
execCmd, err := dc.client.CreateExec(api.CreateExecOptions{
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
Cmd: cmd,
Container: dc.id,
Tty: true,
})
if err != nil {
return err
}
return dc.client.StartExec(execCmd.ID, api.StartExecOptions{
InputStream: &noClosableReader{os.Stdin},
OutputStream: &frameWriter{os.Stdout, os.Stderr, os.Stdin},
ErrorStream: os.Stderr,
RawTerminal: true,
})
}
func (dc *Docker) Start() error { func (dc *Docker) Start() error {
c, err := dc.client.InspectContainer(dc.id) c, err := dc.client.InspectContainer(dc.id)
if err != nil { if err != nil {

View File

@@ -0,0 +1,64 @@
package manager
import (
"k8s.io/client-go/kubernetes"
)
type Kubernetes struct {
id string
client *kubernetes.Clientset
}
func NewKubernetes(client *kubernetes.Clientset, id string) *Kubernetes {
return &Kubernetes{
id: id,
client: client,
}
}
func (dc *Kubernetes) Start() error {
//c, err := dc.client.InspectContainer(dc.id)
//if err != nil {
// return fmt.Errorf("cannot inspect container: %v", err)
//}
//if err := dc.client.StartContainer(c.ID, c.HostConfig); err != nil {
// return fmt.Errorf("cannot start container: %v", err)
//}
return nil
}
func (dc *Kubernetes) Stop() error {
//if err := dc.client.StopContainer(dc.id, 3); err != nil {
// return fmt.Errorf("cannot stop container: %v", err)
//}
return nil
}
func (dc *Kubernetes) Remove() error {
//if err := dc.client.RemoveContainer(api.RemoveContainerOptions{ID: dc.id}); err != nil {
// return fmt.Errorf("cannot remove container: %v", err)
//}
return nil
}
func (dc *Kubernetes) Pause() error {
//if err := dc.client.PauseContainer(dc.id); err != nil {
// return fmt.Errorf("cannot pause container: %v", err)
//}
return nil
}
func (dc *Kubernetes) Unpause() error {
//if err := dc.client.UnpauseContainer(dc.id); err != nil {
// return fmt.Errorf("cannot unpause container: %v", err)
//}
return nil
}
func (dc *Kubernetes) Restart() error {
//if err := dc.client.RestartContainer(dc.id, 3); err != nil {
// return fmt.Errorf("cannot restart container: %v", err)
//}
return nil
}

View File

@@ -1,9 +1,5 @@
package manager package manager
import "errors"
var ActionNotImplErr = errors.New("action not implemented")
type Manager interface { type Manager interface {
Start() error Start() error
Stop() error Stop() error
@@ -11,5 +7,4 @@ type Manager interface {
Pause() error Pause() error
Unpause() error Unpause() error
Restart() error Restart() error
Exec(cmd []string) error
} }

View File

@@ -7,29 +7,25 @@ func NewMock() *Mock {
} }
func (m *Mock) Start() error { func (m *Mock) Start() error {
return ActionNotImplErr return nil
} }
func (m *Mock) Stop() error { func (m *Mock) Stop() error {
return ActionNotImplErr return nil
} }
func (m *Mock) Remove() error { func (m *Mock) Remove() error {
return ActionNotImplErr return nil
} }
func (m *Mock) Pause() error { func (m *Mock) Pause() error {
return ActionNotImplErr return nil
} }
func (m *Mock) Unpause() error { func (m *Mock) Unpause() error {
return ActionNotImplErr return nil
} }
func (m *Mock) Restart() error { func (m *Mock) Restart() error {
return ActionNotImplErr return nil
}
func (m *Mock) Exec(cmd []string) error {
return ActionNotImplErr
} }

View File

@@ -7,29 +7,25 @@ func NewRunc() *Runc {
} }
func (rc *Runc) Start() error { func (rc *Runc) Start() error {
return ActionNotImplErr return nil
} }
func (rc *Runc) Stop() error { func (rc *Runc) Stop() error {
return ActionNotImplErr return nil
} }
func (rc *Runc) Remove() error { func (rc *Runc) Remove() error {
return ActionNotImplErr return nil
} }
func (rc *Runc) Pause() error { func (rc *Runc) Pause() error {
return ActionNotImplErr return nil
} }
func (rc *Runc) Unpause() error { func (rc *Runc) Unpause() error {
return ActionNotImplErr return nil
} }
func (rc *Runc) Restart() error { func (rc *Runc) Restart() error {
return ActionNotImplErr return nil
}
func (rc *Runc) Exec(cmd []string) error {
return ActionNotImplErr
} }

View File

@@ -20,11 +20,11 @@ type Mock struct {
containers container.Containers containers container.Containers
} }
func NewMock() (Connector, error) { func NewMock() Connector {
cs := &Mock{} cs := &Mock{}
go cs.Init() go cs.Init()
go cs.Loop() go cs.Loop()
return cs, nil return cs
} }
// Create Mock containers // Create Mock containers
@@ -32,46 +32,21 @@ func (cs *Mock) Init() {
rand.Seed(int64(time.Now().Nanosecond())) rand.Seed(int64(time.Now().Nanosecond()))
for i := 0; i < 4; i++ { for i := 0; i < 4; i++ {
cs.makeContainer(3, true) cs.makeContainer(3)
} }
for i := 0; i < 16; i++ { for i := 0; i < 16; i++ {
cs.makeContainer(1, false) cs.makeContainer(1)
} }
} }
func (cs *Mock) Wait() struct{} { func (cs *Mock) makeContainer(aggression int64) {
ch := make(chan struct{})
go func() {
time.Sleep(30 * time.Second)
close(ch)
}()
return <-ch
}
var healthStates = []string{"starting", "healthy", "unhealthy"}
func (cs *Mock) makeContainer(aggression int64, health bool) {
collector := collector.NewMock(aggression) collector := collector.NewMock(aggression)
manager := manager.NewMock() manager := manager.NewMock()
c := container.New(makeID(), collector, manager) c := container.New(makeID(), collector, manager)
c.SetMeta("name", makeName()) c.SetMeta("name", makeName())
c.SetState(makeState()) c.SetState(makeState())
if health {
var i int
c.SetMeta("health", healthStates[i])
go func() {
for {
i++
if i >= len(healthStates) {
i = 0
}
c.SetMeta("health", healthStates[i])
time.Sleep(12 * time.Second)
}
}()
}
cs.containers = append(cs.containers, c) cs.containers = append(cs.containers, c)
} }
@@ -98,7 +73,7 @@ func (cs *Mock) Get(id string) (*container.Container, bool) {
return nil, false return nil, false
} }
// All returns array of all containers, sorted by field // Return array of all containers, sorted by field
func (cs *Mock) All() container.Containers { func (cs *Mock) All() container.Containers {
cs.containers.Sort() cs.containers.Sort()
cs.containers.Filter() cs.containers.Filter()

View File

@@ -54,44 +54,35 @@ type Runc struct {
factory libcontainer.Factory factory libcontainer.Factory
containers map[string]*container.Container containers map[string]*container.Container
libContainers map[string]libcontainer.Container libContainers map[string]libcontainer.Container
closed chan struct{}
needsRefresh chan string // container IDs requiring refresh needsRefresh chan string // container IDs requiring refresh
lock sync.RWMutex lock sync.RWMutex
} }
func NewRunc() (Connector, error) { func NewRunc() Connector {
opts, err := NewRuncOpts() opts, err := NewRuncOpts()
if err != nil { runcFailOnErr(err)
return nil, err
}
factory, err := getFactory(opts) factory, err := getFactory(opts)
if err != nil { runcFailOnErr(err)
return nil, err
}
cm := &Runc{ cm := &Runc{
opts: opts, opts: opts,
factory: factory, factory: factory,
containers: make(map[string]*container.Container), containers: make(map[string]*container.Container),
libContainers: make(map[string]libcontainer.Container), libContainers: make(map[string]libcontainer.Container),
closed: make(chan struct{}), needsRefresh: make(chan string, 60),
lock: sync.RWMutex{}, lock: sync.RWMutex{},
} }
go func() { go func() {
for { for {
select { cm.refreshAll()
case <-cm.closed: time.Sleep(5 * time.Second)
return
case <-time.After(5 * time.Second):
cm.refreshAll()
}
} }
}() }()
go cm.Loop() go cm.Loop()
return cm, nil return cm
} }
func (cm *Runc) GetLibc(id string) libcontainer.Container { func (cm *Runc) GetLibc(id string) libcontainer.Container {
@@ -150,11 +141,7 @@ func (cm *Runc) refresh(id string) {
// Read runc root, creating any new containers // Read runc root, creating any new containers
func (cm *Runc) refreshAll() { func (cm *Runc) refreshAll() {
list, err := ioutil.ReadDir(cm.opts.root) list, err := ioutil.ReadDir(cm.opts.root)
if err != nil { runcFailOnErr(err)
log.Errorf("%s (%T)", err.Error(), err)
close(cm.closed)
return
}
for _, i := range list { for _, i := range list {
if i.IsDir() { if i.IsDir() {
@@ -181,7 +168,7 @@ func (cm *Runc) Loop() {
} }
} }
// MustGet gets a single ctop container in the map matching libc container, creating one anew if not existing // Get a single ctop container in the map matching libc container, creating one anew if not existing
func (cm *Runc) MustGet(id string) *container.Container { func (cm *Runc) MustGet(id string) *container.Container {
c, ok := cm.Get(id) c, ok := cm.Get(id)
if !ok { if !ok {
@@ -212,6 +199,14 @@ func (cm *Runc) MustGet(id string) *container.Container {
return c return c
} }
// Get a single container, by ID
func (cm *Runc) Get(id string) (*container.Container, bool) {
cm.lock.Lock()
defer cm.lock.Unlock()
c, ok := cm.containers[id]
return c, ok
}
// Remove containers by ID // Remove containers by ID
func (cm *Runc) delByID(id string) { func (cm *Runc) delByID(id string) {
cm.lock.Lock() cm.lock.Lock()
@@ -221,18 +216,7 @@ func (cm *Runc) delByID(id string) {
log.Infof("removed dead container: %s", id) log.Infof("removed dead container: %s", id)
} }
// Runc implements Connector // Return array of all containers, sorted by field
func (cm *Runc) Wait() struct{} { return <-cm.closed }
// Runc implements Connector
func (cm *Runc) Get(id string) (*container.Container, bool) {
cm.lock.Lock()
defer cm.lock.Unlock()
c, ok := cm.containers[id]
return c, ok
}
// Runc implements Connector
func (cm *Runc) All() (containers container.Containers) { func (cm *Runc) All() (containers container.Containers) {
cm.lock.Lock() cm.lock.Lock()
for _, c := range cm.containers { for _, c := range cm.containers {
@@ -247,7 +231,7 @@ func (cm *Runc) All() (containers container.Containers) {
func getFactory(opts RuncOpts) (libcontainer.Factory, error) { func getFactory(opts RuncOpts) (libcontainer.Factory, error) {
cgroupManager := libcontainer.Cgroupfs cgroupManager := libcontainer.Cgroupfs
if opts.systemdCgroups { if opts.systemdCgroups {
if systemd.IsRunningSystemd() { if systemd.UseSystemd() {
cgroupManager = libcontainer.SystemdCgroups cgroupManager = libcontainer.SystemdCgroups
} else { } else {
return nil, fmt.Errorf("systemd cgroup enabled, but systemd support for managing cgroups is not available") return nil, fmt.Errorf("systemd cgroup enabled, but systemd support for managing cgroups is not available")
@@ -255,3 +239,9 @@ func getFactory(opts RuncOpts) (libcontainer.Factory, error) {
} }
return libcontainer.New(opts.root, cgroupManager) return libcontainer.New(opts.root, cgroupManager)
} }
func runcFailOnErr(err error) {
if err != nil {
panic(fmt.Errorf("fatal runc error: %s", err))
}
}

View File

@@ -21,8 +21,8 @@ const (
type Container struct { type Container struct {
models.Metrics models.Metrics
Id string Id string
Meta models.Meta Meta map[string]string
Widgets *compact.CompactRow Widgets *compact.Compact
Display bool // display this container in compact view Display bool // display this container in compact view
updater cwidgets.WidgetUpdater updater cwidgets.WidgetUpdater
collector collector.Collector collector collector.Collector
@@ -30,11 +30,11 @@ type Container struct {
} }
func New(id string, collector collector.Collector, manager manager.Manager) *Container { func New(id string, collector collector.Collector, manager manager.Manager) *Container {
widgets := compact.NewCompactRow() widgets := compact.NewCompact(id)
return &Container{ return &Container{
Metrics: models.NewMetrics(), Metrics: models.NewMetrics(),
Id: id, Id: id,
Meta: models.NewMeta("id", id[:12]), Meta: make(map[string]string),
Widgets: widgets, Widgets: widgets,
updater: widgets, updater: widgets,
collector: collector, collector: collector,
@@ -42,24 +42,23 @@ func New(id string, collector collector.Collector, manager manager.Manager) *Con
} }
} }
func (c *Container) RecreateWidgets() {
c.SetUpdater(cwidgets.NullWidgetUpdater{})
c.Widgets = compact.NewCompactRow()
c.SetUpdater(c.Widgets)
}
func (c *Container) SetUpdater(u cwidgets.WidgetUpdater) { func (c *Container) SetUpdater(u cwidgets.WidgetUpdater) {
c.updater = u c.updater = u
c.updater.SetMeta(c.Meta) for k, v := range c.Meta {
c.updater.SetMeta(k, v)
}
} }
func (c *Container) SetMeta(k, v string) { func (c *Container) SetMeta(k, v string) {
c.Meta[k] = v c.Meta[k] = v
c.updater.SetMeta(c.Meta) c.updater.SetMeta(k, v)
} }
func (c *Container) GetMeta(k string) string { func (c *Container) GetMeta(k string) string {
return c.Meta.Get(k) if v, ok := c.Meta[k]; ok {
return v
}
return ""
} }
func (c *Container) SetState(s string) { func (c *Container) SetState(s string) {
@@ -75,7 +74,7 @@ func (c *Container) SetState(s string) {
} }
} }
// Logs returns container log collector // Return container log collector
func (c *Container) Logs() collector.LogCollector { func (c *Container) Logs() collector.LogCollector {
return c.collector.Logs() return c.collector.Logs()
} }
@@ -154,7 +153,3 @@ func (c *Container) Restart() {
} }
} }
} }
func (c *Container) Exec(cmd []string) error {
return c.manager.Exec(cmd)
}

View File

@@ -11,7 +11,7 @@ import (
type GridCursor struct { type GridCursor struct {
selectedID string // id of currently selected container selectedID string // id of currently selected container
filtered container.Containers filtered container.Containers
cSuper *connector.ConnectorSuper cSource connector.Connector
isScrolling bool // toggled when actively scrolling isScrolling bool // toggled when actively scrolling
} }
@@ -25,20 +25,14 @@ func (gc *GridCursor) Selected() *container.Container {
return nil return nil
} }
// Refresh containers from source, returning whether the quantity of // Refresh containers from source
// containers has changed and any error func (gc *GridCursor) RefreshContainers() (lenChanged bool) {
func (gc *GridCursor) RefreshContainers() (bool, error) {
oldLen := gc.Len() oldLen := gc.Len()
// Containers filtered by display bool
gc.filtered = container.Containers{} gc.filtered = container.Containers{}
cSource, err := gc.cSuper.Get()
if err != nil {
return true, err
}
// filter Containers by display bool
var cursorVisible bool var cursorVisible bool
for _, c := range cSource.All() { for _, c := range gc.cSource.All() {
if c.Display { if c.Display {
if c.Id == gc.selectedID { if c.Id == gc.selectedID {
cursorVisible = true cursorVisible = true
@@ -47,21 +41,22 @@ func (gc *GridCursor) RefreshContainers() (bool, error) {
} }
} }
if !cursorVisible || gc.selectedID == "" { if oldLen != gc.Len() {
gc.Reset() lenChanged = true
} }
return oldLen != gc.Len(), nil if !cursorVisible {
gc.Reset()
}
if gc.selectedID == "" {
gc.Reset()
}
return lenChanged
} }
// Set an initial cursor position, if possible // Set an initial cursor position, if possible
func (gc *GridCursor) Reset() { func (gc *GridCursor) Reset() {
cSource, err := gc.cSuper.Get() for _, c := range gc.cSource.All() {
if err != nil {
return
}
for _, c := range cSource.All() {
c.Widgets.UnHighlight() c.Widgets.UnHighlight()
} }
if gc.Len() > 0 { if gc.Len() > 0 {
@@ -70,7 +65,7 @@ func (gc *GridCursor) Reset() {
} }
} }
// Idx returns current cursor index // Return current cursor index
func (gc *GridCursor) Idx() int { func (gc *GridCursor) Idx() int {
for n, c := range gc.filtered { for n, c := range gc.filtered {
if c.Id == gc.selectedID { if c.Id == gc.selectedID {

View File

@@ -1,54 +0,0 @@
package compact
import (
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/models"
ui "github.com/gizak/termui"
)
var (
allCols = map[string]NewCompactColFn{
"status": NewStatus,
"name": NewNameCol,
"id": NewCIDCol,
"image": NewImageCol,
"ports": NewPortsCol,
"IPs": NewIpsCol,
"created": NewCreatedCol,
"cpu": NewCPUCol,
"cpus": NewCpuScaledCol,
"mem": NewMemCol,
"net": NewNetCol,
"io": NewIOCol,
"pids": NewPIDCol,
}
)
type NewCompactColFn func() CompactCol
func newRowWidgets() []CompactCol {
enabled := config.EnabledColumns()
cols := make([]CompactCol, len(enabled))
for n, name := range enabled {
wFn, ok := allCols[name]
if !ok {
panic("no such widget name: %s" + name)
}
cols[n] = wFn()
}
return cols
}
type CompactCol interface {
ui.GridBufferer
Reset()
Header() string // header text to display for column
FixedWidth() int // fixed width size. if == 0, width is automatically calculated
Highlight()
UnHighlight()
SetMeta(models.Meta)
SetMetrics(models.Metrics)
}

View File

@@ -1,68 +1,21 @@
package compact package compact
import ( import (
"fmt"
"github.com/bcicen/ctop/cwidgets"
"github.com/bcicen/ctop/models"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
type CPUCol struct {
*GaugeCol
scaleCpu bool
}
func NewCPUCol() CompactCol {
return &CPUCol{NewGaugeCol("CPU"), false}
}
func NewCpuScaledCol() CompactCol {
return &CPUCol{NewGaugeCol("CPUS"), true}
}
func (w *CPUCol) SetMetrics(m models.Metrics) {
val := m.CPUUtil
w.BarColor = colorScale(val)
if !w.scaleCpu {
val = val * int(m.NCpus)
}
w.Label = fmt.Sprintf("%d%%", val)
if val > 100 {
val = 100
}
w.Percent = val
}
type MemCol struct {
*GaugeCol
}
func NewMemCol() CompactCol {
return &MemCol{NewGaugeCol("MEM")}
}
func (w *MemCol) SetMetrics(m models.Metrics) {
w.BarColor = ui.ThemeAttr("gauge.bar.bg")
w.Label = fmt.Sprintf("%s / %s", cwidgets.ByteFormat64Short(m.MemUsage), cwidgets.ByteFormat64Short(m.MemLimit))
w.Percent = m.MemPercent
}
type GaugeCol struct { type GaugeCol struct {
*ui.Gauge *ui.Gauge
header string
fWidth int
} }
func NewGaugeCol(header string) *GaugeCol { func NewGaugeCol() *GaugeCol {
g := &GaugeCol{ui.NewGauge(), header, 0} g := ui.NewGauge()
g.Height = 1 g.Height = 1
g.Border = false g.Border = false
g.Percent = 0
g.PaddingBottom = 0 g.PaddingBottom = 0
g.Reset() g.Label = "-"
return g return &GaugeCol{g}
} }
func (w *GaugeCol) Reset() { func (w *GaugeCol) Reset() {
@@ -70,30 +23,11 @@ func (w *GaugeCol) Reset() {
w.Percent = 0 w.Percent = 0
} }
func (w *GaugeCol) Buffer() ui.Buffer {
// if bar would not otherwise be visible, set a minimum
// percentage value and low-contrast color for structure
if w.Percent < 5 {
w.Percent = 5
w.BarColor = ui.ColorBlack
}
return w.Gauge.Buffer()
}
// GaugeCol implements CompactCol
func (w *GaugeCol) SetMeta(models.Meta) {}
func (w *GaugeCol) SetMetrics(models.Metrics) {}
func (w *GaugeCol) Header() string { return w.header }
func (w *GaugeCol) FixedWidth() int { return w.fWidth }
// GaugeCol implements CompactCol
func (w *GaugeCol) Highlight() { func (w *GaugeCol) Highlight() {
w.Bg = ui.ThemeAttr("par.text.fg") w.Bg = ui.ThemeAttr("par.text.fg")
w.PercentColor = ui.ThemeAttr("par.text.hi") w.PercentColor = ui.ThemeAttr("par.text.hi")
} }
// GaugeCol implements CompactCol
func (w *GaugeCol) UnHighlight() { func (w *GaugeCol) UnHighlight() {
w.Bg = ui.ThemeAttr("par.text.bg") w.Bg = ui.ThemeAttr("par.text.bg")
w.PercentColor = ui.ThemeAttr("par.text.bg") w.PercentColor = ui.ThemeAttr("par.text.bg")

View File

@@ -4,11 +4,11 @@ import (
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
var header *CompactHeader
type CompactGrid struct { type CompactGrid struct {
ui.GridBufferer ui.GridBufferer
header *CompactHeader Rows []ui.GridBufferer
cols []CompactCol // reference columns
Rows []RowBufferer
X, Y int X, Y int
Width int Width int
Height int Height int
@@ -16,9 +16,8 @@ type CompactGrid struct {
} }
func NewCompactGrid() *CompactGrid { func NewCompactGrid() *CompactGrid {
cg := &CompactGrid{header: NewCompactHeader()} header = NewCompactHeader() // init column header
cg.rebuildHeader() return &CompactGrid{}
return cg
} }
func (cg *CompactGrid) Align() { func (cg *CompactGrid) Align() {
@@ -29,51 +28,22 @@ func (cg *CompactGrid) Align() {
} }
// update row ypos, width recursively // update row ypos, width recursively
colWidths := cg.calcWidths()
for _, r := range cg.pageRows() { for _, r := range cg.pageRows() {
r.SetY(y) r.SetY(y)
y += r.GetHeight() y += r.GetHeight()
r.SetWidths(cg.Width, colWidths) r.SetWidth(cg.Width)
} }
} }
func (cg *CompactGrid) Clear() { func (cg *CompactGrid) Clear() { cg.Rows = []ui.GridBufferer{} }
cg.Rows = []RowBufferer{} func (cg *CompactGrid) GetHeight() int { return len(cg.Rows) + header.Height }
cg.rebuildHeader()
}
func (cg *CompactGrid) GetHeight() int { return len(cg.Rows) + cg.header.Height }
func (cg *CompactGrid) SetX(x int) { cg.X = x } func (cg *CompactGrid) SetX(x int) { cg.X = x }
func (cg *CompactGrid) SetY(y int) { cg.Y = y } func (cg *CompactGrid) SetY(y int) { cg.Y = y }
func (cg *CompactGrid) SetWidth(w int) { cg.Width = w } func (cg *CompactGrid) SetWidth(w int) { cg.Width = w }
func (cg *CompactGrid) MaxRows() int { return ui.TermHeight() - cg.header.Height - cg.Y } func (cg *CompactGrid) MaxRows() int { return ui.TermHeight() - header.Height - cg.Y }
// calculate and return per-column width func (cg *CompactGrid) pageRows() (rows []ui.GridBufferer) {
func (cg *CompactGrid) calcWidths() []int { rows = append(rows, header)
var autoCols int
width := cg.Width
colWidths := make([]int, len(cg.cols))
for n, w := range cg.cols {
colWidths[n] = w.FixedWidth()
width -= w.FixedWidth()
if w.FixedWidth() == 0 {
autoCols++
}
}
spacing := colSpacing * len(cg.cols)
autoWidth := (width - spacing) / autoCols
for n, val := range colWidths {
if val == 0 {
colWidths[n] = autoWidth
}
}
return colWidths
}
func (cg *CompactGrid) pageRows() (rows []RowBufferer) {
rows = append(rows, cg.header)
rows = append(rows, cg.Rows[cg.Offset:]...) rows = append(rows, cg.Rows[cg.Offset:]...)
return rows return rows
} }
@@ -86,14 +56,6 @@ func (cg *CompactGrid) Buffer() ui.Buffer {
return buf return buf
} }
func (cg *CompactGrid) AddRows(rows ...RowBufferer) { func (cg *CompactGrid) AddRows(rows ...ui.GridBufferer) {
cg.Rows = append(cg.Rows, rows...) cg.Rows = append(cg.Rows, rows...)
} }
func (cg *CompactGrid) rebuildHeader() {
cg.cols = newRowWidgets()
cg.header.clearFieldPars()
for _, col := range cg.cols {
cg.header.addFieldPar(col.Header())
}
}

View File

@@ -8,59 +8,63 @@ type CompactHeader struct {
X, Y int X, Y int
Width int Width int
Height int Height int
cols []CompactCol
widths []int
pars []*ui.Par pars []*ui.Par
} }
func NewCompactHeader() *CompactHeader { func NewCompactHeader() *CompactHeader {
return &CompactHeader{ fields := []string{"", "NAME", "CID", "CPU", "MEM", "NET RX/TX", "IO R/W", "PIDS"}
X: rowPadding, ch := &CompactHeader{}
Height: 2, ch.Height = 2
for _, f := range fields {
ch.addFieldPar(f)
} }
return ch
} }
func (row *CompactHeader) GetHeight() int { func (ch *CompactHeader) GetHeight() int {
return row.Height return ch.Height
} }
func (row *CompactHeader) SetWidths(totalWidth int, widths []int) { func (ch *CompactHeader) SetWidth(w int) {
x := row.X x := ch.X
autoWidth := calcWidth(w)
for n, w := range row.pars { for n, col := range ch.pars {
w.SetX(x) // set column to static width
w.SetWidth(widths[n]) if colWidths[n] != 0 {
x += widths[n] + colSpacing col.SetX(x)
col.SetWidth(colWidths[n])
x += colWidths[n]
continue
}
col.SetX(x)
col.SetWidth(autoWidth)
x += autoWidth + colSpacing
} }
row.Width = totalWidth ch.Width = w
} }
func (row *CompactHeader) SetX(x int) { func (ch *CompactHeader) SetX(x int) {
row.X = x ch.X = x
} }
func (row *CompactHeader) SetY(y int) { func (ch *CompactHeader) SetY(y int) {
for _, p := range row.pars { for _, p := range ch.pars {
p.SetY(y) p.SetY(y)
} }
row.Y = y ch.Y = y
} }
func (row *CompactHeader) Buffer() ui.Buffer { func (ch *CompactHeader) Buffer() ui.Buffer {
buf := ui.NewBuffer() buf := ui.NewBuffer()
for _, p := range row.pars { for _, p := range ch.pars {
buf.Merge(p.Buffer()) buf.Merge(p.Buffer())
} }
return buf return buf
} }
func (row *CompactHeader) clearFieldPars() { func (ch *CompactHeader) addFieldPar(s string) {
row.pars = []*ui.Par{}
}
func (row *CompactHeader) addFieldPar(s string) {
p := ui.NewPar(s) p := ui.NewPar(s)
p.Height = row.Height p.Height = ch.Height
p.Border = false p.Border = false
row.pars = append(row.pars, p) ch.pars = append(ch.pars, p)
} }

195
cwidgets/compact/main.go Normal file
View File

@@ -0,0 +1,195 @@
package compact
import (
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/models"
ui "github.com/gizak/termui"
)
var log = logging.Init()
type Compact struct {
Status *Status
Name *TextCol
Cid *TextCol
Cpu *GaugeCol
Mem *GaugeCol
Net *TextCol
IO *TextCol
Pids *TextCol
Bg *RowBg
X, Y int
Width int
Height int
}
func NewCompact(id string) *Compact {
// truncate container id
if len(id) > 12 {
id = id[:12]
}
row := &Compact{
Status: NewStatus(),
Name: NewTextCol("-"),
Cid: NewTextCol(id),
Cpu: NewGaugeCol(),
Mem: NewGaugeCol(),
Net: NewTextCol("-"),
IO: NewTextCol("-"),
Pids: NewTextCol("-"),
Bg: NewRowBg(),
X: 1,
Height: 1,
}
return row
}
//func (row *Compact) ToggleExpand() {
//if row.Height == 1 {
//row.Height = 4
//} else {
//row.Height = 1
//}
//}
func (row *Compact) SetMeta(k, v string) {
switch k {
case "name":
row.Name.Set(v)
case "state":
row.Status.Set(v)
case "health":
row.Status.SetHealth(v)
}
}
func (row *Compact) SetMetrics(m models.Metrics) {
row.SetCPU(m.CPUUtil)
row.SetNet(m.NetRx, m.NetTx)
row.SetMem(m.MemUsage, m.MemLimit, m.MemPercent)
row.SetIO(m.IOBytesRead, m.IOBytesWrite)
row.SetPids(m.Pids)
}
// Set gauges, counters to default unread values
func (row *Compact) Reset() {
row.Cpu.Reset()
row.Mem.Reset()
row.Net.Reset()
row.IO.Reset()
row.Pids.Reset()
}
func (row *Compact) GetHeight() int {
return row.Height
}
func (row *Compact) SetX(x int) {
row.X = x
}
func (row *Compact) SetY(y int) {
if y == row.Y {
return
}
row.Bg.Y = y
for _, col := range row.all() {
col.SetY(y)
}
row.Y = y
}
func (row *Compact) SetWidth(width int) {
if width == row.Width {
return
}
x := row.X
row.Bg.SetX(x + colWidths[0] + 1)
row.Bg.SetWidth(width)
autoWidth := calcWidth(width)
for n, col := range row.all() {
if colWidths[n] != 0 {
col.SetX(x)
col.SetWidth(colWidths[n])
x += colWidths[n]
continue
}
col.SetX(x)
col.SetWidth(autoWidth)
x += autoWidth + colSpacing
}
row.Width = width
}
func (row *Compact) Buffer() ui.Buffer {
buf := ui.NewBuffer()
buf.Merge(row.Bg.Buffer())
buf.Merge(row.Status.Buffer())
buf.Merge(row.Name.Buffer())
buf.Merge(row.Cid.Buffer())
buf.Merge(row.Cpu.Buffer())
buf.Merge(row.Mem.Buffer())
buf.Merge(row.Net.Buffer())
buf.Merge(row.IO.Buffer())
buf.Merge(row.Pids.Buffer())
return buf
}
func (row *Compact) all() []ui.GridBufferer {
return []ui.GridBufferer{
row.Status,
row.Name,
row.Cid,
row.Cpu,
row.Mem,
row.Net,
row.IO,
row.Pids,
}
}
func (row *Compact) Highlight() {
row.Name.Highlight()
if config.GetSwitchVal("fullRowCursor") {
row.Bg.Highlight()
row.Cid.Highlight()
row.Cpu.Highlight()
row.Mem.Highlight()
row.Net.Highlight()
row.IO.Highlight()
row.Pids.Highlight()
}
}
func (row *Compact) UnHighlight() {
row.Name.UnHighlight()
if config.GetSwitchVal("fullRowCursor") {
row.Bg.UnHighlight()
row.Cid.UnHighlight()
row.Cpu.UnHighlight()
row.Mem.UnHighlight()
row.Net.UnHighlight()
row.IO.UnHighlight()
row.Pids.UnHighlight()
}
}
type RowBg struct {
*ui.Par
}
func NewRowBg() *RowBg {
bg := ui.NewPar("")
bg.Height = 1
bg.Border = false
bg.Bg = ui.ThemeAttr("par.text.bg")
return &RowBg{bg}
}
func (w *RowBg) Highlight() { w.Bg = ui.ThemeAttr("par.text.fg") }
func (w *RowBg) UnHighlight() { w.Bg = ui.ThemeAttr("par.text.bg") }

View File

@@ -1,129 +0,0 @@
package compact
import (
"github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/models"
ui "github.com/gizak/termui"
)
const rowPadding = 1
var log = logging.Init()
type RowBufferer interface {
SetY(int)
SetWidths(int, []int)
GetHeight() int
Buffer() ui.Buffer
}
type CompactRow struct {
Bg *RowBg
Cols []CompactCol
X, Y int
Height int
widths []int // column widths
}
func NewCompactRow() *CompactRow {
row := &CompactRow{
Bg: NewRowBg(),
Cols: newRowWidgets(),
X: rowPadding,
Height: 1,
}
return row
}
func (row *CompactRow) SetMeta(m models.Meta) {
for _, w := range row.Cols {
w.SetMeta(m)
}
}
func (row *CompactRow) SetMetrics(m models.Metrics) {
for _, w := range row.Cols {
w.SetMetrics(m)
}
}
// Set gauges, counters, etc. to default unread values
func (row *CompactRow) Reset() {
for _, w := range row.Cols {
w.Reset()
}
}
func (row *CompactRow) GetHeight() int { return row.Height }
//func (row *CompactRow) SetX(x int) { row.X = x }
func (row *CompactRow) SetY(y int) {
if y == row.Y {
return
}
row.Bg.Y = y
for _, w := range row.Cols {
w.SetY(y)
}
row.Y = y
}
func (row *CompactRow) SetWidths(totalWidth int, widths []int) {
x := row.X
row.Bg.SetX(x)
row.Bg.SetWidth(totalWidth)
for n, w := range row.Cols {
w.SetX(x)
w.SetWidth(widths[n])
x += widths[n] + colSpacing
}
}
func (row *CompactRow) Buffer() ui.Buffer {
buf := ui.NewBuffer()
buf.Merge(row.Bg.Buffer())
for _, w := range row.Cols {
buf.Merge(w.Buffer())
}
return buf
}
func (row *CompactRow) Highlight() {
row.Cols[1].Highlight()
if config.GetSwitchVal("fullRowCursor") {
for _, w := range row.Cols {
w.Highlight()
}
}
}
func (row *CompactRow) UnHighlight() {
row.Cols[1].UnHighlight()
if config.GetSwitchVal("fullRowCursor") {
for _, w := range row.Cols {
w.UnHighlight()
}
}
}
type RowBg struct {
*ui.Par
}
func NewRowBg() *RowBg {
bg := ui.NewPar("")
bg.Height = 1
bg.Border = false
bg.Bg = ui.ThemeAttr("par.text.bg")
return &RowBg{bg}
}
func (w *RowBg) Highlight() { w.Bg = ui.ThemeAttr("par.text.fg") }
func (w *RowBg) UnHighlight() { w.Bg = ui.ThemeAttr("par.text.bg") }

View File

@@ -0,0 +1,48 @@
package compact
import (
"fmt"
"strconv"
"github.com/bcicen/ctop/cwidgets"
ui "github.com/gizak/termui"
)
func (row *Compact) SetNet(rx int64, tx int64) {
label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat(rx), cwidgets.ByteFormat(tx))
row.Net.Set(label)
}
func (row *Compact) SetIO(read int64, write int64) {
label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat(read), cwidgets.ByteFormat(write))
row.IO.Set(label)
}
func (row *Compact) SetPids(val int) {
label := strconv.Itoa(val)
row.Pids.Set(label)
}
func (row *Compact) SetCPU(val int) {
row.Cpu.BarColor = colorScale(val)
row.Cpu.Label = fmt.Sprintf("%s%%", strconv.Itoa(val))
if val < 5 {
val = 5
row.Cpu.BarColor = ui.ThemeAttr("gauge.bar.bg")
}
if val > 100 {
val = 100
}
row.Cpu.Percent = val
}
func (row *Compact) SetMem(val int64, limit int64, percent int) {
row.Mem.Label = fmt.Sprintf("%s / %s", cwidgets.ByteFormat(val), cwidgets.ByteFormat(limit))
if percent < 5 {
percent = 5
row.Mem.BarColor = ui.ColorBlack
} else {
row.Mem.BarColor = ui.ThemeAttr("gauge.bar.bg")
}
row.Mem.Percent = percent
}

View File

@@ -1,14 +1,12 @@
package compact package compact
import ( import (
"github.com/bcicen/ctop/models"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
const ( const (
mark = "◉" mark = string('\u25C9')
healthMark = "✚" healthMark = string('\u207A')
vBar = string('\u25AE') + string('\u25AE') vBar = string('\u25AE') + string('\u25AE')
) )
@@ -19,46 +17,29 @@ type Status struct {
health []ui.Cell health []ui.Cell
} }
func NewStatus() CompactCol { func NewStatus() *Status {
s := &Status{ s := &Status{Block: ui.NewBlock()}
Block: ui.NewBlock(),
health: []ui.Cell{{Ch: ' '}},
}
s.Height = 1 s.Height = 1
s.Border = false s.Border = false
s.setState("") s.Set("")
return s return s
} }
func (s *Status) Buffer() ui.Buffer { func (s *Status) Buffer() ui.Buffer {
buf := s.Block.Buffer() buf := s.Block.Buffer()
x := 0 x := 0
for _, c := range s.health { for _, c := range s.status {
buf.Set(s.InnerX()+x, s.InnerY(), c) buf.Set(s.InnerX()+x, s.InnerY(), c)
x += c.Width() x += c.Width()
} }
x += 1 for _, c := range s.health {
for _, c := range s.status {
buf.Set(s.InnerX()+x, s.InnerY(), c) buf.Set(s.InnerX()+x, s.InnerY(), c)
x += c.Width() x += c.Width()
} }
return buf return buf
} }
func (s *Status) SetMeta(m models.Meta) { func (s *Status) Set(val string) {
s.setState(m.Get("state"))
s.setHealth(m.Get("health"))
}
// Status implements CompactCol
func (s *Status) Reset() {}
func (s *Status) SetMetrics(models.Metrics) {}
func (s *Status) Highlight() {}
func (s *Status) UnHighlight() {}
func (s *Status) Header() string { return "" }
func (s *Status) FixedWidth() int { return 3 }
func (s *Status) setState(val string) {
// defaults // defaults
text := mark text := mark
color := ui.ColorDefault color := ui.ColorDefault
@@ -72,25 +53,31 @@ func (s *Status) setState(val string) {
text = vBar text = vBar
} }
s.status = ui.TextCells(text, color, ui.ColorDefault) var cells []ui.Cell
for _, ch := range text {
cells = append(cells, ui.Cell{Ch: ch, Fg: color})
}
s.status = cells
} }
func (s *Status) setHealth(val string) { func (s *Status) SetHealth(val string) {
if val == "" {
return
}
color := ui.ColorDefault color := ui.ColorDefault
mark := healthMark
switch val { switch val {
case "": case "healthy", "Succeeded":
return
case "healthy":
color = ui.ThemeAttr("status.ok") color = ui.ThemeAttr("status.ok")
case "unhealthy": case "unhealthy", "Failed", "Unknown":
color = ui.ThemeAttr("status.danger") color = ui.ThemeAttr("status.danger")
case "starting": case "starting", "Pending", "Running":
color = ui.ThemeAttr("status.warn") color = ui.ThemeAttr("status.warn")
default:
log.Warningf("unknown health state string: \"%v\"", val)
} }
s.health = ui.TextCells(mark, color, ui.ColorDefault) var cells []ui.Cell
for _, ch := range healthMark {
cells = append(cells, ui.Cell{Ch: ch, Fg: color})
}
s.health = cells
} }

View File

@@ -1,111 +1,19 @@
package compact package compact
import ( import (
"fmt"
"github.com/bcicen/ctop/cwidgets"
"github.com/bcicen/ctop/models"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
// Column that shows container's meta property i.e. name, id, image tc.
type MetaCol struct {
*TextCol
metaName string
}
func (w *MetaCol) SetMeta(m models.Meta) {
w.setText(m.Get(w.metaName))
}
func NewNameCol() CompactCol {
c := &MetaCol{NewTextCol("NAME"), "name"}
c.fWidth = 30
return c
}
func NewCIDCol() CompactCol {
c := &MetaCol{NewTextCol("CID"), "id"}
c.fWidth = 12
return c
}
func NewImageCol() CompactCol {
return &MetaCol{NewTextCol("IMAGE"), "image"}
}
func NewPortsCol() CompactCol {
return &MetaCol{NewTextCol("PORTS"), "ports"}
}
func NewIpsCol() CompactCol {
return &MetaCol{NewTextCol("IPs"), "IPs"}
}
func NewCreatedCol() CompactCol {
c := &MetaCol{NewTextCol("CREATED"), "created"}
c.fWidth = 19 // Year will be stripped e.g. "Thu Nov 26 07:44:03" without 2020 at end
return c
}
type NetCol struct {
*TextCol
}
func NewNetCol() CompactCol {
return &NetCol{NewTextCol("NET RX/TX")}
}
func (w *NetCol) SetMetrics(m models.Metrics) {
label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat64Short(m.NetRx), cwidgets.ByteFormat64Short(m.NetTx))
w.setText(label)
}
type IOCol struct {
*TextCol
}
func NewIOCol() CompactCol {
return &IOCol{NewTextCol("IO R/W")}
}
func (w *IOCol) SetMetrics(m models.Metrics) {
label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat64Short(m.IOBytesRead), cwidgets.ByteFormat64Short(m.IOBytesWrite))
w.setText(label)
}
type PIDCol struct {
*TextCol
}
func NewPIDCol() CompactCol {
w := &PIDCol{NewTextCol("PIDS")}
w.fWidth = 4
return w
}
func (w *PIDCol) SetMetrics(m models.Metrics) {
w.setText(fmt.Sprintf("%d", m.Pids))
}
type TextCol struct { type TextCol struct {
*ui.Par *ui.Par
header string
fWidth int
} }
func NewTextCol(header string) *TextCol { func NewTextCol(s string) *TextCol {
p := ui.NewPar("-") p := ui.NewPar(s)
p.Border = false p.Border = false
p.Height = 1 p.Height = 1
p.Width = 20 p.Width = 20
return &TextCol{p}
return &TextCol{
Par: p,
header: header,
fWidth: 0,
}
} }
func (w *TextCol) Highlight() { func (w *TextCol) Highlight() {
@@ -120,16 +28,10 @@ func (w *TextCol) UnHighlight() {
w.TextBgColor = ui.ThemeAttr("par.text.bg") w.TextBgColor = ui.ThemeAttr("par.text.bg")
} }
// TextCol implements CompactCol func (w *TextCol) Reset() {
func (w *TextCol) Reset() { w.setText("-") } w.Text = "-"
func (w *TextCol) SetMeta(models.Meta) {} }
func (w *TextCol) SetMetrics(models.Metrics) {}
func (w *TextCol) Header() string { return w.header }
func (w *TextCol) FixedWidth() int { return w.fWidth }
func (w *TextCol) setText(s string) { func (w *TextCol) Set(s string) {
if w.fWidth > 0 && len(s) > w.fWidth {
s = s[0:w.fWidth]
}
w.Text = s w.Text = s
} }

View File

@@ -10,6 +10,31 @@ import (
const colSpacing = 1 const colSpacing = 1
// per-column width. 0 == auto width
var colWidths = []int{
3, // status
0, // name
0, // cid
0, // cpu
0, // memory
0, // net
0, // io
4, // pids
}
// Calculate per-column width, given total width
func calcWidth(width int) int {
spacing := colSpacing * len(colWidths)
var staticCols int
for _, w := range colWidths {
width -= w
if w == 0 {
staticCols++
}
}
return (width - spacing) / staticCols
}
func centerParText(p *ui.Par) { func centerParText(p *ui.Par) {
var text string var text string
var padding string var padding string

View File

@@ -8,14 +8,6 @@ import (
var log = logging.Init() var log = logging.Init()
type WidgetUpdater interface { type WidgetUpdater interface {
SetMeta(models.Meta) SetMeta(string, string)
SetMetrics(models.Metrics) SetMetrics(models.Metrics)
} }
type NullWidgetUpdater struct{}
// NullWidgetUpdater implements WidgetUpdater
func (wu NullWidgetUpdater) SetMeta(models.Meta) {}
// NullWidgetUpdater implements WidgetUpdater
func (wu NullWidgetUpdater) SetMetrics(models.Metrics) {}

View File

@@ -3,7 +3,6 @@ package single
import ( import (
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
"regexp" "regexp"
"strings"
) )
var envPattern = regexp.MustCompile(`(?P<KEY>[^=]+)=(?P<VALUJE>.*)`) var envPattern = regexp.MustCompile(`(?P<KEY>[^=]+)=(?P<VALUJE>.*)`)
@@ -24,16 +23,14 @@ func NewEnv() *Env {
return i return i
} }
func (w *Env) Set(allEnvs string) { func (w *Env) Set(k, v string) {
envs := strings.Split(allEnvs, ";") match := envPattern.FindStringSubmatch(v)
key := match[1]
value := match[2]
w.data[key] = value
w.Rows = [][]string{} w.Rows = [][]string{}
for _, env := range envs { w.Rows = append(w.Rows, mkInfoRows(key, value)...)
match := envPattern.FindStringSubmatch(env)
key := match[1]
value := match[2]
w.data[key] = value
w.Rows = append(w.Rows, mkInfoRows(key, value)...)
}
w.Height = len(w.Rows) + 2 w.Height = len(w.Rows) + 2
} }

View File

@@ -13,13 +13,14 @@ type Info struct {
data map[string]string data map[string]string
} }
func NewInfo() *Info { func NewInfo(id string) *Info {
p := ui.NewTable() p := ui.NewTable()
p.Height = 4 p.Height = 4
p.Width = colWidth[0] p.Width = colWidth[0]
p.FgColor = ui.ThemeAttr("par.text.fg") p.FgColor = ui.ThemeAttr("par.text.fg")
p.Separator = false p.Separator = false
i := &Info{p, make(map[string]string)} i := &Info{p, make(map[string]string)}
i.Set("id", id)
return i return i
} }

View File

@@ -42,10 +42,10 @@ func (w *IO) Update(read int64, write int64) {
var rate string var rate string
w.readHist.Append(int(read)) w.readHist.Append(int(read))
rate = strings.ToLower(cwidgets.ByteFormatShort(w.readHist.Val)) rate = strings.ToLower(cwidgets.ByteFormatInt(w.readHist.Val))
w.Lines[0].Title = fmt.Sprintf("read [%s/s]", rate) w.Lines[0].Title = fmt.Sprintf("read [%s/s]", rate)
w.writeHist.Append(int(write)) w.writeHist.Append(int(write))
rate = strings.ToLower(cwidgets.ByteFormatShort(w.writeHist.Val)) rate = strings.ToLower(cwidgets.ByteFormatInt(w.writeHist.Val))
w.Lines[1].Title = fmt.Sprintf("write [%s/s]", rate) w.Lines[1].Title = fmt.Sprintf("write [%s/s]", rate)
} }

View File

@@ -23,9 +23,12 @@ type Single struct {
Width int Width int
} }
func NewSingle() *Single { func NewSingle(id string) *Single {
if len(id) > 12 {
id = id[:12]
}
return &Single{ return &Single{
Info: NewInfo(), Info: NewInfo(id),
Net: NewNet(), Net: NewNet(),
Cpu: NewCpu(), Cpu: NewCpu(),
Mem: NewMem(), Mem: NewMem(),
@@ -52,13 +55,11 @@ func (e *Single) Down() {
} }
func (e *Single) SetWidth(w int) { e.Width = w } func (e *Single) SetWidth(w int) { e.Width = w }
func (e *Single) SetMeta(m models.Meta) { func (e *Single) SetMeta(k, v string) {
for k, v := range m { if k == "[ENV-VAR]" {
if k == "[ENV-VAR]" { e.Env.Set(k, v)
e.Env.Set(v) } else {
} else { e.Info.Set(k, v)
e.Info.Set(k, v)
}
} }
} }
@@ -69,7 +70,7 @@ func (e *Single) SetMetrics(m models.Metrics) {
e.IO.Update(m.IOBytesRead, m.IOBytesWrite) e.IO.Update(m.IOBytesRead, m.IOBytesWrite)
} }
// GetHeight returns total column height // Return total column height
func (e *Single) GetHeight() (h int) { func (e *Single) GetHeight() (h int) {
h += e.Info.Height h += e.Info.Height
h += e.Net.Height h += e.Net.Height

View File

@@ -70,7 +70,7 @@ func newMemChart() *ui.MBarChart {
mbar.BarColor[1] = ui.ColorBlack mbar.BarColor[1] = ui.ColorBlack
mbar.NumColor[1] = ui.ColorBlack mbar.NumColor[1] = ui.ColorBlack
mbar.NumFmt = cwidgets.ByteFormatShort mbar.NumFmt = cwidgets.ByteFormatInt
//mbar.ShowScale = true //mbar.ShowScale = true
return mbar return mbar
} }
@@ -78,6 +78,6 @@ func newMemChart() *ui.MBarChart {
func (w *Mem) Update(val int, limit int) { func (w *Mem) Update(val int, limit int) {
w.valHist.Append(val) w.valHist.Append(val)
w.limitHist.Append(limit - val) w.limitHist.Append(limit - val)
w.InnerLabel.Text = fmt.Sprintf("%v / %v", cwidgets.ByteFormatShort(val), cwidgets.ByteFormatShort(limit)) w.InnerLabel.Text = fmt.Sprintf("%v / %v", cwidgets.ByteFormatInt(val), cwidgets.ByteFormatInt(limit))
//w.Data[0] = w.hist.data //w.Data[0] = w.hist.data
} }

View File

@@ -42,10 +42,10 @@ func (w *Net) Update(rx int64, tx int64) {
var rate string var rate string
w.rxHist.Append(int(rx)) w.rxHist.Append(int(rx))
rate = strings.ToLower(cwidgets.ByteFormat(w.rxHist.Val)) rate = strings.ToLower(cwidgets.ByteFormatInt(w.rxHist.Val))
w.Lines[0].Title = fmt.Sprintf("RX [%s/s]", rate) w.Lines[0].Title = fmt.Sprintf("RX [%s/s]", rate)
w.txHist.Append(int(tx)) w.txHist.Append(int(tx))
rate = strings.ToLower(cwidgets.ByteFormat(w.txHist.Val)) rate = strings.ToLower(cwidgets.ByteFormatInt(w.txHist.Val))
w.Lines[1].Title = fmt.Sprintf("TX [%s/s]", rate) w.Lines[1].Title = fmt.Sprintf("TX [%s/s]", rate)
} }

View File

@@ -1,74 +1,53 @@
package cwidgets package cwidgets
import ( import (
"fmt"
"strconv" "strconv"
) )
const ( const (
// byte ratio constants kb = 1024
_ = iota mb = kb * 1024
kib float64 = 1 << (10 * iota) gb = mb * 1024
mib tb = gb * 1024
gib
tib
pib
) )
var ( // convenience method
units = []float64{ func ByteFormatInt(n int) string {
1, return ByteFormat(int64(n))
kib,
mib,
gib,
tib,
pib,
}
// short, full unit labels
labels = [][2]string{
[2]string{"B", "B"},
[2]string{"K", "KiB"},
[2]string{"M", "MiB"},
[2]string{"G", "GiB"},
[2]string{"T", "TiB"},
[2]string{"P", "PiB"},
}
)
// convenience methods
func ByteFormat(n int) string { return byteFormat(float64(n), false) }
func ByteFormatShort(n int) string { return byteFormat(float64(n), true) }
func ByteFormat64(n int64) string { return byteFormat(float64(n), false) }
func ByteFormat64Short(n int64) string { return byteFormat(float64(n), true) }
func byteFormat(n float64, short bool) string {
i := len(units) - 1
for i > 0 {
if n >= units[i] {
n /= units[i]
break
}
i--
}
if short {
return unpadFloat(n, 0) + labels[i][0]
}
return unpadFloat(n, 2) + labels[i][1]
} }
func unpadFloat(f float64, maxp int) string { func ByteFormat(n int64) string {
return strconv.FormatFloat(f, 'f', getPrecision(f, maxp), 64) if n < kb {
return fmt.Sprintf("%sB", strconv.FormatInt(n, 10))
}
if n < mb {
n = n / kb
return fmt.Sprintf("%sK", strconv.FormatInt(n, 10))
}
if n < gb {
n = n / mb
return fmt.Sprintf("%sM", strconv.FormatInt(n, 10))
}
if n < tb {
nf := float64(n) / gb
return fmt.Sprintf("%sG", unpadFloat(nf))
}
nf := float64(n) / tb
return fmt.Sprintf("%sT", unpadFloat(nf))
} }
func getPrecision(f float64, maxp int) int { func unpadFloat(f float64) string {
return strconv.FormatFloat(f, 'f', getPrecision(f), 64)
}
func getPrecision(f float64) int {
frac := int((f - float64(int(f))) * 100) frac := int((f - float64(int(f))) * 100)
if frac == 0 || maxp == 0 { if frac == 0 {
return 0 return 0
} }
if frac%10 == 0 || maxp < 2 { if frac%10 == 0 {
return 1 return 1
} }
return maxp return 2 // default precision
} }

View File

@@ -12,10 +12,6 @@ import (
var mstats = &runtime.MemStats{} var mstats = &runtime.MemStats{}
func logEvent(e ui.Event) { func logEvent(e ui.Event) {
// skip timer events e.g. /timer/1s
if e.From == "timer" {
return
}
var s string var s string
s += fmt.Sprintf("Type=%s", quote(e.Type)) s += fmt.Sprintf("Type=%s", quote(e.Type))
s += fmt.Sprintf(" Path=%s", quote(e.Path)) s += fmt.Sprintf(" Path=%s", quote(e.Path))

54
go.mod
View File

@@ -1,21 +1,61 @@
module github.com/bcicen/ctop module github.com/bcicen/ctop
require ( require (
github.com/BurntSushi/toml v0.3.1 github.com/Azure/go-ansiterm v0.0.0-20160622173216-fa152c58bc15 // indirect
github.com/BurntSushi/toml v0.3.0
github.com/Microsoft/go-winio v0.3.8 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/Sirupsen/logrus v0.0.0-20150423025312-26709e271410 // indirect
github.com/c9s/goprocinfo v0.0.0-20170609001544-b34328d6e0cd github.com/c9s/goprocinfo v0.0.0-20170609001544-b34328d6e0cd
github.com/fsouza/go-dockerclient v1.6.6 github.com/coreos/go-systemd v0.0.0-20151104194251-b4a58d95188d // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/docker v0.0.0-20170502054910-90d35abf7b35 // indirect
github.com/docker/go-connections v0.0.0-20170301234100-a2afab980204 // indirect
github.com/docker/go-units v0.3.2 // indirect
github.com/fsouza/go-dockerclient v0.0.0-20170307141636-318513eb1ab2
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gizak/termui v2.3.0+incompatible github.com/gizak/termui v2.3.0+incompatible
github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55 // indirect
github.com/gogo/protobuf v1.1.1 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
github.com/googleapis/gnostic v0.2.0 // indirect
github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f // indirect
github.com/hashicorp/go-cleanhttp v0.0.0-20170211013415-3573b8b52aa7 // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/jgautheron/codename-generator v0.0.0-20150829203204-16d037c7cc3c github.com/jgautheron/codename-generator v0.0.0-20150829203204-16d037c7cc3c
github.com/json-iterator/go v1.1.5 // indirect
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c // indirect github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c // indirect
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/nsf/termbox-go v0.0.0-20180303152453-e2050e41c884 github.com/nsf/termbox-go v0.0.0-20180303152453-e2050e41c884
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473 github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473
github.com/opencontainers/runc v1.0.0-rc92 github.com/opencontainers/runc v0.1.1
github.com/pkg/errors v0.9.1 github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/stretchr/testify v1.4.0 github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/seccomp/libseccomp-golang v0.0.0-20150813023252-1b506fc7c24e // indirect
github.com/spf13/pflag v1.0.3 // indirect
github.com/stretchr/testify v1.2.2 // indirect
github.com/syndtr/gocapability v0.0.0-20150716010906-2c00daeb6c3b // indirect
github.com/vishvananda/netlink v0.0.0-20150820014904-1e2e08e8a2dc // indirect
github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc // indirect
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85 // indirect
golang.org/x/oauth2 v0.0.0-20181128211412-28207608b838 // indirect
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 // indirect
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
google.golang.org/appengine v1.3.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.2.2 // indirect
k8s.io/api v0.0.0-20181130031204-d04500c8c3dd
k8s.io/apimachinery v0.0.0-20181201231028-18a5ff3097b4
k8s.io/client-go v9.0.0+incompatible
k8s.io/klog v0.1.0 // indirect
k8s.io/metrics v0.0.0-20181121073115-d8618695b08f
sigs.k8s.io/yaml v1.1.0 // indirect
) )
replace github.com/gizak/termui => github.com/bcicen/termui v0.0.0-20180326052246-4eb80249d3f5 replace github.com/gizak/termui => github.com/bcicen/termui v0.0.0-20180326052246-4eb80249d3f5
go 1.15

321
go.sum
View File

@@ -1,321 +0,0 @@
bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898 h1:SC+c6A1qTFstO9qmB86mPV2IpYme/2ZoEQ0hrP+wo+Q=
bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
github.com/Microsoft/go-winio v0.4.15-0.20200113171025-3fe6c5262873 h1:93nQ7k53GjoMQ07HVP8g6Zj1fQZDDj7Xy2VkNNtvX8o=
github.com/Microsoft/go-winio v0.4.15-0.20200113171025-3fe6c5262873/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
github.com/Microsoft/hcsshim v0.8.9 h1:VrfodqvztU8YSOvygU+DN1BGaSGxmrNfqOv5oOuX2Bk=
github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8=
github.com/bcicen/termui v0.0.0-20180326052246-4eb80249d3f5 h1:2pI3ZsoefWIi++8EqmANoC7Px/v2lRwnleVUcCuFgLg=
github.com/bcicen/termui v0.0.0-20180326052246-4eb80249d3f5/go.mod h1:yIA9ITWZD1p4/DvCQ44xvhyVb9XEUlVnY1rzGSHwbiM=
github.com/c9s/goprocinfo v0.0.0-20170609001544-b34328d6e0cd h1:xqaBnULC8wEnQpRDXAsDgXkU/STqoluz1REOoegSfNU=
github.com/c9s/goprocinfo v0.0.0-20170609001544-b34328d6e0cd/go.mod h1:uEyr4WpAH4hio6LFriaPkL938XnrvLpNPmQHBdrmbIE=
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/checkpoint-restore/go-criu/v4 v4.1.0 h1:WW2B2uxx9KWF6bGlHqhm8Okiafwwx7Y2kcpn8lCpjgo=
github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775 h1:cHzBGGVew0ezFsq2grfy2RsB8hO/eNyBgOLHBCqfR1U=
github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f h1:WBZRG4aNOuI15bLRrCgN8fCq8E5Xuty6jGbmSNEvSsU=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f h1:tSNMc+rJDfmYntojat8lljbt1mgKNpTxUZJsSzJ9Y1s=
github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
github.com/containerd/console v1.0.0 h1:fU3UuQapBs+zLJu82NhR11Rif1ny2zfMMAyPJzSN5tQ=
github.com/containerd/console v1.0.0/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE=
github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/containerd v1.3.4 h1:3o0smo5SKY7H6AJCmJhsnCjR2/V2T8VmiHt7seN2/kI=
github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/containerd/continuity v0.0.0-20200413184840-d3ef23f19fbb h1:nXPkFq8X1a9ycY3GYQpFNxHh3j2JgY7zDZfq2EXMIzk=
github.com/containerd/continuity v0.0.0-20200413184840-d3ef23f19fbb/go.mod h1:Dq467ZllaHgAtVp4p1xUQWBrFXR9s/wyoTpG8zOJGkY=
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448 h1:PUD50EuOMkXVcpBIA/R95d56duJR9VxhwncsFbNnxW4=
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3 h1:esQOJREg8nw8aXj6uCN5dfW5cKUBiEJ/+nni1Q/D/sw=
github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de h1:dlfGmNcE3jDAecLqwKPMNX6nk2qh1c1Vg1/YTzpOOF4=
github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd h1:JNn81o/xG+8NEo3bC/vx9pbi/g2WI8mtP2/nXzu297Y=
github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.1.0 h1:kq/SbG2BCKLkDKkjQf5OWwKWUKj1lgs3lFI4PxnR5lg=
github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg=
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v17.12.0-ce-rc1.0.20200505174321-1655290016ac+incompatible h1:ZxJX4ZSNg1LORBsStUojbrLfkrE3Ut122XhzyZnN110=
github.com/docker/docker v17.12.0-ce-rc1.0.20200505174321-1655290016ac+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4 h1:qk/FSDDxo05wdJH28W+p5yivv7LuLYLRXPPD8KQCtZs=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4 h1:rEvIZUSZ3fx39WIi3JkQqQBitGwpELBIYWeBVh6wn+E=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsouza/go-dockerclient v1.6.6 h1:9e3xkBrVkPb81gzYq23i7iDUEd6sx2ooeJA/gnYU6R4=
github.com/fsouza/go-dockerclient v1.6.6/go.mod h1:3/oRIWoe7uT6bwtAayj/EmJmepBjeL4pYvt7ZxC7Rnk=
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e h1:BWhy2j3IXJhjCbC68FptL43tDKIq8FladmaTs3Xs7Z8=
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jgautheron/codename-generator v0.0.0-20150829203204-16d037c7cc3c h1:/hc+TxW4Q1v6aqNPHE5jiaNF2xEK0CzWTgo25RQhQ+U=
github.com/jgautheron/codename-generator v0.0.0-20150829203204-16d037c7cc3c/go.mod h1:FJRkXmPrkHw0WDjB/LXMUhjWJ112Y6JUYnIVBOy8oH8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.2.0 h1:reN85Pxc5larApoH1keMBiu2GWtPqXQ1nc9gx+jOU+E=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c h1:eFzthqtg3W6Pihj3DMTXLAF4f+ge5r5Ie5g6HLIZAF0=
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/moby/sys/mount v0.1.0 h1:Ytx78EatgFKtrqZ0BvJ0UtJE472ZvawVmil6pIfuCCU=
github.com/moby/sys/mount v0.1.0/go.mod h1:FVQFLDRWwyBjDTBNQXDlWnSFREqOo3OKX9aqhmeoo74=
github.com/moby/sys/mountinfo v0.1.0/go.mod h1:w2t2Avltqx8vE7gX5l+QiBKxODu2TX0+Syr3h52Tw4o=
github.com/moby/sys/mountinfo v0.1.3 h1:KIrhRO14+AkwKvG/g2yIpNMOUVZ02xNhOw8KY1WsLOI=
github.com/moby/sys/mountinfo v0.1.3/go.mod h1:w2t2Avltqx8vE7gX5l+QiBKxODu2TX0+Syr3h52Tw4o=
github.com/moby/term v0.0.0-20200429084858-129dac9f73f6 h1:3Y9aosU6S5Bo8GYH0s+t1ej4m30GuUKvQ3c9ZLqdL28=
github.com/moby/term v0.0.0-20200429084858-129dac9f73f6/go.mod h1:or9wGItza1sRcM4Wd3dIv8DsFHYQuFsMHEdxUIlUxms=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mrunalp/fileutils v0.0.0-20200520151820-abd8a0e76976 h1:aZQToFSLH8ejFeSkTc3r3L4dPImcj7Ib/KgmkQqbGGg=
github.com/mrunalp/fileutils v0.0.0-20200520151820-abd8a0e76976/go.mod h1:x8F1gnqOkIEiO4rqoeEEEqQbo7HjGMTvyoq3gej4iT0=
github.com/nsf/termbox-go v0.0.0-20180303152453-e2050e41c884 h1:fcs71SMqqDhUD+PbpIv9xf3EH9F9s6HfiLwr6jKm1VA=
github.com/nsf/termbox-go v0.0.0-20180303152453-e2050e41c884/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473 h1:J1QZwDXgZ4dJD2s19iqR9+U00OWM2kDzbf1O/fmvCWg=
github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runc v1.0.0-rc92 h1:+IczUKCRzDzFDnw99O/PAqrcBBCoRp9xN3cB1SYSNS4=
github.com/opencontainers/runc v1.0.0-rc92/go.mod h1:X1zlU4p7wOlX4+WRCz+hvlRv8phdL7UqbYD+vQwNMmE=
github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/runtime-spec v1.0.3-0.20200728170252-4d89ac9fbff6 h1:NhsM2gc769rVWDqJvapK37r+7+CBXI8xHhnfnt8uQsg=
github.com/opencontainers/runtime-spec v1.0.3-0.20200728170252-4d89ac9fbff6/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/selinux v1.6.0 h1:+bIAS/Za3q5FTwWym4fTB0vObnfCf3G/NC7K6Jx62mY=
github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE=
github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7 h1:hhvfGDVThBnd4kYisSFmYuHYeUhglxcwag7FhVPH9zM=
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/seccomp/libseccomp-golang v0.9.1 h1:NJjM5DNFOs0s3kYE1WUOr6G8V97sdt46rlXTMfXGWBo=
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee h1:GQkkv3XSnxhAMjdq2wLfEnptEVr+2BNvmHizILHn+d4=
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2 h1:b6uOv7YOFK0TYG7HtkIgExQo+2RdLuwRft63jn2HWj8=
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243 h1:R43TdZy32XXSXjJn7M/HhALJ9imq6ztLnChfYJpVDnM=
github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROxz1KyMlKp4uyvL6+kQ7C88=
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI=
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 h1:sIky/MyNRSHTrdxfsiUSS4WIAMvInbeXljJz+jDjeYE=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4 h1:1mMox4TgefDwqluYCv677yNXwlfTkija4owZve/jr78=
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

72
grid.go
View File

@@ -6,44 +6,6 @@ import (
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
func ShowConnError(err error) (exit bool) {
ui.Clear()
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
setErr := func(err error) {
errView.Append(err.Error())
errView.Append("attempting to reconnect...")
ui.Render(errView)
}
HandleKeys("exit", func() {
exit = true
ui.StopLoop()
})
ui.Handle("/timer/1s", func(ui.Event) {
_, err := cursor.RefreshContainers()
if err == nil {
ui.StopLoop()
return
}
setErr(err)
})
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
errView.Resize()
ui.Clear()
ui.Render(errView)
log.Infof("RESIZE")
})
errView.Resize()
setErr(err)
ui.Loop()
return exit
}
func RedrawRows(clr bool) { func RedrawRows(clr bool) {
// reinit body rows // reinit body rows
cGrid.Clear() cGrid.Clear()
@@ -71,6 +33,7 @@ func RedrawRows(clr bool) {
} }
cGrid.Align() cGrid.Align()
ui.Render(cGrid) ui.Render(cGrid)
} }
func SingleView() MenuFn { func SingleView() MenuFn {
@@ -83,7 +46,7 @@ func SingleView() MenuFn {
ui.DefaultEvtStream.ResetHandlers() ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers() defer ui.DefaultEvtStream.ResetHandlers()
ex := single.NewSingle() ex := single.NewSingle(c.Id)
c.SetUpdater(ex) c.SetUpdater(ex)
ex.Align() ex.Align()
@@ -105,21 +68,16 @@ func SingleView() MenuFn {
return nil return nil
} }
func RefreshDisplay() error { func RefreshDisplay() {
// skip display refresh during scroll // skip display refresh during scroll
if !cursor.isScrolling { if !cursor.isScrolling {
needsClear, err := cursor.RefreshContainers() needsClear := cursor.RefreshContainers()
if err != nil {
return err
}
RedrawRows(needsClear) RedrawRows(needsClear)
} }
return nil
} }
func Display() bool { func Display() bool {
var menu MenuFn var menu MenuFn
var connErr error
cGrid.SetWidth(ui.TermWidth()) cGrid.SetWidth(ui.TermWidth())
ui.DefaultEvtStream.Hook(logEvent) ui.DefaultEvtStream.Hook(logEvent)
@@ -158,20 +116,13 @@ func Display() bool {
menu = LogMenu menu = LogMenu
ui.StopLoop() ui.StopLoop()
}) })
ui.Handle("/sys/kbd/e", func(ui.Event) {
menu = ExecShell
ui.StopLoop()
})
ui.Handle("/sys/kbd/o", func(ui.Event) { ui.Handle("/sys/kbd/o", func(ui.Event) {
menu = SingleView menu = SingleView
ui.StopLoop() ui.StopLoop()
}) })
ui.Handle("/sys/kbd/a", func(ui.Event) { ui.Handle("/sys/kbd/a", func(ui.Event) {
config.Toggle("allContainers") config.Toggle("allContainers")
connErr = RefreshDisplay() RefreshDisplay()
if connErr != nil {
ui.StopLoop()
}
}) })
ui.Handle("/sys/kbd/D", func(ui.Event) { ui.Handle("/sys/kbd/D", func(ui.Event) {
dumpContainer(cursor.Selected()) dumpContainer(cursor.Selected())
@@ -191,10 +142,6 @@ func Display() bool {
menu = SortMenu menu = SortMenu
ui.StopLoop() ui.StopLoop()
}) })
ui.Handle("/sys/kbd/c", func(ui.Event) {
menu = ColumnsMenu
ui.StopLoop()
})
ui.Handle("/sys/kbd/S", func(ui.Event) { ui.Handle("/sys/kbd/S", func(ui.Event) {
path, err := config.Write() path, err := config.Write()
if err == nil { if err == nil {
@@ -209,10 +156,7 @@ func Display() bool {
if log.StatusQueued() { if log.StatusQueued() {
ui.StopLoop() ui.StopLoop()
} }
connErr = RefreshDisplay() RefreshDisplay()
if connErr != nil {
ui.StopLoop()
}
}) })
ui.Handle("/sys/wnd/resize", func(e ui.Event) { ui.Handle("/sys/wnd/resize", func(e ui.Event) {
@@ -226,10 +170,6 @@ func Display() bool {
ui.Loop() ui.Loop()
if connErr != nil {
return ShowConnError(connErr)
}
if log.StatusQueued() { if log.StatusQueued() {
for sm := range log.FlushStatus() { for sm := range log.FlushStatus() {
if sm.IsError { if sm.IsError {

View File

@@ -29,7 +29,6 @@ type statusMsg struct {
type CTopLogger struct { type CTopLogger struct {
*logging.Logger *logging.Logger
backend *logging.MemoryBackend backend *logging.MemoryBackend
logFile *os.File
sLog []statusMsg sLog []statusMsg
} }
@@ -59,37 +58,18 @@ func Init() *CTopLogger {
Log = &CTopLogger{ Log = &CTopLogger{
logging.MustGetLogger("ctop"), logging.MustGetLogger("ctop"),
logging.NewMemoryBackend(size), logging.NewMemoryBackend(size),
nil,
[]statusMsg{}, []statusMsg{},
} }
debugMode := debugMode() if debugMode() {
if debugMode {
level = logging.DEBUG level = logging.DEBUG
StartServer()
} }
backendLvl := logging.AddModuleLevel(Log.backend) backendLvl := logging.AddModuleLevel(Log.backend)
backendLvl.SetLevel(level, "") backendLvl.SetLevel(level, "")
logFilePath := debugModeFile() logging.SetBackend(backendLvl)
if logFilePath == "" {
logging.SetBackend(backendLvl)
} else {
logFile, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
logging.SetBackend(backendLvl)
Log.Error("Unable to create log file: %s", err.Error())
} else {
backendFile := logging.NewLogBackend(logFile, "", 0)
backendFileLvl := logging.AddModuleLevel(backendFile)
backendFileLvl.SetLevel(level, "")
logging.SetBackend(backendLvl, backendFileLvl)
Log.logFile = logFile
}
}
if debugMode {
StartServer()
}
Log.Notice("logger initialized") Log.Notice("logger initialized")
} }
return Log return Log
@@ -122,12 +102,8 @@ func (log *CTopLogger) tail() chan string {
func (log *CTopLogger) Exit() { func (log *CTopLogger) Exit() {
exited = true exited = true
if log.logFile != nil {
_ = log.logFile.Close()
}
StopServer() StopServer()
} }
func debugMode() bool { return os.Getenv("CTOP_DEBUG") == "1" } func debugMode() bool { return os.Getenv("CTOP_DEBUG") == "1" }
func debugModeTCP() bool { return os.Getenv("CTOP_DEBUG_TCP") == "1" } func debugModeTCP() bool { return os.Getenv("CTOP_DEBUG_TCP") == "1" }
func debugModeFile() string { return os.Getenv("CTOP_DEBUG_FILE") }

53
main.go
View File

@@ -22,31 +22,31 @@ var (
version = "dev-build" version = "dev-build"
goVersion = runtime.Version() goVersion = runtime.Version()
log *logging.CTopLogger log *logging.CTopLogger
cursor *GridCursor cursor *GridCursor
cGrid *compact.CompactGrid cGrid *compact.CompactGrid
header *widgets.CTopHeader header *widgets.CTopHeader
status *widgets.StatusLine status *widgets.StatusLine
errView *widgets.ErrorView
versionStr = fmt.Sprintf("ctop version %v, build %v %v", version, build, goVersion) versionStr = fmt.Sprintf("ctop version %v, build %v %v", version, build, goVersion)
fs = flag.NewFlagSet("ctop", flag.ExitOnError)
versionFlag = fs.Bool("version", false, "output version information and exit")
helpFlag = fs.Bool("h", false, "display this help dialog")
filterFlag = fs.String("f", "", "filter containers")
activeOnlyFlag = fs.Bool("a", false, "show active containers only")
sortFieldFlag = fs.String("s", "", "select container sort field")
reverseSortFlag = fs.Bool("r", false, "reverse container sort order")
invertFlag = fs.Bool("i", false, "invert default colors")
scaleCpu = fs.Bool("scale-cpu", false, "show cpu as % of system total")
connectorFlag = fs.String("connector", "docker", "container connector to use")
namespaceFlag = fs.String("n", "default", "Kubernetes namespace for monitoring")
) )
func main() { func main() {
defer panicExit() defer panicExit()
// parse command line arguments fs.Parse(os.Args[1:])
var (
versionFlag = flag.Bool("v", false, "output version information and exit")
helpFlag = flag.Bool("h", false, "display this help dialog")
filterFlag = flag.String("f", "", "filter containers")
activeOnlyFlag = flag.Bool("a", false, "show active containers only")
sortFieldFlag = flag.String("s", "", "select container sort field")
reverseSortFlag = flag.Bool("r", false, "reverse container sort order")
invertFlag = flag.Bool("i", false, "invert default colors")
connectorFlag = flag.String("connector", "docker", "container connector to use")
)
flag.Parse()
if *versionFlag { if *versionFlag {
fmt.Println(versionStr) fmt.Println(versionStr)
@@ -63,9 +63,7 @@ func main() {
// init global config and read config file if exists // init global config and read config file if exists
config.Init() config.Init()
if err := config.Read(); err != nil { config.Read()
log.Warningf("reading config: %s", err)
}
// override default config values with command line flags // override default config values with command line flags
if *filterFlag != "" { if *filterFlag != "" {
@@ -85,10 +83,15 @@ func main() {
config.Toggle("sortReversed") config.Toggle("sortReversed")
} }
if *scaleCpu {
config.Toggle("scaleCpu")
}
// init ui // init ui
if *invertFlag { if *invertFlag {
InvertColorMap() InvertColorMap()
} }
config.Update("namespace", *namespaceFlag)
ui.ColorMap = ColorMap // override default colormap ui.ColorMap = ColorMap // override default colormap
if err := ui.Init(); err != nil { if err := ui.Init(); err != nil {
panic(err) panic(err)
@@ -97,15 +100,14 @@ func main() {
defer Shutdown() defer Shutdown()
// init grid, cursor, header // init grid, cursor, header
cSuper, err := connector.ByName(*connectorFlag) conn, err := connector.ByName(*connectorFlag)
if err != nil { if err != nil {
panic(err) panic(err)
} }
cursor = &GridCursor{cSuper: cSuper} cursor = &GridCursor{cSource: conn}
cGrid = compact.NewCompactGrid() cGrid = compact.NewCompactGrid()
header = widgets.NewCTopHeader() header = widgets.NewCTopHeader()
status = widgets.NewStatusLine() status = widgets.NewStatusLine()
errView = widgets.NewErrorView()
for { for {
exit := Display() exit := Display()
@@ -134,7 +136,6 @@ func validSort(s string) {
func panicExit() { func panicExit() {
if r := recover(); r != nil { if r := recover(); r != nil {
Shutdown() Shutdown()
panic(r)
fmt.Printf("error: %s\n", r) fmt.Printf("error: %s\n", r)
os.Exit(1) os.Exit(1)
} }
@@ -149,7 +150,7 @@ options:
func printHelp() { func printHelp() {
fmt.Println(helpMsg) fmt.Println(helpMsg)
flag.PrintDefaults() fs.PrintDefaults()
fmt.Printf("\navailable connectors: ") fmt.Printf("\navailable connectors: ")
fmt.Println(strings.Join(connector.Enabled(), ", ")) fmt.Println(strings.Join(connector.Enabled(), ", "))
} }

227
menus.go
View File

@@ -2,7 +2,6 @@ package main
import ( import (
"fmt" "fmt"
"strings"
"time" "time"
"github.com/bcicen/ctop/config" "github.com/bcicen/ctop/config"
@@ -26,8 +25,6 @@ var helpDialog = []menu.Item{
{"[r] - reverse container sort order", ""}, {"[r] - reverse container sort order", ""},
{"[o] - open single view", ""}, {"[o] - open single view", ""},
{"[l] - view container logs ([t] to toggle timestamp when open)", ""}, {"[l] - view container logs ([t] to toggle timestamp when open)", ""},
{"[e] - exec shell", ""},
{"[c] - configure columns", ""},
{"[S] - save current configuration to file", ""}, {"[S] - save current configuration to file", ""},
{"[q] - exit ctop", ""}, {"[q] - exit ctop", ""},
} }
@@ -106,90 +103,7 @@ func SortMenu() MenuFn {
HandleKeys("exit", ui.StopLoop) HandleKeys("exit", ui.StopLoop)
ui.Handle("/sys/kbd/<enter>", func(ui.Event) { ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
config.Update("sortField", m.SelectedValue()) config.Update("sortField", m.SelectedItem().Val)
ui.StopLoop()
})
ui.Render(m)
ui.Loop()
return nil
}
func ColumnsMenu() MenuFn {
const (
enabledStr = "[X]"
disabledStr = "[ ]"
padding = 2
)
ui.Clear()
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
m := menu.NewMenu()
m.Selectable = true
m.SortItems = false
m.BorderLabel = "Columns"
m.SubText = "Re-order: <Page Up> / <Page Down>"
rebuild := func() {
// get padding for right alignment of enabled status
var maxLen int
for _, col := range config.GlobalColumns {
if len(col.Label) > maxLen {
maxLen = len(col.Label)
}
}
maxLen += padding
// rebuild menu items
m.ClearItems()
for _, col := range config.GlobalColumns {
txt := col.Label + strings.Repeat(" ", maxLen-len(col.Label))
if col.Enabled {
txt += enabledStr
} else {
txt += disabledStr
}
m.AddItems(menu.Item{col.Name, txt})
}
}
upFn := func() {
config.ColumnLeft(m.SelectedValue())
m.Up()
rebuild()
}
downFn := func() {
config.ColumnRight(m.SelectedValue())
m.Down()
rebuild()
}
toggleFn := func() {
config.ColumnToggle(m.SelectedValue())
rebuild()
}
rebuild()
HandleKeys("up", m.Up)
HandleKeys("down", m.Down)
HandleKeys("enter", toggleFn)
HandleKeys("pgup", upFn)
HandleKeys("pgdown", downFn)
ui.Handle("/sys/kbd/x", func(ui.Event) { toggleFn() })
ui.Handle("/sys/kbd/<enter>", func(ui.Event) { toggleFn() })
HandleKeys("exit", func() {
cSource, err := cursor.cSuper.Get()
if err == nil {
for _, c := range cSource.All() {
c.RecreateWidgets()
}
}
ui.StopLoop() ui.StopLoop()
}) })
@@ -212,111 +126,55 @@ func ContainerMenu() MenuFn {
m.BorderLabel = "Menu" m.BorderLabel = "Menu"
items := []menu.Item{ items := []menu.Item{
menu.Item{Val: "single", Label: "[o] single view"}, menu.Item{Val: "single", Label: "single view"},
menu.Item{Val: "logs", Label: "[l] log view"}, menu.Item{Val: "logs", Label: "log view"},
} }
if c.Meta["state"] == "running" { if c.Meta["state"] == "running" {
items = append(items, menu.Item{Val: "stop", Label: "[s] stop"}) items = append(items, menu.Item{Val: "stop", Label: "stop"})
items = append(items, menu.Item{Val: "pause", Label: "[p] pause"}) items = append(items, menu.Item{Val: "pause", Label: "pause"})
items = append(items, menu.Item{Val: "restart", Label: "[r] restart"}) items = append(items, menu.Item{Val: "restart", Label: "restart"})
items = append(items, menu.Item{Val: "exec", Label: "[e] exec shell"})
} }
if c.Meta["state"] == "exited" || c.Meta["state"] == "created" { if c.Meta["state"] == "exited" || c.Meta["state"] == "created" {
items = append(items, menu.Item{Val: "start", Label: "[s] start"}) items = append(items, menu.Item{Val: "start", Label: "start"})
items = append(items, menu.Item{Val: "remove", Label: "[R] remove"}) items = append(items, menu.Item{Val: "remove", Label: "remove"})
} }
if c.Meta["state"] == "paused" { if c.Meta["state"] == "paused" {
items = append(items, menu.Item{Val: "unpause", Label: "[p] unpause"}) items = append(items, menu.Item{Val: "unpause", Label: "unpause"})
} }
items = append(items, menu.Item{Val: "cancel", Label: "[c] cancel"}) items = append(items, menu.Item{Val: "cancel", Label: "cancel"})
m.AddItems(items...) m.AddItems(items...)
ui.Render(m) ui.Render(m)
var nextMenu MenuFn
HandleKeys("up", m.Up) HandleKeys("up", m.Up)
HandleKeys("down", m.Down) HandleKeys("down", m.Down)
var selected string
// shortcuts
ui.Handle("/sys/kbd/o", func(ui.Event) {
selected = "single"
ui.StopLoop()
})
ui.Handle("/sys/kbd/l", func(ui.Event) {
selected = "logs"
ui.StopLoop()
})
if c.Meta["state"] != "paused" {
ui.Handle("/sys/kbd/s", func(ui.Event) {
if c.Meta["state"] == "running" {
selected = "stop"
} else {
selected = "start"
}
ui.StopLoop()
})
}
if c.Meta["state"] != "exited" && c.Meta["state"] != "created" {
ui.Handle("/sys/kbd/p", func(ui.Event) {
if c.Meta["state"] == "paused" {
selected = "unpause"
} else {
selected = "pause"
}
ui.StopLoop()
})
}
if c.Meta["state"] == "running" {
ui.Handle("/sys/kbd/e", func(ui.Event) {
selected = "exec"
ui.StopLoop()
})
ui.Handle("/sys/kbd/r", func(ui.Event) {
selected = "restart"
ui.StopLoop()
})
}
ui.Handle("/sys/kbd/R", func(ui.Event) {
selected = "remove"
ui.StopLoop()
})
ui.Handle("/sys/kbd/c", func(ui.Event) {
ui.StopLoop()
})
ui.Handle("/sys/kbd/<enter>", func(ui.Event) { ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
selected = m.SelectedValue() switch m.SelectedItem().Val {
case "single":
nextMenu = SingleView
case "logs":
nextMenu = LogMenu
case "start":
nextMenu = Confirm(confirmTxt("start", c.GetMeta("name")), c.Start)
case "stop":
nextMenu = Confirm(confirmTxt("stop", c.GetMeta("name")), c.Stop)
case "remove":
nextMenu = Confirm(confirmTxt("remove", c.GetMeta("name")), c.Remove)
case "pause":
nextMenu = Confirm(confirmTxt("pause", c.GetMeta("name")), c.Pause)
case "unpause":
nextMenu = Confirm(confirmTxt("unpause", c.GetMeta("name")), c.Unpause)
case "restart":
nextMenu = Confirm(confirmTxt("restart", c.GetMeta("name")), c.Restart)
}
ui.StopLoop() ui.StopLoop()
}) })
ui.Handle("/sys/kbd/", func(ui.Event) { ui.Handle("/sys/kbd/", func(ui.Event) {
ui.StopLoop() ui.StopLoop()
}) })
ui.Loop() ui.Loop()
var nextMenu MenuFn
switch selected {
case "single":
nextMenu = SingleView
case "logs":
nextMenu = LogMenu
case "exec":
nextMenu = ExecShell
case "start":
nextMenu = Confirm(confirmTxt("start", c.GetMeta("name")), c.Start)
case "stop":
nextMenu = Confirm(confirmTxt("stop", c.GetMeta("name")), c.Stop)
case "remove":
nextMenu = Confirm(confirmTxt("remove", c.GetMeta("name")), c.Remove)
case "pause":
nextMenu = Confirm(confirmTxt("pause", c.GetMeta("name")), c.Pause)
case "unpause":
nextMenu = Confirm(confirmTxt("unpause", c.GetMeta("name")), c.Unpause)
case "restart":
nextMenu = Confirm(confirmTxt("restart", c.GetMeta("name")), c.Restart)
}
return nextMenu return nextMenu
} }
@@ -349,31 +207,6 @@ func LogMenu() MenuFn {
return nil return nil
} }
func ExecShell() MenuFn {
c := cursor.Selected()
if c == nil {
return nil
}
ui.DefaultEvtStream.ResetHandlers()
defer ui.DefaultEvtStream.ResetHandlers()
// Detect and execute default shell in container.
// Execute Ash shell command: /bin/sh -c
// Reset colors: printf '\e[0m\e[?25h'
// Clear screen
// Run default shell for the user. It's configured in /etc/passwd and looks like root:x:0:0:root:/root:/bin/bash:
// 1. Get current user id: id -un
// 2. Find user's line in /etc/passwd by grep
// 3. Extract default user's shell by cutting seven's column separated by :
// 4. Execute the shell path with eval
if err := c.Exec([]string{"/bin/sh", "-c", "printf '\\e[0m\\e[?25h' && clear && eval `grep ^$(id -un): /etc/passwd | cut -d : -f 7-`"}); err != nil {
log.StatusErr(err)
}
return nil
}
// Create a confirmation dialog with a given description string and // Create a confirmation dialog with a given description string and
// func to perform if confirmed // func to perform if confirmed
func Confirm(txt string, fn func()) MenuFn { func Confirm(txt string, fn func()) MenuFn {
@@ -413,7 +246,7 @@ func Confirm(txt string, fn func()) MenuFn {
ui.Handle("/sys/kbd/y", func(ui.Event) { yes() }) ui.Handle("/sys/kbd/y", func(ui.Event) { yes() })
ui.Handle("/sys/kbd/<enter>", func(ui.Event) { ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
switch m.SelectedValue() { switch m.SelectedItem().Val {
case "cancel": case "cancel":
no() no()
case "yes": case "yes":

View File

@@ -7,31 +7,7 @@ type Log struct {
Message string Message string
} }
type Meta map[string]string
// NewMeta returns an initialized Meta map.
// An optional series of key, values may be provided to populate the map prior to returning
func NewMeta(kvs ...string) Meta {
m := make(Meta)
var i int
for i < len(kvs)-1 {
m[kvs[i]] = kvs[i+1]
i += 2
}
return m
}
func (m Meta) Get(k string) string {
if s, ok := m[k]; ok {
return s
}
return ""
}
type Metrics struct { type Metrics struct {
NCpus uint8
CPUUtil int CPUUtil int
NetTx int64 NetTx int64
NetRx int64 NetRx int64

View File

@@ -1,60 +0,0 @@
package widgets
import (
"fmt"
"strings"
"time"
ui "github.com/gizak/termui"
)
type ErrorView struct {
*ui.Par
lines []string
}
func NewErrorView() *ErrorView {
const yPad = 1
const xPad = 2
p := ui.NewPar("")
p.X = xPad
p.Y = yPad
p.Border = true
p.Height = 10
p.Width = 20
p.PaddingTop = yPad
p.PaddingBottom = yPad
p.PaddingLeft = xPad
p.PaddingRight = xPad
p.BorderLabel = " ctop - error "
p.Bg = ui.ThemeAttr("bg")
p.TextFgColor = ui.ThemeAttr("status.warn")
p.TextBgColor = ui.ThemeAttr("menu.text.bg")
p.BorderFg = ui.ThemeAttr("status.warn")
p.BorderLabelFg = ui.ThemeAttr("status.warn")
return &ErrorView{p, make([]string, 0, 50)}
}
func (w *ErrorView) Append(s string) {
if len(w.lines)+2 >= cap(w.lines) {
w.lines = append(w.lines[:0], w.lines[2:]...)
}
ts := time.Now().Local().Format("15:04:05 MST")
w.lines = append(w.lines, fmt.Sprintf("[%s] %s", ts, s))
w.lines = append(w.lines, "")
}
func (w *ErrorView) Buffer() ui.Buffer {
offset := len(w.lines) - w.InnerHeight()
if offset < 0 {
offset = 0
}
w.Text = strings.Join(w.lines[offset:len(w.lines)], "\n")
return w.Par.Buffer()
}
func (w *ErrorView) Resize() {
w.Height = ui.TermHeight() - (w.PaddingTop + w.PaddingBottom)
w.SetWidth(ui.TermWidth() - (w.PaddingLeft + w.PaddingRight))
}

View File

@@ -16,7 +16,7 @@ type CTopHeader struct {
func NewCTopHeader() *CTopHeader { func NewCTopHeader() *CTopHeader {
return &CTopHeader{ return &CTopHeader{
Time: headerPar(2, ""), Time: headerPar(2, timeStr()),
Count: headerPar(24, "-"), Count: headerPar(24, "-"),
Filter: headerPar(40, ""), Filter: headerPar(40, ""),
bg: headerBg(), bg: headerBg(),

View File

@@ -11,14 +11,13 @@ type Padding [2]int // x,y padding
type Menu struct { type Menu struct {
ui.Block ui.Block
SortItems bool // enable automatic sorting of menu items SortItems bool // enable automatic sorting of menu items
Selectable bool // whether menu is navigable
SubText string // optional text to display before items SubText string // optional text to display before items
TextFgColor ui.Attribute TextFgColor ui.Attribute
TextBgColor ui.Attribute TextBgColor ui.Attribute
Selectable bool
cursorPos int cursorPos int
items Items items Items
padding Padding padding Padding
toolTip *ToolTip
} }
func NewMenu() *Menu { func NewMenu() *Menu {
@@ -43,7 +42,7 @@ func (m *Menu) AddItems(items ...Item) {
m.refresh() m.refresh()
} }
// DelItem removes menu item by value or label // Remove menu item by value or label
func (m *Menu) DelItem(s string) (success bool) { func (m *Menu) DelItem(s string) (success bool) {
for n, i := range m.items { for n, i := range m.items {
if i.Val == s || i.Label == s { if i.Val == s || i.Label == s {
@@ -56,11 +55,6 @@ func (m *Menu) DelItem(s string) (success bool) {
return success return success
} }
// ClearItems removes all current menu items
func (m *Menu) ClearItems() {
m.items = m.items[:0]
}
// Move cursor to an position by Item value or label // Move cursor to an position by Item value or label
func (m *Menu) SetCursor(s string) (success bool) { func (m *Menu) SetCursor(s string) (success bool) {
for n, i := range m.items { for n, i := range m.items {
@@ -72,19 +66,19 @@ func (m *Menu) SetCursor(s string) (success bool) {
return false return false
} }
// SetToolTip sets an optional tooltip string to show at bottom of screen // Sort menu items(if enabled) and re-calculate window size
func (m *Menu) SetToolTip(lines ...string) { func (m *Menu) refresh() {
m.toolTip = NewToolTip(lines...) if m.SortItems {
sort.Sort(m.items)
}
m.calcSize()
ui.Render(m)
} }
func (m *Menu) SelectedItem() Item { func (m *Menu) SelectedItem() Item {
return m.items[m.cursorPos] return m.items[m.cursorPos]
} }
func (m *Menu) SelectedValue() string {
return m.items[m.cursorPos].Val
}
func (m *Menu) Buffer() ui.Buffer { func (m *Menu) Buffer() ui.Buffer {
var cell ui.Cell var cell ui.Cell
buf := m.Block.Buffer() buf := m.Block.Buffer()
@@ -114,10 +108,6 @@ func (m *Menu) Buffer() ui.Buffer {
} }
} }
if m.toolTip != nil {
buf.Merge(m.toolTip.Buffer())
}
return buf return buf
} }
@@ -135,15 +125,6 @@ func (m *Menu) Down() {
} }
} }
// Sort menu items(if enabled) and re-calculate window size
func (m *Menu) refresh() {
if m.SortItems {
sort.Sort(m.items)
}
m.calcSize()
ui.Render(m)
}
// Set width and height based on menu items // Set width and height based on menu items
func (m *Menu) calcSize() { func (m *Menu) calcSize() {
m.Width = 7 // minimum width m.Width = 7 // minimum width

View File

@@ -1,55 +0,0 @@
package menu
import (
ui "github.com/gizak/termui"
)
type ToolTip struct {
ui.Block
Lines []string
TextFgColor ui.Attribute
TextBgColor ui.Attribute
padding Padding
}
func NewToolTip(lines ...string) *ToolTip {
t := &ToolTip{
Block: *ui.NewBlock(),
Lines: lines,
TextFgColor: ui.ThemeAttr("menu.text.fg"),
TextBgColor: ui.ThemeAttr("menu.text.bg"),
padding: Padding{2, 1},
}
t.BorderFg = ui.ThemeAttr("menu.border.fg")
t.BorderLabelFg = ui.ThemeAttr("menu.label.fg")
t.X = 1
t.Align()
return t
}
func (t *ToolTip) Buffer() ui.Buffer {
var cell ui.Cell
buf := t.Block.Buffer()
y := t.Y + t.padding[1]
for n, line := range t.Lines {
x := t.X + t.padding[0]
for _, ch := range line {
cell = ui.Cell{Ch: ch, Fg: t.TextFgColor, Bg: t.TextBgColor}
buf.Set(x, y+n, cell)
x++
}
}
return buf
}
// Set width and height based on screen size
func (t *ToolTip) Align() {
t.Width = ui.TermWidth() - (t.padding[0] * 2)
t.Height = len(t.Lines) + (t.padding[1] * 2)
t.Y = ui.TermHeight() - t.Height
t.Block.Align()
}

View File

@@ -2,7 +2,6 @@ package widgets
import ( import (
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
"github.com/mattn/go-runewidth"
) )
type ToggleText interface { type ToggleText interface {
@@ -71,7 +70,7 @@ func (t *TextView) Buffer() ui.Buffer {
for _, ch := range line { for _, ch := range line {
cell = ui.Cell{Ch: ch, Fg: t.TextFgColor, Bg: t.TextBgColor} cell = ui.Cell{Ch: ch, Fg: t.TextFgColor, Bg: t.TextBgColor}
buf.Set(x, y, cell) buf.Set(x, y, cell)
x = x + runewidth.RuneWidth(ch) x++
} }
x = t.Block.X + t.padding[0] x = t.Block.X + t.padding[0]
y++ y++