├── .circleci
└── config.yml
├── .gitignore
├── .goreleaser.yml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── common
├── common.go
├── common_unix.go
├── common_windows.go
├── error.go
└── logger.go
├── docker
├── client.go
├── client_test.go
├── container.go
├── image.go
├── logs.go
├── network.go
├── streamer
│ ├── common.go
│ ├── in.go
│ ├── out.go
│ ├── streamer.go
│ ├── streamer_unix.go
│ └── streamer_windows.go
├── testdata
│ ├── ca.pem
│ ├── cert.pem
│ └── key.pem
└── volume.go
├── go.mod
├── go.sum
├── gui
├── containers.go
├── gui.go
├── images.go
├── info.go
├── keybindings.go
├── navigate.go
├── networks.go
├── panel.go
├── searchImages.go
├── tasks.go
└── volumes.go
├── main.go
└── wiki.md
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 |
3 | executors:
4 | build:
5 | parameters:
6 | go-version:
7 | type: string
8 | docker:
9 | - image: circleci/golang:<< parameters.go-version >>
10 | environment:
11 | GO111MODULE: "on"
12 | working_directory: /go/src/github.com/skanehira/docui
13 |
14 | docker:
15 | parameters:
16 | docker-version:
17 | type: string
18 | docker:
19 | - image: docker:<< parameters.docker-version >>
20 | working_directory: /go/src/github.com/skanehira/docui
21 |
22 | commands:
23 | go_mod_download:
24 | steps:
25 | - restore_cache:
26 | name: Restore go modules cache
27 | keys:
28 | - go-modules-{{ checksum "go.sum" }}
29 |
30 | - run: go mod download
31 |
32 | - save_cache:
33 | name: Save go modules cache
34 | key: go-modules-{{ checksum "go.sum" }}
35 | paths:
36 | - "/go/pkg/mod"
37 |
38 | jobs:
39 | build:
40 | parameters:
41 | go-version:
42 | type: string
43 |
44 | executor:
45 | name: build
46 | go-version: << parameters.go-version >>
47 |
48 | steps:
49 | - checkout
50 |
51 | - go_mod_download
52 |
53 | - run:
54 | name: Run tests
55 | command: go test -race -v ./...
56 |
57 | lint:
58 | parameters:
59 | go-version:
60 | type: string
61 | golangci-lint-version:
62 | type: string
63 |
64 | executor:
65 | name: build
66 | go-version: << parameters.go-version >>
67 |
68 | steps:
69 | - checkout
70 |
71 | - go_mod_download
72 |
73 | - run:
74 | name: Install GolangCI-Lint
75 | command: curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s v<< parameters.golangci-lint-version >>
76 |
77 | - run:
78 | name: Run GolangCI-Lint
79 | command: ./bin/golangci-lint run --tests --disable-all --enable=goimports --enable=golint --enable=govet
80 |
81 | build-docker-image-not-push:
82 | parameters:
83 | docker-version:
84 | type: string
85 |
86 | executor:
87 | name: docker
88 | docker-version: << parameters.docker-version >>
89 |
90 | steps:
91 | - checkout
92 |
93 | - setup_remote_docker:
94 | version: << parameters.docker-version >>
95 |
96 | - run:
97 | name: Build Docker Image
98 | command: docker build -t skanehira/docui .
99 |
100 | build-docker-image:
101 | parameters:
102 | docker-version:
103 | type: string
104 |
105 | executor:
106 | name: docker
107 | docker-version: << parameters.docker-version >>
108 |
109 | steps:
110 | - checkout
111 |
112 | - setup_remote_docker:
113 | version: << parameters.docker-version >>
114 |
115 | - run:
116 | name: Build Docker Image
117 | command: docker build -t skanehira/docui .
118 |
119 | - run:
120 | name: Login Docker Hub
121 | command: docker login -u ${DOCKER_USER} -p ${DOCKER_PASS}
122 |
123 | - run:
124 | name: Push Image
125 | command: docker push skanehira/docui
126 |
127 | release:
128 | parameters:
129 | go-version:
130 | type: string
131 |
132 | executor:
133 | name: build
134 | go-version: << parameters.go-version >>
135 |
136 | steps:
137 | - checkout
138 | - go_mod_download
139 | - run:
140 | name: Run goreleaser
141 | command: curl -sL https://git.io/goreleaser | bash -s -- --rm-dist
142 |
143 | workflows:
144 | stable-build:
145 | jobs:
146 | - lint:
147 | go-version: "1.12.8"
148 | golangci-lint-version: "1.17.1"
149 | - build:
150 | go-version: "1.12.8"
151 | requires:
152 | - lint
153 |
154 | latest-build:
155 | jobs:
156 | - lint:
157 | go-version: "1.12.8"
158 | golangci-lint-version: "1.17.1"
159 | - build:
160 | go-version: "1.12.8"
161 | requires:
162 | - lint
163 |
164 | docker-image-build-only:
165 | jobs:
166 | - build-docker-image-not-push:
167 | docker-version: "18.09.3"
168 | filters:
169 | branches:
170 | only: develop
171 |
172 | docker-image-build:
173 | jobs:
174 | - build-docker-image:
175 | docker-version: "18.09.3"
176 | filters:
177 | branches:
178 | only: master
179 |
180 | release:
181 | jobs:
182 | - lint:
183 | go-version: "1.12.8"
184 | golangci-lint-version: "1.17.1"
185 | - release:
186 | go-version: "1.12.8"
187 | filters:
188 | branches:
189 | ignore: /.*/
190 | tags:
191 | only: /[0-9]+(\.[0-9]+)(\.[0-9]+)/
192 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | *.snap
3 | .idea
4 | dist/
5 | /prime/
6 | /stage/
7 | /parts/
8 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | project_name: docui
2 |
3 | builds:
4 | - env:
5 | - CGO_ENABLED=0
6 | - GO111MODULE=on
7 |
8 | archives:
9 | - replacements:
10 | darwin: Darwin
11 | linux: Linux
12 | 386: i386
13 | amd64: x86_64
14 |
15 | checksum:
16 | name_template: 'checksums.txt'
17 |
18 | snapshot:
19 | name_template: "{{ .Tag }}-next"
20 |
21 | changelog:
22 | skip: true
23 |
24 | brews:
25 | - name: docui
26 | github:
27 | owner: skanehira
28 | name: homebrew-docui
29 | folder: Formula
30 | description: "TUI Client for Docker"
31 | homepage: "https://github.com/skanehira/docui"
32 | commit_author:
33 | name: goreleaserbot
34 | email: goreleaser@carlosbecker.com
35 | dependencies:
36 | - go
37 | install: |
38 | bin.install "docui"
39 | test: |
40 | system "#{bin}/docui"
41 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # build docui
2 | FROM golang:1.16.0 AS build-docui
3 | ENV GOOS linux
4 | ENV GOARCH amd64
5 | ENV CGO_ENABLED 0
6 | COPY . ./src/github.com/skanehira/docui
7 | WORKDIR /go/src/github.com/skanehira/docui
8 | RUN go build
9 |
10 | # copy artifact from the build stage
11 | FROM busybox:1.33
12 | ENV TERM "xterm-256color"
13 | COPY --from=build-docui /go/src/github.com/skanehira/docui/docui /usr/local/bin/docui
14 |
15 | ENTRYPOINT ["docui"]
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 skanehira
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Go parameters
2 | GOBUILD=go build
3 | GOCLEAN=go clean
4 | BINARY_NAME=docui
5 | DOCKER_BINARY_NAME=docui-docker
6 |
7 | export GO111MODULE=on
8 |
9 | all: build
10 |
11 | clean:
12 | $(GOCLEAN)
13 |
14 | build: clean
15 | $(GOBUILD) -o $(BINARY_NAME)
16 |
17 | # copy to $GOBIN
18 | install: build
19 | cp -f $(BINARY_NAME) $(GOBIN)/
20 |
21 | # build release binary
22 | release: clean
23 | GOOS=darwin GOARCH=amd64 $(GOBUILD) && zip MacOS.zip ./docui && rm -rf ./docui
24 | GOOS=linux GOARCH=amd64 $(GOBUILD) && zip Linux.zip ./docui && rm -rf ./docui
25 |
26 | docker-build:
27 | docker build -t skanehira/docui .
28 |
29 | docker-push:
30 | docker push skanehira/docui
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # docui - TUI Client for Docker Written in Go
2 |
3 | [](https://goreportcard.com/report/github.com/skanehira/docui)
4 | [](https://goreportcard.com/report/github.com/skanehira/docui)
5 | [](https://github.com/skanehira/docui/releases)
6 | 
7 | 
8 |
9 | # This repository is no longer maintenance. Please use [lazydocker](https://github.com/jesseduffield/lazydocker) instead.
10 |
11 | ## About docui
12 | 
13 |
14 | docui is a TUI Client for Docker.
15 | It can do the following:
16 |
17 | - image
18 | - search/pull/remove
19 | - save/import/load
20 | - inspect/filtering
21 |
22 | - container
23 | - create/remove
24 | - start/stop/kill
25 | - export/commit
26 | - inspect/rename/filtering
27 | - exec cmd
28 |
29 | - volume
30 | - create/remove
31 | - inspect/filtering
32 |
33 | - network
34 | - remove
35 | - inspect/filtering
36 |
37 | ## Supported OSes
38 | - Mac
39 | - Linux
40 |
41 | ## Required Tools
42 | - Go Ver.1.11.4~
43 | - Docker Engine Ver.18.06.1~
44 | - Git
45 |
46 | ## Installation
47 | ### Environment variables
48 | The following environment variables must be set.
49 |
50 | ```
51 | export LC_CTYPE=en_US.UTF-8
52 | export TERM=xterm-256color
53 | ```
54 |
55 | ### From Source
56 |
57 | If you have not installed go and set GOPATH/GOBIN,
58 | you must install and set env before installing docui.
59 |
60 | This project uses [Go Modules](https://github.com/golang/go/wiki/Modules) for dependencies introduced in Go 1.11.
61 |
62 | Use go get or git clone:
63 |
64 | ```
65 | $ go get -d github.com/skanehira/docui
66 | $ cd $GOPATH/src/github.com/skanehira/docui
67 | $ GO111MODULE=on go install
68 | ```
69 |
70 | ```
71 | $ git clone https://github.com/skanehira/docui.git
72 | $ cd docui/
73 | $ GO111MODULE=on go install
74 | ```
75 |
76 | Make sure your PATH includes the $GOPATH/bin directory so your commands can be easily used:
77 |
78 | ```sh
79 | export PATH=$PATH:$GOPATH/bin
80 | ```
81 |
82 | ### Homebrew
83 |
84 | ```sh
85 | $ brew install docui
86 | ```
87 |
88 | ### Nix
89 |
90 | docui is available on nixpkgs unstable channel:
91 |
92 | ```sh
93 | $ nix-env -i docui
94 | ```
95 |
96 | ## Update
97 |
98 | Use git pull:
99 |
100 | ```sh
101 | $ git pull
102 | $ GO111MODULE=on go install
103 | ```
104 |
105 | ## Log file
106 |
107 | Check [wiki](https://github.com/skanehira/docui/blob/master/wiki.md).
108 |
109 | ## Use on Docker
110 | ```
111 | $ docker run --rm -itv /var/run/docker.sock:/var/run/docker.sock skanehira/docui
112 | ```
113 |
114 | ## Build Docker Image
115 | ```sh
116 | $ make docker-build
117 | ```
118 |
119 | ## Keybindings
120 | | panel | operation | key |
121 | |------------------|------------------------|----------------------------------------------------|
122 | | all | change panel | Tab / Shift + Tab |
123 | | all | quit | q |
124 | | list panels | next entry | j / ↓ |
125 | | list panels | previous entry | k / ↑ |
126 | | list panels | next page | Ctrl / f |
127 | | list panels | previous page | Ctrl / b |
128 | | list panels | scroll to top | g |
129 | | list panels | scroll to bottom | G |
130 | | image list | pull image | p |
131 | | image list | search images | f |
132 | | image list | remove image | d |
133 | | image list | create container | c |
134 | | image list | inspect image | Enter |
135 | | image list | save image | s |
136 | | image list | import image | i |
137 | | image list | load image | Ctrl + l |
138 | | image list | refresh image list | Ctrl + r |
139 | | image list | filter image | / |
140 | | container list | inspect container | Enter |
141 | | container list | remove container | d |
142 | | container list | start container | u |
143 | | container list | stop container | s |
144 | | container list | kill container | Ctrl + k |
145 | | container list | export container | e |
146 | | container list | commit container | c |
147 | | container list | rename container | r |
148 | | container list | refresh container list | Ctrl + r |
149 | | container list | filter image | / |
150 | | container list | exec container cmd | Ctrl + e |
151 | | container logs | show container logs | Ctrl + l |
152 | | volume list | create volume | c |
153 | | volume list | remove volume | d |
154 | | volume list | inspect volume | Enter |
155 | | volume list | refresh volume list | Ctrl + r |
156 | | volume list | filter volume | / |
157 | | network list | inspect network | Enter |
158 | | network list | remove network | d |
159 | | network list | filter network | / |
160 | | pull image | pull image | Enter |
161 | | pull image | close panel | Esc |
162 | | create container | next input box | Tab |
163 | | create container | previous input box | Shift + Tab |
164 | | detail | cursor dwon | j |
165 | | detail | cursor up | k |
166 | | detail | next page | Ctrl / f |
167 | | detail | previous page | Ctrl / b |
168 | | search images | search image | Enter |
169 | | search images | close panel | Esc |
170 | | search result | next image | j |
171 | | search result | previous image | k |
172 | | search result | pull image | Enter |
173 | | search result | close panel | q |
174 | | create volume | close panel | Esc |
175 | | create volume | next input box | Tab |
176 | | create volume | previous input box | Shift + Tab |
177 |
178 | ## How to use
179 | For details of the input panel please refer to [wiki](https://github.com/skanehira/docui/blob/master/wiki.md)
180 |
181 | ## Alternatives
182 | - [lazydocker](https://github.com/jesseduffield/lazydocker)
183 | A simple terminal UI for both docker and docker-compose, written in Go with the gocui library.
184 | - [docker.vim](https://github.com/skanehira/docker.vim)
185 | Manage docker containers and images in Vim
186 | - See [Awesome Docker list](https://github.com/veggiemonk/awesome-docker/blob/master/README.md#terminal) for similar tools to work with Docker.
187 |
--------------------------------------------------------------------------------
/common/common.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "os"
8 | "sort"
9 | "strings"
10 | "time"
11 |
12 | "github.com/docker/docker/api/types"
13 | )
14 |
15 | var cutNewlineReplacer = strings.NewReplacer("\r", "", "\n", "")
16 |
17 | // StructToJSON convert struct to json.
18 | func StructToJSON(i interface{}) string {
19 | j, err := json.Marshal(i)
20 | if err != nil {
21 | return ""
22 | }
23 |
24 | out := new(bytes.Buffer)
25 | json.Indent(out, j, "", " ")
26 | return out.String()
27 | }
28 |
29 | // SortKeys sort keys.
30 | func SortKeys(keys []string) []string {
31 | sort.Strings(keys)
32 | return keys
33 | }
34 |
35 | // GetOSenv get os environment.
36 | func GetOSenv(env string) string {
37 | keyval := strings.SplitN(env, "=", 2)
38 | if keyval[1][:1] == "$" {
39 | keyval[1] = os.Getenv(keyval[1][1:])
40 | return strings.Join(keyval, "=")
41 | }
42 |
43 | return env
44 | }
45 |
46 | // ParseDateToString parse date to string.
47 | func ParseDateToString(unixtime int64) string {
48 | t := time.Unix(unixtime, 0)
49 | return t.Format("2006/01/02 15:04:05")
50 | }
51 |
52 | // ParseSizeToString parse size to string.
53 | func ParseSizeToString(size int64) string {
54 | mb := float64(size) / 1024 / 1024
55 | return fmt.Sprintf("%.1fMB", mb)
56 | }
57 |
58 | // ParsePortToString parse port to string.
59 | func ParsePortToString(ports []types.Port) string {
60 | var port string
61 | for _, p := range ports {
62 | if p.PublicPort == 0 {
63 | port += fmt.Sprintf("%d/%s ", p.PrivatePort, p.Type)
64 | } else {
65 | port += fmt.Sprintf("%s:%d->%d/%s ", p.IP, p.PublicPort, p.PrivatePort, p.Type)
66 | }
67 | }
68 | return port
69 | }
70 |
71 | // ParseRepoTag parse image repo and tag.
72 | func ParseRepoTag(repoTag string) (string, string) {
73 | tmp := strings.Split(repoTag, ":")
74 | tag := tmp[len(tmp)-1]
75 | repo := strings.Join(tmp[0:len(tmp)-1], ":")
76 | return repo, tag
77 | }
78 |
79 | // ParseLabels parse image labels.
80 | func ParseLabels(labels map[string]string) string {
81 | if len(labels) < 1 {
82 | return ""
83 | }
84 |
85 | var result string
86 | for label, value := range labels {
87 | result += fmt.Sprintf("%s=%s ", label, value)
88 | }
89 |
90 | return result
91 | }
92 |
93 | // DateNow return date time.
94 | func DateNow() string {
95 | return time.Now().Format("2006/01/02 15:04:05")
96 | }
97 |
98 | // CutNewline cut new line.
99 | func CutNewline(i string) string {
100 | return cutNewlineReplacer.Replace(i)
101 | }
102 |
--------------------------------------------------------------------------------
/common/common_unix.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 | // +build !windows
3 |
4 | package common
5 |
6 | import (
7 | "os"
8 | "os/signal"
9 | "syscall"
10 | "unsafe"
11 | )
12 |
13 | func getTermSize(fd uintptr) (int, int) {
14 | var sz struct {
15 | rows uint16
16 | cols uint16
17 | }
18 |
19 | _, _, _ = syscall.Syscall(syscall.SYS_IOCTL,
20 | fd, uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&sz)))
21 | return int(sz.cols), int(sz.rows)
22 | }
23 |
24 | // IsTerminalWindowSizeThanZero check terminal window size
25 | func IsTerminalWindowSizeThanZero() bool {
26 | out, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
27 | if err != nil {
28 | Logger.Error(err)
29 | return false
30 | }
31 |
32 | defer out.Close()
33 |
34 | signalCh := make(chan os.Signal, 1)
35 | signal.Notify(signalCh, syscall.SIGWINCH, syscall.SIGINT)
36 |
37 | for {
38 | // check terminal window size
39 | termw, termh := getTermSize(out.Fd())
40 | if termw > 0 && termh > 0 {
41 | return true
42 | }
43 |
44 | select {
45 | case signal := <-signalCh:
46 | switch signal {
47 | // when the terminal window size is changed
48 | case syscall.SIGWINCH:
49 | continue
50 | // use ctrl + c to cancel
51 | case syscall.SIGINT:
52 | return false
53 | }
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/common/common_windows.go:
--------------------------------------------------------------------------------
1 | //go:build windows
2 | // +build windows
3 |
4 | package common
5 |
6 | import (
7 | "syscall"
8 | "unsafe"
9 | )
10 |
11 | const (
12 | rightAltPressed = 1
13 | leftAltPressed = 2
14 | rightCtrlPressed = 4
15 | leftCtrlPressed = 8
16 | shiftPressed = 0x0010
17 | ctrlPressed = rightCtrlPressed | leftCtrlPressed
18 | altPressed = rightAltPressed | leftAltPressed
19 | )
20 |
21 | const (
22 | enableProcessedInput = 0x1
23 | enableLineInput = 0x2
24 | enableEchoInput = 0x4
25 | enableWindowInput = 0x8
26 | enableMouseInput = 0x10
27 | enableInsertMode = 0x20
28 | enableQuickEditMode = 0x40
29 | enableExtendedFlag = 0x80
30 |
31 | enableProcessedOutput = 1
32 | enableWrapAtEolOutput = 2
33 |
34 | keyEvent = 0x1
35 | mouseEvent = 0x2
36 | windowBufferSizeEvent = 0x4
37 | )
38 |
39 | var kernel32 = syscall.NewLazyDLL("kernel32.dll")
40 |
41 | var (
42 | procAllocConsole = kernel32.NewProc("AllocConsole")
43 | procSetStdHandle = kernel32.NewProc("SetStdHandle")
44 | procGetStdHandle = kernel32.NewProc("GetStdHandle")
45 | procSetConsoleScreenBufferSize = kernel32.NewProc("SetConsoleScreenBufferSize")
46 | procCreateConsoleScreenBuffer = kernel32.NewProc("CreateConsoleScreenBuffer")
47 | procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
48 | procWriteConsoleOutputCharacter = kernel32.NewProc("WriteConsoleOutputCharacterW")
49 | procWriteConsoleOutputAttribute = kernel32.NewProc("WriteConsoleOutputAttribute")
50 | procGetConsoleCursorInfo = kernel32.NewProc("GetConsoleCursorInfo")
51 | procSetConsoleCursorInfo = kernel32.NewProc("SetConsoleCursorInfo")
52 | procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition")
53 | procReadConsoleInput = kernel32.NewProc("ReadConsoleInputW")
54 | procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
55 | procSetConsoleMode = kernel32.NewProc("SetConsoleMode")
56 | procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW")
57 | procFillConsoleOutputAttribute = kernel32.NewProc("FillConsoleOutputAttribute")
58 | procScrollConsoleScreenBuffer = kernel32.NewProc("ScrollConsoleScreenBufferW")
59 | )
60 |
61 | type wchar uint16
62 | type short int16
63 | type dword uint32
64 | type word uint16
65 |
66 | type coord struct {
67 | x short
68 | y short
69 | }
70 |
71 | type smallRect struct {
72 | left short
73 | top short
74 | right short
75 | bottom short
76 | }
77 |
78 | type consoleScreenBufferInfo struct {
79 | size coord
80 | cursorPosition coord
81 | attributes word
82 | window smallRect
83 | maximumWindowSize coord
84 | }
85 |
86 | func getTermSize(fd uintptr) (int, int) {
87 | var csbi consoleScreenBufferInfo
88 | r1, _, _ := procGetConsoleScreenBufferInfo.Call(fd, uintptr(unsafe.Pointer(&csbi)))
89 | if r1 == 0 {
90 | return 80, 25
91 | }
92 | return int(csbi.window.right - csbi.window.left + 1), int(csbi.window.bottom - csbi.window.top + 1)
93 | }
94 |
95 | // IsTerminalWindowSizeThanZero check terminal window size
96 | func IsTerminalWindowSizeThanZero() bool {
97 | h, err := syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE)
98 | if err != nil {
99 | return true
100 | }
101 | termw, termh := getTermSize(uintptr(h))
102 | if termw > 0 && termh > 0 {
103 | return true
104 | }
105 | return false
106 | }
107 |
--------------------------------------------------------------------------------
/common/error.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import "errors"
4 |
5 | var (
6 | // ErrNoContainer no container error.
7 | ErrNoContainer = errors.New("no container")
8 | // ErrNoImage no image error.
9 | ErrNoImage = errors.New("no image")
10 | // ErrNoVolume no volume error.
11 | ErrNoVolume = errors.New("no volume")
12 | // ErrNoNetwork no network error.
13 | ErrNoNetwork = errors.New("no network")
14 | // ErrDockerConnect cannot connect to docker engine error.
15 | ErrDockerConnect = errors.New("unable to connect to Docker")
16 | // ErrSmallTerminalWindowSize cannot run docui because of a small terminal window size
17 | ErrSmallTerminalWindowSize = errors.New("unable to run docui because of a small terminal window size")
18 | )
19 |
--------------------------------------------------------------------------------
/common/logger.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "io"
5 | "io/ioutil"
6 | "os"
7 |
8 | log "github.com/sirupsen/logrus"
9 | )
10 |
11 | // Logger logger
12 | var Logger *logger
13 |
14 | // Logger logger.
15 | type logger struct {
16 | io.Writer
17 | *log.Logger
18 | }
19 |
20 | // NewLogger create new logger.
21 | func NewLogger(logLevel string, logFileName string) {
22 | level, err := log.ParseLevel(logLevel)
23 | if err != nil {
24 | panic(err)
25 | }
26 |
27 | var logWriter io.Writer
28 |
29 | if logFileName == "" {
30 | logWriter = ioutil.Discard
31 | } else {
32 | logWriter, err = os.OpenFile(logFileName, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
33 | if err != nil {
34 | panic(err)
35 | }
36 | }
37 |
38 | log.SetFormatter(&log.TextFormatter{
39 | DisableColors: true,
40 | FullTimestamp: true,
41 | })
42 | log.SetLevel(level)
43 | log.SetOutput(logWriter)
44 | log.SetReportCaller(true)
45 |
46 | Logger = &logger{
47 | Writer: logWriter,
48 | Logger: log.StandardLogger(),
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/docker/client.go:
--------------------------------------------------------------------------------
1 | package docker
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/docker/docker/client"
7 | )
8 |
9 | // Client docker client
10 | var Client *Docker
11 |
12 | // Docker docker client
13 | type Docker struct {
14 | *client.Client
15 | }
16 |
17 | // ClientConfig docker client config
18 | type ClientConfig struct {
19 | endpoint string
20 | certPath string
21 | keyPath string
22 | caPath string
23 | apiVersion string
24 | }
25 |
26 | // NewClientConfig create docker client config
27 | func NewClientConfig(endpoint, cert, key, ca, apiVersion string) *ClientConfig {
28 | return &ClientConfig{
29 | endpoint: endpoint,
30 | certPath: cert,
31 | keyPath: key,
32 | caPath: ca,
33 | apiVersion: apiVersion,
34 | }
35 | }
36 |
37 | // NewDocker create new docker client
38 | func NewDocker(config *ClientConfig) *Docker {
39 | if os.Getenv("DOCKER_HOST") != "" {
40 | client, err := client.NewClientWithOpts(client.FromEnv, client.WithVersion(config.apiVersion))
41 | if err != nil {
42 | panic(err)
43 | }
44 | return &Docker{client}
45 | }
46 |
47 | if config.caPath != "" &&
48 | config.certPath != "" &&
49 | config.keyPath != "" {
50 | client, err := client.NewClientWithOpts(client.WithTLSClientConfig(config.caPath, config.certPath, config.keyPath),
51 | client.WithHost(config.endpoint),
52 | client.WithVersion(config.apiVersion))
53 | if err != nil {
54 | panic(err)
55 | }
56 | return &Docker{client}
57 | }
58 |
59 | // default api version 1.39
60 | client, err := client.NewClientWithOpts(client.WithHost(config.endpoint), client.WithVersion(config.apiVersion))
61 | if err != nil {
62 | panic(err)
63 | }
64 |
65 | Client = &Docker{client}
66 |
67 | return Client
68 | }
69 |
--------------------------------------------------------------------------------
/docker/client_test.go:
--------------------------------------------------------------------------------
1 | package docker
2 |
3 | import (
4 | "os"
5 | "reflect"
6 | "testing"
7 |
8 | "github.com/docker/docker/client"
9 | )
10 |
11 | const (
12 | endpoint = "unix:///var/run/docker.sock"
13 | certPath = "testdata/cert.pem"
14 | keyPath = "testdata/key.pem"
15 | caPath = "testdata/ca.pem"
16 | apiVersion = "1.39"
17 | )
18 |
19 | func TestNewClientConfig(t *testing.T) {
20 | config := &ClientConfig{
21 | endpoint: endpoint,
22 | certPath: certPath,
23 | keyPath: keyPath,
24 | caPath: caPath,
25 | apiVersion: apiVersion,
26 | }
27 |
28 | verify := NewClientConfig(endpoint, certPath, keyPath, caPath, apiVersion)
29 | if config.endpoint != verify.endpoint {
30 | t.Errorf("Expected endpoint %+v. Got %+v.", config.endpoint, verify.endpoint)
31 | }
32 | if config.certPath != verify.certPath {
33 | t.Errorf("Expected certPath %+v. Got %+v.", config.certPath, verify.certPath)
34 | }
35 | if config.keyPath != verify.keyPath {
36 | t.Errorf("Expected keyPath %+v. Got %+v.", config.keyPath, verify.keyPath)
37 | }
38 | if config.caPath != verify.caPath {
39 | t.Errorf("Expected caPath %+v. Got %+v.", config.caPath, verify.caPath)
40 | }
41 | }
42 |
43 | func TestNewDocker(t *testing.T) {
44 | client, err := client.NewClientWithOpts(client.WithHost(endpoint), client.WithVersion(apiVersion))
45 | if err != nil {
46 | t.Fatal(err)
47 | }
48 | dockerClient := &Docker{client}
49 |
50 | config := NewClientConfig(endpoint, "", "", "", "")
51 | verify := NewDocker(config)
52 |
53 | expect := reflect.ValueOf(dockerClient).Elem().FieldByName("endpoint").String()
54 | got := reflect.ValueOf(verify).Elem().FieldByName("endpoint").String()
55 |
56 | if expect != got {
57 | t.Errorf("Expected endpoint %s. Got %s.", expect, got)
58 | }
59 | }
60 |
61 | func TestNewDockerTLS(t *testing.T) {
62 | client, err := client.NewClientWithOpts(client.WithTLSClientConfig(caPath, certPath, keyPath), client.WithHost(endpoint), client.WithVersion(apiVersion))
63 | if err != nil {
64 | t.Fatal(err)
65 | }
66 | dockerClient := &Docker{client}
67 |
68 | config := NewClientConfig(endpoint, certPath, keyPath, caPath, apiVersion)
69 | verify := NewDocker(config)
70 |
71 | expect := reflect.ValueOf(dockerClient).Elem().FieldByName("endpoint").String()
72 | got := reflect.ValueOf(verify).Elem().FieldByName("endpoint").String()
73 |
74 | if expect != got {
75 | t.Errorf("Expected endpoint %s. Got %s.", expect, got)
76 | }
77 | }
78 |
79 | func TestNewDockerFromEnv(t *testing.T) {
80 | os.Setenv("DOCKER_HOST", endpoint)
81 | client, err := client.NewClientWithOpts(client.FromEnv, client.WithVersion(apiVersion))
82 | if err != nil {
83 | t.Fatal(err)
84 | }
85 |
86 | dockerClient := &Docker{client}
87 | config := NewClientConfig("dummy endpoint", "", "", "", "")
88 | verify := NewDocker(config)
89 |
90 | if reflect.DeepEqual(dockerClient, verify) {
91 | t.Errorf("Expected Docker env clinet %+v. Got %+v.", dockerClient.Client, verify.Client)
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/docker/container.go:
--------------------------------------------------------------------------------
1 | package docker
2 |
3 | import (
4 | "context"
5 | "io"
6 | "os"
7 | "strings"
8 |
9 | "github.com/docker/docker/api/types"
10 | "github.com/docker/docker/api/types/container"
11 | "github.com/docker/docker/api/types/mount"
12 | "github.com/docker/docker/api/types/network"
13 | "github.com/docker/go-connections/nat"
14 | "github.com/skanehira/docui/common"
15 | "github.com/skanehira/docui/docker/streamer"
16 | )
17 |
18 | // CreateContainerOptions create container options
19 | type CreateContainerOptions struct {
20 | Config *container.Config
21 | HostConfig *container.HostConfig
22 | NetworkConfig *network.NetworkingConfig
23 | Name string
24 | }
25 |
26 | // Containers get containers
27 | func (d *Docker) Containers(opt types.ContainerListOptions) ([]types.Container, error) {
28 | return d.ContainerList(context.TODO(), opt)
29 | }
30 |
31 | // InspectContainer inspect container
32 | func (d *Docker) InspectContainer(name string) (types.ContainerJSON, error) {
33 | container, _, err := d.ContainerInspectWithRaw(context.TODO(), name, false)
34 | return container, err
35 | }
36 |
37 | // CreateContainer create container
38 | func (d *Docker) CreateContainer(opt CreateContainerOptions) error {
39 | _, err := d.ContainerCreate(context.TODO(), opt.Config, opt.HostConfig, opt.NetworkConfig, opt.Name)
40 | return err
41 | }
42 |
43 | // NewContainerOptions generate container options to create container
44 | func (d *Docker) NewContainerOptions(config map[string]string, isAttach bool) (CreateContainerOptions, error) {
45 |
46 | options := CreateContainerOptions{
47 | Config: &container.Config{},
48 | HostConfig: &container.HostConfig{},
49 | }
50 |
51 | options.Config.Image = config["Image"]
52 | options.Name = config["Name"]
53 |
54 | image, _, err := d.ImageInspectWithRaw(context.TODO(), options.Config.Image)
55 |
56 | if user := config["User"]; user != "" {
57 | options.Config.User = user
58 | }
59 |
60 | if err != nil {
61 | return options, err
62 | }
63 |
64 | options.Config.Env = image.Config.Env
65 |
66 | port := config["Port"]
67 | hostPort := config["HostPort"]
68 | ip := config["HostIP"]
69 |
70 | if ip == "" {
71 | ip = "0.0.0.0"
72 | }
73 |
74 | if port != "" && hostPort != "" {
75 | options.HostConfig.PortBindings = nat.PortMap{
76 | nat.Port(port + "/tcp"): {
77 | {
78 | HostIP: ip,
79 | HostPort: hostPort,
80 | },
81 | },
82 | }
83 | }
84 |
85 | if cmd := config["Cmd"]; cmd != "" {
86 | cmds := strings.Split(cmd, ",")
87 | for _, c := range cmds {
88 | options.Config.Cmd = append(options.Config.Cmd, c)
89 | }
90 | }
91 |
92 | if env := config["Env"]; env != "" {
93 | for _, v := range strings.Split(env, ",") {
94 | v = common.GetOSenv(v)
95 | options.Config.Env = append(options.Config.Env, v)
96 | }
97 | }
98 |
99 | hostVolume := config["HostVolume"]
100 | volume := config["Volume"]
101 | if hostVolume != "" && volume != "" {
102 | options.HostConfig.Mounts = []mount.Mount{
103 | {
104 | Target: volume,
105 | Source: hostVolume,
106 | Type: mount.Type(config["VolumeType"]),
107 | },
108 | }
109 | }
110 |
111 | options.Config.AttachStdout = true
112 | options.Config.AttachStderr = true
113 |
114 | if isAttach {
115 | options.Config.Tty = true
116 | options.Config.AttachStdin = true
117 | options.Config.OpenStdin = true
118 | }
119 |
120 | return options, nil
121 | }
122 |
123 | // CommitContainer commit container
124 | func (d *Docker) CommitContainer(name string, opt types.ContainerCommitOptions) error {
125 | _, err := d.ContainerCommit(context.TODO(), name, opt)
126 | return err
127 | }
128 |
129 | // RemoveContainer remove container
130 | func (d *Docker) RemoveContainer(name string) error {
131 | return d.ContainerRemove(context.TODO(), name, types.ContainerRemoveOptions{})
132 | }
133 |
134 | // KillContainer kill container
135 | func (d *Docker) KillContainer(name string) error {
136 | return d.ContainerKill(context.TODO(), name, "KILL")
137 | }
138 |
139 | // RenameContainer rename container
140 | func (d *Docker) RenameContainer(id, newName string) error {
141 | return d.ContainerRename(context.TODO(), id, newName)
142 | }
143 |
144 | // StartContainer start container with id
145 | func (d *Docker) StartContainer(id string) error {
146 | return d.ContainerStart(context.TODO(), id, types.ContainerStartOptions{})
147 | }
148 |
149 | // StopContainer stop container with id
150 | func (d *Docker) StopContainer(id string) error {
151 | return d.ContainerStop(context.TODO(), id, nil)
152 | }
153 |
154 | // ExportContainer export container
155 | func (d *Docker) ExportContainer(name, path string) error {
156 | file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
157 | if err != nil {
158 | return err
159 | }
160 | defer file.Close()
161 |
162 | out, err := d.ContainerExport(context.TODO(), name)
163 |
164 | if err != nil {
165 | return err
166 | }
167 |
168 | if _, err = io.Copy(file, out); err != nil {
169 | return err
170 | }
171 |
172 | return nil
173 | }
174 |
175 | // CreateExec container exec create
176 | func (d *Docker) CreateExec(container, cmd string) (types.IDResponse, error) {
177 | return d.ContainerExecCreate(context.TODO(), container, types.ExecConfig{
178 | Tty: true,
179 | AttachStdin: true,
180 | AttachStderr: true,
181 | AttachStdout: true,
182 | Cmd: []string{cmd},
183 | })
184 | }
185 |
186 | // AttachExecContainer attach container
187 | func (d *Docker) AttachExecContainer(id, cmd string) error {
188 | exec, err := d.CreateExec(id, cmd)
189 |
190 | if err != nil {
191 | common.Logger.Error(err)
192 | return err
193 | }
194 |
195 | ctx := context.TODO()
196 |
197 | resp, err := d.ContainerExecAttach(ctx, exec.ID, types.ExecStartCheck{Tty: true})
198 | if err != nil {
199 | common.Logger.Error(err)
200 | return err
201 | }
202 | defer resp.Close()
203 |
204 | f := func(ctx context.Context, id string, options types.ResizeOptions) error {
205 | return d.ContainerExecResize(ctx, id, options)
206 | }
207 |
208 | s := streamer.New()
209 | if err := s.Stream(ctx, exec.ID, resp, streamer.ResizeContainer(f)); err != nil {
210 | return err
211 | }
212 | return nil
213 | }
214 |
--------------------------------------------------------------------------------
/docker/image.go:
--------------------------------------------------------------------------------
1 | package docker
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io"
7 | "io/ioutil"
8 | "os"
9 |
10 | "github.com/docker/docker/api/types"
11 | "github.com/docker/docker/api/types/filters"
12 | "github.com/docker/docker/api/types/registry"
13 | )
14 |
15 | // Images get images from
16 | func (d *Docker) Images(opt types.ImageListOptions) ([]types.ImageSummary, error) {
17 | return d.ImageList(context.TODO(), opt)
18 | }
19 |
20 | // InspectImage inspect image
21 | func (d *Docker) InspectImage(name string) (types.ImageInspect, error) {
22 | img, _, err := d.ImageInspectWithRaw(context.TODO(), name)
23 | return img, err
24 | }
25 |
26 | // PullImage pull image
27 | func (d *Docker) PullImage(name string) error {
28 | resp, err := d.ImagePull(context.TODO(), name, types.ImagePullOptions{})
29 |
30 | if err != nil {
31 | return err
32 | }
33 |
34 | _, err = io.Copy(ioutil.Discard, resp)
35 | if err != nil {
36 | return err
37 | }
38 |
39 | return nil
40 | }
41 |
42 | // RemoveImage remove image
43 | func (d *Docker) RemoveImage(name string) error {
44 | _, err := d.ImageRemove(context.TODO(), name, types.ImageRemoveOptions{})
45 | return err
46 | }
47 |
48 | // RemoveDanglingImages remove dangling images
49 | func (d *Docker) RemoveDanglingImages() error {
50 | opt := types.ImageListOptions{
51 | Filters: filters.NewArgs(filters.Arg("dangling", "true")),
52 | }
53 |
54 | images, err := d.Images(opt)
55 | if err != nil {
56 | return err
57 | }
58 |
59 | errIDs := []string{}
60 |
61 | for _, image := range images {
62 | if err := d.RemoveImage(image.ID); err != nil {
63 | errIDs = append(errIDs, image.ID[7:19])
64 | }
65 | }
66 |
67 | if len(errIDs) > 1 {
68 | return fmt.Errorf("can not remove ids\n%s", errIDs)
69 | }
70 |
71 | return nil
72 | }
73 |
74 | // SaveImage save image to tar file
75 | func (d *Docker) SaveImage(ids []string, path string) error {
76 | file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
77 | if err != nil {
78 | return err
79 | }
80 | defer file.Close()
81 |
82 | out, err := d.ImageSave(context.TODO(), ids)
83 | if err != nil {
84 | return err
85 | }
86 |
87 | if _, err = io.Copy(file, out); err != nil {
88 | return err
89 | }
90 |
91 | return nil
92 | }
93 |
94 | // LoadImage load image from tar file
95 | func (d *Docker) LoadImage(path string) error {
96 | file, err := os.Open(path)
97 | if err != nil {
98 | return err
99 | }
100 | defer file.Close()
101 |
102 | _, err = d.ImageLoad(context.TODO(), file, true)
103 | return err
104 | }
105 |
106 | // ImportImage import image
107 | func (d *Docker) ImportImage(name, tag, path string) error {
108 | file, err := os.Open(path)
109 | if err != nil {
110 | return err
111 | }
112 | defer file.Close()
113 |
114 | source := types.ImageImportSource{
115 | Source: file,
116 | SourceName: "-",
117 | }
118 |
119 | opt := types.ImageImportOptions{
120 | Tag: tag,
121 | }
122 |
123 | _, err = d.ImageImport(context.TODO(), source, name, opt)
124 |
125 | return err
126 | }
127 |
128 | // SearchImage search images
129 | func (d *Docker) SearchImage(name string) ([]registry.SearchResult, error) {
130 | // https://github.com/moby/moby/blob/8e610b2b55bfd1bfa9436ab110d311f5e8a74dcb/registry/service.go#L22
131 | // Limit default:25 min:1 max:100
132 | return d.ImageSearch(context.TODO(), name, types.ImageSearchOptions{Limit: 100})
133 | }
134 |
--------------------------------------------------------------------------------
/docker/logs.go:
--------------------------------------------------------------------------------
1 | package docker
2 |
3 | import (
4 | "context"
5 | "io"
6 |
7 | "github.com/docker/docker/api/types"
8 | )
9 |
10 | // ContainerLogStream returns the logs generated by a container in an io.ReadCloser.
11 | func (d *Docker) ContainerLogStream(name string) (io.ReadCloser, error) {
12 | options := types.ContainerLogsOptions{
13 | ShowStdout: true,
14 | ShowStderr: true,
15 | Since: "",
16 | Until: "",
17 | Timestamps: false,
18 | Follow: true,
19 | Tail: "all",
20 | Details: false,
21 | }
22 |
23 | ctx := context.Background()
24 | reader, err := d.ContainerLogs(ctx, name, options)
25 | if err != nil {
26 | return nil, err
27 | }
28 |
29 | return reader, nil
30 | }
31 |
--------------------------------------------------------------------------------
/docker/network.go:
--------------------------------------------------------------------------------
1 | package docker
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/docker/docker/api/types"
7 | )
8 |
9 | // Networks get networks
10 | func (d *Docker) Networks(opt types.NetworkListOptions) ([]types.NetworkResource, error) {
11 | return d.NetworkList(context.TODO(), opt)
12 | }
13 |
14 | // InspectNetwork inspect network
15 | func (d *Docker) InspectNetwork(name string) (types.NetworkResource, error) {
16 | return d.NetworkInspect(context.TODO(), name, types.NetworkInspectOptions{})
17 | }
18 |
19 | // RemoveNetwork remove network
20 | func (d *Docker) RemoveNetwork(name string) error {
21 | return d.NetworkRemove(context.TODO(), name)
22 | }
23 |
--------------------------------------------------------------------------------
/docker/streamer/common.go:
--------------------------------------------------------------------------------
1 | package streamer
2 |
3 | import "github.com/docker/docker/pkg/term"
4 |
5 | type CommonStream struct {
6 | Fd uintptr
7 | IsTerminal bool
8 | State *term.State
9 | }
10 |
11 | func (s *CommonStream) RestoreTerminal() {
12 | if s.State != nil {
13 | term.RestoreTerminal(s.Fd, s.State)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/docker/streamer/in.go:
--------------------------------------------------------------------------------
1 | package streamer
2 |
3 | import (
4 | "io"
5 |
6 | "github.com/docker/docker/pkg/term"
7 | )
8 |
9 | type In struct {
10 | CommonStream
11 | in io.ReadCloser
12 | }
13 |
14 | func (i *In) Read(p []byte) (int, error) {
15 | return i.in.Read(p)
16 | }
17 |
18 | func (i *In) Close() error {
19 | return i.in.Close()
20 | }
21 |
22 | func (i *In) SetRawTerminal() (err error) {
23 | i.CommonStream.State, err = term.SetRawTerminal(i.Fd)
24 | return
25 | }
26 |
27 | func (i *In) RestoreTerminal() error {
28 | if i.CommonStream.State == nil {
29 | return nil
30 | }
31 | return term.RestoreTerminal(i.CommonStream.Fd, i.CommonStream.State)
32 | }
33 |
34 | func NewIn(in io.ReadCloser) *In {
35 | fd, isTerminal := term.GetFdInfo(in)
36 | return &In{
37 | in: in,
38 | CommonStream: CommonStream{
39 | Fd: fd,
40 | IsTerminal: isTerminal,
41 | },
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/docker/streamer/out.go:
--------------------------------------------------------------------------------
1 | package streamer
2 |
3 | import (
4 | "io"
5 | "log"
6 |
7 | "github.com/docker/docker/pkg/term"
8 | )
9 |
10 | type Out struct {
11 | CommonStream
12 | out io.Writer
13 | }
14 |
15 | func (o *Out) Write(p []byte) (int, error) {
16 | return o.out.Write(p)
17 | }
18 |
19 | func (o *Out) GetTtySize() (uint, uint) {
20 | if !o.IsTerminal {
21 | return 0, 0
22 | }
23 | ws, err := term.GetWinsize(o.Fd)
24 | if err != nil {
25 | log.Printf("getting tty size: %s\n", err)
26 | return 0, 0
27 | }
28 | return uint(ws.Height), uint(ws.Width)
29 | }
30 |
31 | func NewOut(out io.Writer) *Out {
32 | fd, isTerminal := term.GetFdInfo(out)
33 | return &Out{
34 | out: out,
35 | CommonStream: CommonStream{
36 | Fd: fd,
37 | IsTerminal: isTerminal,
38 | },
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/docker/streamer/streamer.go:
--------------------------------------------------------------------------------
1 | package streamer
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "io"
7 | "log"
8 | "os"
9 | "sync"
10 | "time"
11 |
12 | "github.com/docker/docker/api/types"
13 | "github.com/docker/docker/pkg/term"
14 | "github.com/skanehira/docui/common"
15 | )
16 |
17 | type ResizeContainer func(ctx context.Context, id string, options types.ResizeOptions) error
18 |
19 | var (
20 | ErrEmptyExecID = errors.New("emtpy exec id")
21 | ErrTtySizeIsZero = errors.New("tty size is 0")
22 | )
23 |
24 | type Streamer struct {
25 | In *In
26 | Out *Out
27 | Err io.Writer
28 | isTty bool
29 | }
30 |
31 | func New() *Streamer {
32 | return &Streamer{
33 | In: NewIn(os.Stdin),
34 | Out: NewOut(os.Stdout),
35 | Err: os.Stderr,
36 | }
37 | }
38 |
39 | func (s *Streamer) Stream(ctx context.Context, id string, resp types.HijackedResponse, resize ResizeContainer) (err error) {
40 | if id == "" {
41 | return ErrEmptyExecID
42 | }
43 |
44 | errCh := make(chan error, 1)
45 |
46 | go func() {
47 | defer close(errCh)
48 | errCh <- s.stream(ctx, resp)
49 | }()
50 |
51 | if s.In.IsTerminal {
52 | s.monitorTtySize(ctx, resize, id)
53 | }
54 |
55 | if err := <-errCh; err != nil {
56 | common.Logger.Errorf("stream error: %s", err)
57 | return err
58 | }
59 |
60 | return nil
61 | }
62 |
63 | func (s *Streamer) stream(ctx context.Context, resp types.HijackedResponse) error {
64 | // set raw mode
65 | restore, err := s.SetRawTerminal()
66 | if err != nil {
67 | return err
68 | }
69 | defer restore()
70 |
71 | // start stdin/stdout stream
72 | outDone := s.streamOut(restore, resp)
73 | inDone := s.streamIn(restore, resp)
74 |
75 | select {
76 | case err := <-outDone:
77 | return err
78 | case <-inDone:
79 | select {
80 | case err := <-outDone:
81 | return err
82 | case <-ctx.Done():
83 | return ctx.Err()
84 | }
85 | case <-ctx.Done():
86 | return ctx.Err()
87 | }
88 | }
89 |
90 | func (s *Streamer) streamIn(restore func(), resp types.HijackedResponse) <-chan struct{} {
91 | done := make(chan struct{})
92 |
93 | go func() {
94 | defer close(done)
95 | _, err := io.Copy(resp.Conn, s.In)
96 | restore()
97 |
98 | if _, ok := err.(term.EscapeError); ok {
99 | return
100 | }
101 |
102 | if err != nil {
103 | common.Logger.Errorf("in stream error: %s", err)
104 | return
105 | }
106 |
107 | if err := resp.CloseWrite(); err != nil {
108 | common.Logger.Errorf("close response error: %s", err)
109 | }
110 | }()
111 |
112 | return done
113 | }
114 |
115 | func (s *Streamer) streamOut(restore func(), resp types.HijackedResponse) <-chan error {
116 | done := make(chan error, 1)
117 |
118 | go func() {
119 | _, err := io.Copy(s.Out, resp.Reader)
120 | restore()
121 |
122 | if err != nil {
123 | common.Logger.Errorf("output stream error: %s", err)
124 | return
125 | }
126 |
127 | done <- err
128 | }()
129 |
130 | return done
131 | }
132 |
133 | func (s *Streamer) SetRawTerminal() (func(), error) {
134 | if err := s.In.SetRawTerminal(); err != nil {
135 | return nil, err
136 | }
137 |
138 | var once sync.Once
139 | restore := func() {
140 | once.Do(func() {
141 | if err := s.In.RestoreTerminal(); err != nil {
142 | common.Logger.Errorf("failed to restore terminal: %s\n", err)
143 | }
144 | })
145 | }
146 |
147 | return restore, nil
148 | }
149 |
150 | func (s *Streamer) resizeTty(ctx context.Context, resize ResizeContainer, id string) error {
151 | h, w := s.Out.GetTtySize()
152 | if h == 0 && w == 0 {
153 | return ErrTtySizeIsZero
154 | }
155 |
156 | options := types.ResizeOptions{
157 | Height: h,
158 | Width: w,
159 | }
160 |
161 | return resize(ctx, id, options)
162 | }
163 |
164 | func (s *Streamer) initTtySize(ctx context.Context, resize ResizeContainer, id string) {
165 | if err := s.resizeTty(ctx, resize, id); err != nil {
166 | go func() {
167 | common.Logger.Errorf("failed to resize tty: %s\n", err)
168 | for retry := 0; retry < 5; retry++ {
169 | time.Sleep(10 * time.Millisecond)
170 | if err = s.resizeTty(ctx, resize, id); err == nil {
171 | break
172 | }
173 | }
174 | if err != nil {
175 | log.Println("failed to resize tty, using default size")
176 | }
177 | }()
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/docker/streamer/streamer_unix.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 | // +build !windows
3 |
4 | package streamer
5 |
6 | import (
7 | "context"
8 | "os"
9 | "os/signal"
10 | "syscall"
11 | )
12 |
13 | func (s *Streamer) monitorTtySize(ctx context.Context, resize ResizeContainer, id string) {
14 | s.initTtySize(ctx, resize, id)
15 | sigchan := make(chan os.Signal, 1)
16 | signal.Notify(sigchan, syscall.SIGWINCH)
17 | go func() {
18 | for range sigchan {
19 | s.resizeTty(ctx, resize, id)
20 | }
21 | }()
22 | }
23 |
--------------------------------------------------------------------------------
/docker/streamer/streamer_windows.go:
--------------------------------------------------------------------------------
1 | //go:build windows
2 | // +build windows
3 |
4 | package streamer
5 |
6 | import (
7 | "context"
8 | )
9 |
10 | func (s *Streamer) monitorTtySize(ctx context.Context, resize ResizeContainer, id string) {
11 | /* TODO: mattn: Currently, this is not supported on Windows.
12 | s.initTtySize(ctx, resize, id)
13 | sigchan := make(chan os.Signal, 1)
14 | signal.Notify(sigchan, syscall.SIGWINCH)
15 | go func() {
16 | for range sigchan {
17 | s.resizeTty(ctx, resize, id)
18 | }
19 | }()
20 | */
21 | }
22 |
--------------------------------------------------------------------------------
/docker/testdata/ca.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDyzCCArOgAwIBAgIJAMqHPLMzLU8eMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNV
3 | BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
4 | aWRnaXRzIFB0eSBMdGQxFDASBgNVBAMMC2Jvb3QyZG9ja2VyMR8wHQYJKoZIhvcN
5 | AQkBFhB1c2VyQGV4YW1wbGUuY29tMB4XDTE3MTAwNDA1MzkyMFoXDTI3MTAwMjA1
6 | MzkyMFowfDELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNV
7 | BAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEUMBIGA1UEAwwLYm9vdDJkb2Nr
8 | ZXIxHzAdBgkqhkiG9w0BCQEWEHVzZXJAZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3
9 | DQEBAQUAA4IBDwAwggEKAoIBAQDGpWLvwBuD74o0XERYcoHAwN4FqzwKSuwVI0e6
10 | 7R+YCWDRBRLmrJIaqTEEMO/CbXA7eSYr8ak/pXxrgy7cf3eDIEMQIjyK11PSdVV2
11 | ezMabxBzmyo0fESvBVPTOKY5HBo0+c0Q1pjPZOKFWjr3/xSnTQLHxztw9+yFo1vY
12 | /DGYK9Zb/gQly1rKpgMKiYzoYxLFSzL7EgGMDK9zVWs8MQthjsqR0wLKK4MBQXmr
13 | cOwNRRgo//bTgp+wf4NiCuOHw9DzvxVS3gFifCzab8QtBgurB2e0Ej5IjcHKsXvQ
14 | lkPFpX/5YEcRb71Ns+Wo3NsP7aFkdGYiRkyudbShxX+fp8ErAgMBAAGjUDBOMB0G
15 | A1UdDgQWBBRCxuobp2cZVi0FOlv2Cikv+yeleDAfBgNVHSMEGDAWgBRCxuobp2cZ
16 | Vi0FOlv2Cikv+yeleDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBj
17 | BuAJS3GbQJbm1c8x9OjKm/3EPwouxEDmbPI5xhjCfgHFiLqE8p60XAA8AH/l3Y8j
18 | r5ARH0NNmKf1fQ0AXO8Z7ctuHFYsVN8XN09se3F2o5sxkfDjQr5rLsxVJwwSfB5n
19 | 7FA+pv+6gWEqwa9P/mx+7YjRBNpRi2IZ2DMYNt5T1c2JXlcoDms99oMcpElKU0Vs
20 | VygGN86wDDrdUNcQotvtnNRhVxdNCyQGNxak/QolJQL57fdIkEvKLLhmCxT+IIyu
21 | bPHH6DiYI6ZxsHQb3CL6qfWO2MvIWYdW70/Wpzz/y2SSaUEo4LjUqLUw/Dd0+juv
22 | GSr4a8LUxiG7bkHa4xNS
23 | -----END CERTIFICATE-----
--------------------------------------------------------------------------------
/docker/testdata/cert.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDUDCCAjigAwIBAgIJALw2I+++VX++MA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNV
3 | BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
4 | aWRnaXRzIFB0eSBMdGQxFDASBgNVBAMMC2Jvb3QyZG9ja2VyMR8wHQYJKoZIhvcN
5 | AQkBFhB1c2VyQGV4YW1wbGUuY29tMB4XDTE3MTAwNDA1NDEzNFoXDTI3MTAwMjA1
6 | NDEzNFowETEPMA0GA1UEAwwGY2xpZW50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
7 | MIIBCgKCAQEAs1bJPjF06hdKcX47c2q24lYmTCJUd3lPaG83MjyZb16jQaZ8J1En
8 | ckULNbZaArYQgjAPxUVDpp1WVfRqqsXa6rT/BjTSp+Abtq+lcL2ABa5Rrf9XI8qB
9 | xs4EiiE45N9w0GxG2+WCUulu5ldDW7E9VeQ/yCwMc8cKYv+zZRxHj0fVRtAtk1ea
10 | idqhL0zmShfklPDa5EyNm03kfqVQcV3WpPRBfEbbek7sLbAXmJUdyp+uMeo8a147
11 | pKbv0njRMfj0TA/ATzKF+Dq3fmpb+lCM0wuPJXwDgpWrKfO+50aRuax2sod3yHBU
12 | Vs33+qmzLBrq5OzTyRfk+ALQ+5md+l3F4wIDAQABo0AwPjAnBgNVHREEIDAeggti
13 | b290MmRvY2tlcoIJbG9jYWxob3N0hwR/AAABMBMGA1UdJQQMMAoGCCsGAQUFBwMC
14 | MA0GCSqGSIb3DQEBCwUAA4IBAQAtlDz0QjW//j0NMKnNZv5IDLhomXMFzou2FUD1
15 | j93H5grBQVWtUfHCTZgr92LL5qnxSEGnGLX+f1e4hkkOnSLqenJORJDwFYuShj13
16 | GmQSGfKLndH5+0uLLjIcDqO9wMlQ+Z0sHF0lEi5CN5Y7JlBsgZY/ucRzZc8FP+J2
17 | 63MqgvV/BQBPNzr8X14MU9F2lR4RhimS5HgMofNpDHjlVDnl+/N3EQX4JoZAAEht
18 | CfMchLmdVU6fnMIyxOP5FnxILBFhXalt/qJjrbMQ/++fiR82zedB8gM609G1HFb1
19 | RC9W6rTdDChmKzvBHv0ZgPRYnrE30zmdAixfwYs0GrLrgZoI
20 | -----END CERTIFICATE-----
--------------------------------------------------------------------------------
/docker/testdata/key.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEpAIBAAKCAQEAs1bJPjF06hdKcX47c2q24lYmTCJUd3lPaG83MjyZb16jQaZ8
3 | J1EnckULNbZaArYQgjAPxUVDpp1WVfRqqsXa6rT/BjTSp+Abtq+lcL2ABa5Rrf9X
4 | I8qBxs4EiiE45N9w0GxG2+WCUulu5ldDW7E9VeQ/yCwMc8cKYv+zZRxHj0fVRtAt
5 | k1eaidqhL0zmShfklPDa5EyNm03kfqVQcV3WpPRBfEbbek7sLbAXmJUdyp+uMeo8
6 | a147pKbv0njRMfj0TA/ATzKF+Dq3fmpb+lCM0wuPJXwDgpWrKfO+50aRuax2sod3
7 | yHBUVs33+qmzLBrq5OzTyRfk+ALQ+5md+l3F4wIDAQABAoIBAAHQc/K8H1mq6Kbj
8 | Rwiw4K7Dflmw0zfuMz50OZO5hzfYNkCYxZLEQGjoh+eAM8LpQpt+jvpI7BlIaDIw
9 | ac/WdpN/R0Ex0Zu/KaiceEj+scfc7pibB6+mAdAYD3WyRlgSZprFLaZV2Q6rq+KK
10 | qXTJAzzWNUr6HxogvlsoCRGRyquJYynltZH7zKNsKkMn+bUmWHQoBp/y16xGF7zN
11 | Xi4iO5L5fBqK0jw+mv2Dgqfavr7NTlMUIJwzOut7NpYNGh/cnQh+G/gStOkbJD9E
12 | N+jeja7tio9vEUtBbX76eM+qIqvAkhCbwLy5v07X5z/cobZRvm+SGiby6yOwik7k
13 | yEsWuekCgYEA2qVoebE6WMbcrnNJQQ5GJ34ITJGrUPeqHUflzHeax81do3Plc1I0
14 | eiBa2Pie8UsfJTpyAxRNdB8xvan+OMq1ZcXrhGIZL1MoE0s/qhsQWrLrHZmPeKe3
15 | E5BMr0QAzwWKAN94SINqQvYsVDWH5oamrJfJ8tUYSsEwU1tpN/b562cCgYEA0fpD
16 | OfydNbTIAza1s3jOaqa/U0F1wEMzBpQmOzDRdRW7RNPljApu6noGy84kRu3p5d+9
17 | 2K7G+8Jp+OjjjZsbDedjxChNFdkt63mlzN3p4HewqlD9ow5U14o4Uygeoe0wC5JC
18 | yrFWXl/HDtEl0zq1NOmx2fuxkkHB42UTwM1TQCUCgYEAzEU0slytkjtX+XOjwK/B
19 | QywwPLjrQR3hRLHjM3+aj4iCLy8b8v2eIWsQ9RxyAiSY2IMd+VqCcjailfu3kv9v
20 | McITIv8zYE9kZYIr3JSkVthJaMIKlEK8e/6YZjY2OzOFlwMwsS51nFG5YIMSkIDE
21 | PwMgnUoRA+Yxa+8/AhYvr4kCgYEAuBo6GTxwxmb0O0UbuPkB2qbK939cc683rNv2
22 | TU13SLY+FJHJIKkBKNrG3KioS/xLaL5soPBVDOYfrhrbcQv+g4lOgu1m8PITffOR
23 | uHcVmTa4Egyi5fUp1DWfPuHdQdJJnsulfjxQYryNNmOQS4yc1oDT7DDvNzIqgmrc
24 | D9620HUCgYAQo4xjNtFFH8/enegUNxyxytMrAYTie33acHXbq0Xmv7S1x5bo8BuS
25 | iHd/Hi33kqFfwFe9FcM/qcHagfMu76cFC7PdTRVDxhFXu9qoQs9u7ZlnfuYNRAzs
26 | jv3+xXKcBmwVc2+8wHbGAuz3oIg886Atlyx8b0nBcAPFwvgimpgKcg==
27 | -----END RSA PRIVATE KEY-----
--------------------------------------------------------------------------------
/docker/volume.go:
--------------------------------------------------------------------------------
1 | package docker
2 |
3 | import (
4 | "context"
5 | "strings"
6 |
7 | "github.com/docker/docker/api/types"
8 | "github.com/docker/docker/api/types/filters"
9 | volumetypes "github.com/docker/docker/api/types/volume"
10 | )
11 |
12 | // Volumes get volumes
13 | func (d *Docker) Volumes() ([]*types.Volume, error) {
14 | res, err := d.VolumeList(context.TODO(), filters.Args{})
15 | if err != nil {
16 | return nil, err
17 | }
18 |
19 | return res.Volumes, nil
20 | }
21 |
22 | // InspectVolume inspect volume
23 | func (d *Docker) InspectVolume(name string) (types.Volume, error) {
24 | volume, _, err := d.VolumeInspectWithRaw(context.TODO(), name)
25 | return volume, err
26 | }
27 |
28 | // RemoveVolume remove volume
29 | func (d *Docker) RemoveVolume(name string) error {
30 | return d.VolumeRemove(context.TODO(), name, false)
31 | }
32 |
33 | // PruneVolumes remove unused volume
34 | func (d *Docker) PruneVolumes() error {
35 | _, err := d.VolumesPrune(context.TODO(), filters.Args{})
36 | return err
37 | }
38 |
39 | // CreateVolume create volume
40 | func (d *Docker) CreateVolume(opt volumetypes.VolumeCreateBody) error {
41 | _, err := d.VolumeCreate(context.TODO(), opt)
42 | return err
43 | }
44 |
45 | // NewCreateVolumeOptions generate options to create volume
46 | func (d *Docker) NewCreateVolumeOptions(data map[string]string) volumetypes.VolumeCreateBody {
47 | driverOpts := make(map[string]string)
48 | labels := make(map[string]string)
49 |
50 | for _, label := range strings.Split(data["Labels"], " ") {
51 | kv := strings.SplitN(label, "=", 2)
52 |
53 | if len(kv) > 1 && kv[1] != "" {
54 | labels[kv[0]] = kv[1]
55 | }
56 | }
57 |
58 | for _, opt := range strings.Split(data["Options"], " ") {
59 | kv := strings.SplitN(opt, "=", 2)
60 |
61 | if len(kv) > 1 && kv[1] != "" {
62 | driverOpts[kv[0]] = kv[1]
63 | }
64 | }
65 |
66 | return volumetypes.VolumeCreateBody{
67 | Name: data["Name"],
68 | Driver: data["Driver"],
69 | DriverOpts: driverOpts,
70 | Labels: labels,
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/skanehira/docui
2 |
3 | require (
4 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
5 | github.com/Microsoft/go-winio v0.4.11 // indirect
6 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
7 | github.com/docker/distribution v2.7.1+incompatible // indirect
8 | github.com/docker/docker v0.7.3-0.20190111153827-295413c9d0e1
9 | github.com/docker/go-connections v0.4.0
10 | github.com/docker/go-units v0.3.3 // indirect
11 | github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1
12 | github.com/gogo/protobuf v1.2.0 // indirect
13 | github.com/google/go-cmp v0.2.0 // indirect
14 | github.com/gorilla/mux v1.7.0 // indirect
15 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
16 | github.com/mattn/go-runewidth v0.0.13
17 | github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
18 | github.com/opencontainers/image-spec v1.0.1 // indirect
19 | github.com/pkg/errors v0.8.1 // indirect
20 | github.com/rivo/tview v0.0.0-20211109175620-badfa0f0b301
21 | github.com/sirupsen/logrus v1.4.1
22 | github.com/stretchr/testify v1.3.0 // indirect
23 | golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6 // indirect
24 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect
25 | golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect
26 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
27 | golang.org/x/text v0.3.7 // indirect
28 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
29 | google.golang.org/grpc v1.18.0 // indirect
30 | gotest.tools v2.2.0+incompatible // indirect
31 | )
32 |
33 | go 1.13
34 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
3 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
4 | github.com/Microsoft/go-winio v0.4.11 h1:zoIOcVf0xPN1tnMVbTtEdI+P8OofVk3NObnwOQ6nK2Q=
5 | github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
6 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
7 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
8 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12 | github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
13 | github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
14 | github.com/docker/docker v0.7.3-0.20190111153827-295413c9d0e1 h1:5Z3Uksuiv0lpPslfRA25dYUV85hI+Pfvz/Pi1NM2wPA=
15 | github.com/docker/docker v0.7.3-0.20190111153827-295413c9d0e1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
16 | github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
17 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
18 | github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
19 | github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
20 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
21 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
22 | github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1 h1:QqwPZCwh/k1uYqq6uXSb9TRDhTkfQbO80v8zhnIe5zM=
23 | github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04=
24 | github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI=
25 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
26 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
27 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
28 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
29 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
30 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
31 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
32 | github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U=
33 | github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
34 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
35 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
36 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
37 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
38 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
39 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
40 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
41 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
42 | github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
43 | github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
44 | github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
45 | github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
46 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
47 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
48 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
49 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
50 | github.com/rivo/tview v0.0.0-20211109175620-badfa0f0b301 h1:FbY5ESMtIcvCyBUQyI9QJUZFe3JtVfTUzBzG1CjrCDY=
51 | github.com/rivo/tview v0.0.0-20211109175620-badfa0f0b301/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk=
52 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
53 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
54 | github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
55 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
56 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
57 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
58 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
59 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
60 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
61 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
62 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
63 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
64 | golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6 h1:FP8hkuE6yUEaJnK7O2eTuejKWwW+Rhfj80dQ2JcKxCU=
65 | golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
66 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
67 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
68 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
69 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
70 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
71 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
72 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
73 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
74 | golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
75 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
76 | golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI=
77 | golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
78 | golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
79 | golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
80 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
81 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
82 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
83 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
84 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
85 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
86 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
87 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
88 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
89 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
90 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
91 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
92 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
93 | google.golang.org/grpc v1.18.0 h1:IZl7mfBGfbhYx2p2rKRtYgDFw6SBz+kclmxYrCksPPA=
94 | google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
95 | gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
96 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
97 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
98 |
--------------------------------------------------------------------------------
/gui/containers.go:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import (
4 | "strings"
5 | "time"
6 |
7 | "github.com/docker/docker/api/types"
8 | "github.com/gdamore/tcell/v2"
9 | "github.com/rivo/tview"
10 | "github.com/skanehira/docui/common"
11 | "github.com/skanehira/docui/docker"
12 | )
13 |
14 | type container struct {
15 | ID string
16 | Name string
17 | Image string
18 | Status string
19 | Created string
20 | Port string
21 | }
22 |
23 | type containers struct {
24 | *tview.Table
25 | filterWord string
26 | }
27 |
28 | func newContainers(g *Gui) *containers {
29 | containers := &containers{
30 | Table: tview.NewTable().SetSelectable(true, false).Select(0, 0).SetFixed(1, 1),
31 | }
32 |
33 | containers.SetTitle("container list").SetTitleAlign(tview.AlignLeft)
34 | containers.SetBorder(true)
35 | containers.setEntries(g)
36 | containers.setKeybinding(g)
37 | return containers
38 | }
39 |
40 | func (c *containers) name() string {
41 | return "containers"
42 | }
43 |
44 | func (c *containers) setKeybinding(g *Gui) {
45 | c.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
46 | g.setGlobalKeybinding(event)
47 | switch event.Key() {
48 | case tcell.KeyEnter:
49 | g.inspectContainer()
50 | case tcell.KeyCtrlE:
51 | g.attachContainerForm()
52 | case tcell.KeyCtrlL:
53 | g.tailContainerLog()
54 | case tcell.KeyCtrlK:
55 | g.killContainer()
56 | case tcell.KeyCtrlR:
57 | c.setEntries(g)
58 | }
59 |
60 | switch event.Rune() {
61 | case 'd':
62 | g.removeContainer()
63 | case 'r':
64 | g.renameContainerForm()
65 | case 'u':
66 | g.startContainer()
67 | case 's':
68 | g.stopContainer()
69 | case 'e':
70 | g.exportContainerForm()
71 | case 'c':
72 | g.commitContainerForm()
73 | }
74 |
75 | return event
76 | })
77 | }
78 |
79 | func (c *containers) entries(g *Gui) {
80 | containers, err := docker.Client.Containers(types.ContainerListOptions{All: true})
81 | if err != nil {
82 | return
83 | }
84 |
85 | g.state.resources.containers = make([]*container, 0)
86 |
87 | for _, con := range containers {
88 | if strings.Index(con.Names[0][1:], c.filterWord) == -1 {
89 | continue
90 | }
91 |
92 | g.state.resources.containers = append(g.state.resources.containers, &container{
93 | ID: con.ID[:12],
94 | Image: con.Image,
95 | Name: con.Names[0][1:],
96 | Status: con.Status,
97 | Created: common.ParseDateToString(con.Created),
98 | Port: common.ParsePortToString(con.Ports),
99 | })
100 | }
101 | }
102 |
103 | func (c *containers) setEntries(g *Gui) {
104 | c.entries(g)
105 | table := c.Clear()
106 |
107 | headers := []string{
108 | "ID",
109 | "Name",
110 | "Image",
111 | "Status",
112 | "Created",
113 | "Port",
114 | }
115 |
116 | for i, header := range headers {
117 | table.SetCell(0, i, &tview.TableCell{
118 | Text: header,
119 | NotSelectable: true,
120 | Align: tview.AlignLeft,
121 | Color: tcell.ColorWhite,
122 | BackgroundColor: tcell.ColorDefault,
123 | Attributes: tcell.AttrBold,
124 | })
125 | }
126 |
127 | for i, container := range g.state.resources.containers {
128 | table.SetCell(i+1, 0, tview.NewTableCell(container.ID).
129 | SetTextColor(tcell.ColorLightGreen).
130 | SetMaxWidth(1).
131 | SetExpansion(1))
132 |
133 | table.SetCell(i+1, 1, tview.NewTableCell(container.Name).
134 | SetTextColor(tcell.ColorLightGreen).
135 | SetMaxWidth(1).
136 | SetExpansion(1))
137 |
138 | table.SetCell(i+1, 2, tview.NewTableCell(container.Image).
139 | SetTextColor(tcell.ColorLightGreen).
140 | SetMaxWidth(1).
141 | SetExpansion(1))
142 |
143 | table.SetCell(i+1, 3, tview.NewTableCell(container.Status).
144 | SetTextColor(tcell.ColorLightGreen).
145 | SetMaxWidth(1).
146 | SetExpansion(1))
147 |
148 | table.SetCell(i+1, 4, tview.NewTableCell(container.Created).
149 | SetTextColor(tcell.ColorLightGreen).
150 | SetMaxWidth(1).
151 | SetExpansion(1))
152 |
153 | table.SetCell(i+1, 5, tview.NewTableCell(container.Port).
154 | SetTextColor(tcell.ColorLightGreen).
155 | SetMaxWidth(1).
156 | SetExpansion(1))
157 | }
158 | }
159 |
160 | func (c *containers) focus(g *Gui) {
161 | c.SetSelectable(true, false)
162 | g.app.SetFocus(c)
163 | }
164 |
165 | func (c *containers) unfocus() {
166 | c.SetSelectable(false, false)
167 | }
168 |
169 | func (c *containers) updateEntries(g *Gui) {
170 | go g.app.QueueUpdateDraw(func() {
171 | c.setEntries(g)
172 | })
173 | }
174 |
175 | func (c *containers) setFilterWord(word string) {
176 | c.filterWord = word
177 | }
178 |
179 | func (c *containers) monitoringContainers(g *Gui) {
180 | common.Logger.Info("start monitoring containers")
181 | ticker := time.NewTicker(5 * time.Second)
182 |
183 | LOOP:
184 | for {
185 | select {
186 | case <-ticker.C:
187 | c.updateEntries(g)
188 | case <-g.state.stopChans["container"]:
189 | ticker.Stop()
190 | break LOOP
191 | }
192 | }
193 | common.Logger.Info("stop monitoring containers")
194 | }
195 |
--------------------------------------------------------------------------------
/gui/gui.go:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/rivo/tview"
7 | "github.com/skanehira/docui/common"
8 | )
9 |
10 | type panels struct {
11 | currentPanel int
12 | panel []panel
13 | }
14 |
15 | // docker resources
16 | type resources struct {
17 | images []*image
18 | containers []*container
19 | networks []*network
20 | volumes []*volume
21 | tasks []*task
22 | }
23 |
24 | type state struct {
25 | panels panels
26 | navigate *navigate
27 | resources resources
28 | stopChans map[string]chan int
29 | }
30 |
31 | func newState() *state {
32 | return &state{
33 | stopChans: make(map[string]chan int),
34 | }
35 | }
36 |
37 | // Gui have all panels
38 | type Gui struct {
39 | app *tview.Application
40 | pages *tview.Pages
41 | state *state
42 | }
43 |
44 | // New create new gui
45 | func New() *Gui {
46 | return &Gui{
47 | app: tview.NewApplication(),
48 | state: newState(),
49 | }
50 | }
51 |
52 | func (g *Gui) imagePanel() *images {
53 | for _, panel := range g.state.panels.panel {
54 | if panel.name() == "images" {
55 | return panel.(*images)
56 | }
57 | }
58 | return nil
59 | }
60 |
61 | func (g *Gui) containerPanel() *containers {
62 | for _, panel := range g.state.panels.panel {
63 | if panel.name() == "containers" {
64 | return panel.(*containers)
65 | }
66 | }
67 | return nil
68 | }
69 |
70 | func (g *Gui) volumePanel() *volumes {
71 | for _, panel := range g.state.panels.panel {
72 | if panel.name() == "volumes" {
73 | return panel.(*volumes)
74 | }
75 | }
76 | return nil
77 | }
78 |
79 | func (g *Gui) networkPanel() *networks {
80 | for _, panel := range g.state.panels.panel {
81 | if panel.name() == "networks" {
82 | return panel.(*networks)
83 | }
84 | }
85 | return nil
86 | }
87 |
88 | func (g *Gui) taskPanel() *tasks {
89 | for _, panel := range g.state.panels.panel {
90 | if panel.name() == "tasks" {
91 | return panel.(*tasks)
92 | }
93 | }
94 | return nil
95 | }
96 |
97 | func (g *Gui) monitoringTask() {
98 | common.Logger.Info("start monitoring task")
99 | LOOP:
100 | for {
101 | select {
102 | case task := <-g.taskPanel().tasks:
103 | go func() {
104 | if err := task.Func(task.Ctx); err != nil {
105 | task.Status = err.Error()
106 | } else {
107 | task.Status = success
108 | }
109 | g.updateTask()
110 | }()
111 | case <-g.state.stopChans["task"]:
112 | common.Logger.Info("stop monitoring task")
113 | break LOOP
114 | }
115 | }
116 | }
117 |
118 | func (g *Gui) startTask(taskName string, f func(ctx context.Context) error) {
119 | ctx, cancel := context.WithCancel(context.Background())
120 |
121 | task := &task{
122 | Name: taskName,
123 | Status: executing,
124 | Created: common.DateNow(),
125 | Func: f,
126 | Ctx: ctx,
127 | Cancel: cancel,
128 | }
129 |
130 | g.state.resources.tasks = append(g.state.resources.tasks, task)
131 | g.updateTask()
132 | g.taskPanel().tasks <- task
133 | }
134 |
135 | func (g *Gui) cancelTask() {
136 | taskPanel := g.taskPanel()
137 | row, _ := taskPanel.GetSelection()
138 |
139 | task := g.state.resources.tasks[row-1]
140 | if task.Status == executing {
141 | task.Cancel()
142 | task.Status = cancel
143 | g.updateTask()
144 | }
145 | }
146 |
147 | func (g *Gui) updateTask() {
148 | go g.app.QueueUpdateDraw(func() {
149 | g.taskPanel().setEntries(g)
150 | })
151 | }
152 |
153 | func (g *Gui) initPanels() {
154 | tasks := newTasks(g)
155 | images := newImages(g)
156 | containers := newContainers(g)
157 | volumes := newVolumes(g)
158 | networks := newNetworks(g)
159 | info := newInfo()
160 | navi := newNavigate()
161 |
162 | g.state.panels.panel = append(g.state.panels.panel, tasks)
163 | g.state.panels.panel = append(g.state.panels.panel, images)
164 | g.state.panels.panel = append(g.state.panels.panel, containers)
165 | g.state.panels.panel = append(g.state.panels.panel, volumes)
166 | g.state.panels.panel = append(g.state.panels.panel, networks)
167 | g.state.navigate = navi
168 |
169 | grid := tview.NewGrid().SetRows(2, 0, 0, 0, 0, 0, 2).
170 | AddItem(info, 0, 0, 1, 1, 0, 0, true).
171 | AddItem(tasks, 1, 0, 1, 1, 0, 0, true).
172 | AddItem(images, 2, 0, 1, 1, 0, 0, true).
173 | AddItem(containers, 3, 0, 1, 1, 0, 0, true).
174 | AddItem(volumes, 4, 0, 1, 1, 0, 0, true).
175 | AddItem(networks, 5, 0, 1, 1, 0, 0, true).
176 | AddItem(navi, 6, 0, 1, 1, 0, 0, true)
177 |
178 | g.pages = tview.NewPages().
179 | AddAndSwitchToPage("main", grid, true)
180 |
181 | g.app.SetRoot(g.pages, true)
182 | g.switchPanel("images")
183 | }
184 |
185 | func (g *Gui) startMonitoring() {
186 | stop := make(chan int, 1)
187 | g.state.stopChans["task"] = stop
188 | g.state.stopChans["image"] = stop
189 | g.state.stopChans["volume"] = stop
190 | g.state.stopChans["network"] = stop
191 | g.state.stopChans["container"] = stop
192 | go g.monitoringTask()
193 | go g.imagePanel().monitoringImages(g)
194 | go g.networkPanel().monitoringNetworks(g)
195 | go g.volumePanel().monitoringVolumes(g)
196 | go g.containerPanel().monitoringContainers(g)
197 | }
198 |
199 | func (g *Gui) stopMonitoring() {
200 | g.state.stopChans["task"] <- 1
201 | g.state.stopChans["image"] <- 1
202 | g.state.stopChans["volume"] <- 1
203 | g.state.stopChans["network"] <- 1
204 | g.state.stopChans["container"] <- 1
205 | }
206 |
207 | // Start start application
208 | func (g *Gui) Start() error {
209 | g.initPanels()
210 | g.startMonitoring()
211 | if err := g.app.Run(); err != nil {
212 | g.app.Stop()
213 | return err
214 | }
215 |
216 | return nil
217 | }
218 |
219 | // Stop stop application
220 | func (g *Gui) Stop() error {
221 | g.stopMonitoring()
222 | g.app.Stop()
223 | return nil
224 | }
225 |
226 | func (g *Gui) selectedImage() *image {
227 | row, _ := g.imagePanel().GetSelection()
228 | if len(g.state.resources.images) == 0 {
229 | return nil
230 | }
231 | if row-1 < 0 {
232 | return nil
233 | }
234 |
235 | return g.state.resources.images[row-1]
236 | }
237 |
238 | func (g *Gui) selectedContainer() *container {
239 | row, _ := g.containerPanel().GetSelection()
240 | if len(g.state.resources.containers) == 0 {
241 | return nil
242 | }
243 | if row-1 < 0 {
244 | return nil
245 | }
246 |
247 | return g.state.resources.containers[row-1]
248 | }
249 |
250 | func (g *Gui) selectedVolume() *volume {
251 | row, _ := g.volumePanel().GetSelection()
252 | if len(g.state.resources.volumes) == 0 {
253 | return nil
254 | }
255 | if row-1 < 0 {
256 | return nil
257 | }
258 |
259 | return g.state.resources.volumes[row-1]
260 | }
261 |
262 | func (g *Gui) selectedNetwork() *network {
263 | row, _ := g.networkPanel().GetSelection()
264 | if len(g.state.resources.networks) == 0 {
265 | return nil
266 | }
267 | if row-1 < 0 {
268 | return nil
269 | }
270 |
271 | return g.state.resources.networks[row-1]
272 | }
273 |
274 | func (g *Gui) message(message, doneLabel, page string, doneFunc func()) {
275 | modal := tview.NewModal().
276 | SetText(message).
277 | AddButtons([]string{doneLabel}).
278 | SetDoneFunc(func(buttonIndex int, buttonLabel string) {
279 | g.closeAndSwitchPanel("modal", page)
280 | if buttonLabel == doneLabel {
281 | doneFunc()
282 | }
283 | })
284 |
285 | g.pages.AddAndSwitchToPage("modal", g.modal(modal, 80, 29), true).ShowPage("main")
286 | }
287 |
288 | func (g *Gui) confirm(message, doneLabel, page string, doneFunc func()) {
289 | modal := tview.NewModal().
290 | SetText(message).
291 | AddButtons([]string{doneLabel, "Cancel"}).
292 | SetDoneFunc(func(buttonIndex int, buttonLabel string) {
293 | g.closeAndSwitchPanel("modal", page)
294 | if buttonLabel == doneLabel {
295 | doneFunc()
296 | }
297 | })
298 |
299 | g.pages.AddAndSwitchToPage("modal", g.modal(modal, 80, 29), true).ShowPage("main")
300 | }
301 |
302 | func (g *Gui) switchPanel(panelName string) {
303 | for i, panel := range g.state.panels.panel {
304 | if panel.name() == panelName {
305 | g.state.navigate.update(panelName)
306 | panel.focus(g)
307 | g.state.panels.currentPanel = i
308 | } else {
309 | panel.unfocus()
310 | }
311 | }
312 | }
313 |
314 | func (g *Gui) closeAndSwitchPanel(removePanel, switchPanel string) {
315 | g.pages.RemovePage(removePanel).ShowPage("main")
316 | g.switchPanel(switchPanel)
317 | }
318 |
319 | func (g *Gui) modal(p tview.Primitive, width, height int) tview.Primitive {
320 | return tview.NewGrid().
321 | SetColumns(0, width, 0).
322 | SetRows(0, height, 0).
323 | AddItem(p, 1, 1, 1, 1, 0, 0, true)
324 | }
325 |
326 | func (g *Gui) currentPanel() panel {
327 | return g.state.panels.panel[g.state.panels.currentPanel]
328 | }
329 |
--------------------------------------------------------------------------------
/gui/images.go:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import (
4 | "strings"
5 | "time"
6 |
7 | "github.com/docker/docker/api/types"
8 | "github.com/gdamore/tcell/v2"
9 | "github.com/rivo/tview"
10 | "github.com/skanehira/docui/common"
11 | "github.com/skanehira/docui/docker"
12 | )
13 |
14 | type image struct {
15 | ID string
16 | Repo string
17 | Tag string
18 | Created string
19 | Size string
20 | }
21 |
22 | type images struct {
23 | *tview.Table
24 | filterWord string
25 | }
26 |
27 | func newImages(g *Gui) *images {
28 | images := &images{
29 | Table: tview.NewTable().SetSelectable(true, false).Select(0, 0).SetFixed(1, 1),
30 | }
31 |
32 | images.SetTitle("image list").SetTitleAlign(tview.AlignLeft)
33 | images.SetBorder(true)
34 | images.setEntries(g)
35 | images.setKeybinding(g)
36 | return images
37 | }
38 |
39 | func (i *images) name() string {
40 | return "images"
41 | }
42 |
43 | func (i *images) setKeybinding(g *Gui) {
44 | i.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
45 | g.setGlobalKeybinding(event)
46 | switch event.Key() {
47 | case tcell.KeyEnter:
48 | g.inspectImage()
49 | case tcell.KeyCtrlL:
50 | g.loadImageForm()
51 | case tcell.KeyCtrlR:
52 | i.setEntries(g)
53 | }
54 |
55 | switch event.Rune() {
56 | case 'c':
57 | g.createContainerForm()
58 | case 'p':
59 | g.pullImageForm()
60 | case 'd':
61 | g.removeImage()
62 | case 'i':
63 | g.importImageForm()
64 | case 's':
65 | g.saveImageForm()
66 | case 'f':
67 | newSearchInputField(g)
68 | }
69 |
70 | return event
71 | })
72 | }
73 |
74 | func (i *images) entries(g *Gui) {
75 | images, err := docker.Client.Images(types.ImageListOptions{})
76 | if err != nil {
77 | return
78 | }
79 |
80 | g.state.resources.images = make([]*image, 0)
81 |
82 | for _, imgInfo := range images {
83 | for _, repoTag := range imgInfo.RepoTags {
84 | repo, tag := common.ParseRepoTag(repoTag)
85 | if strings.Index(repo, i.filterWord) == -1 {
86 | continue
87 | }
88 |
89 | g.state.resources.images = append(g.state.resources.images, &image{
90 | ID: imgInfo.ID[7:19],
91 | Repo: repo,
92 | Tag: tag,
93 | Created: common.ParseDateToString(imgInfo.Created),
94 | Size: common.ParseSizeToString(imgInfo.Size),
95 | })
96 | }
97 | }
98 | }
99 |
100 | func (i *images) setEntries(g *Gui) {
101 | i.entries(g)
102 | table := i.Clear()
103 |
104 | headers := []string{
105 | "ID",
106 | "Repo",
107 | "Tag",
108 | "Created",
109 | "Size",
110 | }
111 |
112 | for i, header := range headers {
113 | table.SetCell(0, i, &tview.TableCell{
114 | Text: header,
115 | NotSelectable: true,
116 | Align: tview.AlignLeft,
117 | Color: tcell.ColorWhite,
118 | BackgroundColor: tcell.ColorDefault,
119 | Attributes: tcell.AttrBold,
120 | })
121 | }
122 |
123 | for i, image := range g.state.resources.images {
124 | table.SetCell(i+1, 0, tview.NewTableCell(image.ID).
125 | SetTextColor(tcell.ColorLightYellow).
126 | SetMaxWidth(1).
127 | SetExpansion(1))
128 |
129 | table.SetCell(i+1, 1, tview.NewTableCell(image.Repo).
130 | SetTextColor(tcell.ColorLightYellow).
131 | SetMaxWidth(1).
132 | SetExpansion(1))
133 |
134 | table.SetCell(i+1, 2, tview.NewTableCell(image.Tag).
135 | SetTextColor(tcell.ColorLightYellow).
136 | SetMaxWidth(1).
137 | SetExpansion(1))
138 |
139 | table.SetCell(i+1, 3, tview.NewTableCell(image.Created).
140 | SetTextColor(tcell.ColorLightYellow).
141 | SetMaxWidth(1).
142 | SetExpansion(1))
143 |
144 | table.SetCell(i+1, 4, tview.NewTableCell(image.Size).
145 | SetTextColor(tcell.ColorLightYellow).
146 | SetMaxWidth(1).
147 | SetExpansion(1))
148 | }
149 | }
150 |
151 | func (i *images) updateEntries(g *Gui) {
152 | go g.app.QueueUpdateDraw(func() {
153 | i.setEntries(g)
154 | })
155 | }
156 |
157 | func (i *images) focus(g *Gui) {
158 | i.SetSelectable(true, false)
159 | g.app.SetFocus(i)
160 | }
161 |
162 | func (i *images) unfocus() {
163 | i.SetSelectable(false, false)
164 | }
165 |
166 | func (i *images) setFilterWord(word string) {
167 | i.filterWord = word
168 | }
169 |
170 | func (i *images) monitoringImages(g *Gui) {
171 | common.Logger.Info("start monitoring images")
172 | ticker := time.NewTicker(5 * time.Second)
173 |
174 | LOOP:
175 | for {
176 | select {
177 | case <-ticker.C:
178 | i.updateEntries(g)
179 | case <-g.state.stopChans["image"]:
180 | ticker.Stop()
181 | break LOOP
182 | }
183 | }
184 | common.Logger.Info("stop monitoring images")
185 | }
186 |
--------------------------------------------------------------------------------
/gui/info.go:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "runtime"
7 |
8 | "github.com/gdamore/tcell/v2"
9 | "github.com/rivo/tview"
10 | "github.com/skanehira/docui/docker"
11 | )
12 |
13 | type info struct {
14 | *tview.TextView
15 | Docker *dockerInfo
16 | Host *hostInfo
17 | Docui *docui
18 | }
19 |
20 | type docui struct {
21 | Name string
22 | Version string
23 | }
24 |
25 | type dockerInfo struct {
26 | HostName string
27 | ServerVersion string
28 | APIVersion string
29 | KernelVersion string
30 | OSType string
31 | Architecture string
32 | Endpoint string
33 | Containers int
34 | Images int
35 | MemTotal string
36 | }
37 |
38 | type hostInfo struct {
39 | OSType string
40 | Architecture string
41 | }
42 |
43 | func newDocuiInfo() *docui {
44 | return &docui{
45 | Name: "docui",
46 | Version: "2.0.4",
47 | }
48 | }
49 |
50 | func newHostInfo() *hostInfo {
51 | return &hostInfo{
52 | OSType: runtime.GOOS,
53 | Architecture: runtime.GOARCH,
54 | }
55 | }
56 |
57 | func newDockerInfo() *dockerInfo {
58 | info, err := docker.Client.Info(context.TODO())
59 | if err != nil {
60 | return nil
61 | }
62 |
63 | var apiVersion string
64 | if v, err := docker.Client.ServerVersion(context.TODO()); err != nil {
65 | apiVersion = ""
66 | } else {
67 | apiVersion = v.APIVersion
68 | }
69 |
70 | return &dockerInfo{
71 | HostName: info.Name,
72 | ServerVersion: info.ServerVersion,
73 | APIVersion: apiVersion,
74 | KernelVersion: info.KernelVersion,
75 | OSType: info.OSType,
76 | Architecture: info.Architecture,
77 | Endpoint: docker.Client.DaemonHost(),
78 | Containers: info.Containers,
79 | Images: info.Images,
80 | MemTotal: fmt.Sprintf("%dMB", info.MemTotal/1024/1024),
81 | }
82 | }
83 |
84 | func newInfo() *info {
85 | i := &info{
86 | TextView: tview.NewTextView(),
87 | Docker: newDockerInfo(),
88 | Host: newHostInfo(),
89 | Docui: newDocuiInfo(),
90 | }
91 |
92 | i.display()
93 |
94 | return i
95 | }
96 |
97 | func (i *info) display() {
98 | dockerAPI := fmt.Sprintf("api version:%s", i.Docker.APIVersion)
99 | dockerVersion := fmt.Sprintf("server version:%s", i.Docker.ServerVersion)
100 | dockerEndpoint := fmt.Sprintf("endpoint:%s", i.Docker.Endpoint)
101 | docuiVersion := fmt.Sprintf("version:%s", i.Docui.Version)
102 |
103 | i.SetTextColor(tcell.ColorYellow)
104 | i.SetText(fmt.Sprintf(" docker\t| %s %s %s\n docui\t | %s", dockerAPI, dockerVersion, dockerEndpoint, docuiVersion))
105 | }
106 |
--------------------------------------------------------------------------------
/gui/keybindings.go:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "os"
9 | "os/signal"
10 | "path/filepath"
11 |
12 | "github.com/docker/docker/api/types"
13 | "github.com/docker/docker/pkg/stdcopy"
14 | "github.com/gdamore/tcell/v2"
15 | "github.com/rivo/tview"
16 | "github.com/skanehira/docui/common"
17 | "github.com/skanehira/docui/docker"
18 | )
19 |
20 | var inputWidth = 70
21 |
22 | func (g *Gui) setGlobalKeybinding(event *tcell.EventKey) {
23 | switch event.Rune() {
24 | case 'h':
25 | g.prevPanel()
26 | case 'l':
27 | g.nextPanel()
28 | case 'q':
29 | g.Stop()
30 | case '/':
31 | g.filter()
32 | }
33 |
34 | switch event.Key() {
35 | case tcell.KeyTab:
36 | g.nextPanel()
37 | case tcell.KeyBacktab:
38 | g.prevPanel()
39 | case tcell.KeyRight:
40 | g.nextPanel()
41 | case tcell.KeyLeft:
42 | g.prevPanel()
43 | }
44 | }
45 |
46 | func (g *Gui) filter() {
47 | currentPanel := g.state.panels.panel[g.state.panels.currentPanel]
48 | if currentPanel.name() == "tasks" {
49 | return
50 | }
51 | currentPanel.setFilterWord("")
52 | currentPanel.updateEntries(g)
53 |
54 | viewName := "filter"
55 | searchInput := tview.NewInputField().SetLabel("Word")
56 | searchInput.SetLabelWidth(6)
57 | searchInput.SetTitle("filter")
58 | searchInput.SetTitleAlign(tview.AlignLeft)
59 | searchInput.SetBorder(true)
60 |
61 | closeSearchInput := func() {
62 | g.closeAndSwitchPanel(viewName, g.state.panels.panel[g.state.panels.currentPanel].name())
63 | }
64 |
65 | searchInput.SetDoneFunc(func(key tcell.Key) {
66 | if key == tcell.KeyEnter {
67 | closeSearchInput()
68 | }
69 | })
70 |
71 | searchInput.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
72 | if event.Key() == tcell.KeyEsc {
73 | closeSearchInput()
74 | }
75 | return event
76 | })
77 |
78 | searchInput.SetChangedFunc(func(text string) {
79 | currentPanel.setFilterWord(text)
80 | currentPanel.updateEntries(g)
81 | })
82 |
83 | g.pages.AddAndSwitchToPage(viewName, g.modal(searchInput, 80, 3), true).ShowPage("main")
84 | }
85 |
86 | func (g *Gui) nextPanel() {
87 | idx := (g.state.panels.currentPanel + 1) % len(g.state.panels.panel)
88 | g.switchPanel(g.state.panels.panel[idx].name())
89 | }
90 |
91 | func (g *Gui) prevPanel() {
92 | g.state.panels.currentPanel--
93 |
94 | if g.state.panels.currentPanel < 0 {
95 | g.state.panels.currentPanel = len(g.state.panels.panel) - 1
96 | }
97 |
98 | idx := (g.state.panels.currentPanel) % len(g.state.panels.panel)
99 | g.switchPanel(g.state.panels.panel[idx].name())
100 | }
101 |
102 | func (g *Gui) createContainerForm() {
103 | selectedImage := g.selectedImage()
104 | if selectedImage == nil {
105 | common.Logger.Error("please input image")
106 | return
107 | }
108 |
109 | image := fmt.Sprintf("%s:%s", selectedImage.Repo, selectedImage.Tag)
110 |
111 | form := tview.NewForm()
112 | form.SetBorder(true)
113 | form.SetTitle("Create container")
114 | form.SetTitleAlign(tview.AlignLeft)
115 |
116 | form.AddInputField("Name", "", inputWidth, nil, nil).
117 | AddInputField("HostIP", "", inputWidth, nil, nil).
118 | AddInputField("HostPort", "", inputWidth, nil, nil).
119 | AddInputField("Port", "", inputWidth, nil, nil).
120 | AddDropDown("VolumeType", []string{"bind", "volume"}, 0, func(option string, optionIndex int) {}).
121 | AddInputField("HostVolume", "", inputWidth, nil, nil).
122 | AddInputField("Volume", "", inputWidth, nil, nil).
123 | AddInputField("Image", image, inputWidth, nil, nil).
124 | AddInputField("User", "", inputWidth, nil, nil).
125 | AddCheckbox("Attach", false, nil).
126 | AddInputField("Env", "", inputWidth, nil, nil).
127 | AddInputField("Cmd", "", inputWidth, nil, nil).
128 | AddButton("Create", func() {
129 | g.createContainer(form, image)
130 | }).
131 | AddButton("Cancel", func() {
132 | g.closeAndSwitchPanel("form", "images")
133 | })
134 |
135 | g.pages.AddAndSwitchToPage("form", g.modal(form, 80, 29), true).ShowPage("main")
136 | }
137 |
138 | func (g *Gui) createContainer(form *tview.Form, image string) {
139 | g.startTask("create container "+image, func(ctx context.Context) error {
140 | inputLabels := []string{
141 | "Name",
142 | "HostIP",
143 | "Port",
144 | "HostVolume",
145 | "Volume",
146 | "Image",
147 | "User",
148 | }
149 |
150 | var data = make(map[string]string)
151 |
152 | for _, label := range inputLabels {
153 | data[label] = form.GetFormItemByLabel(label).(*tview.InputField).GetText()
154 | }
155 |
156 | _, volumeType := form.GetFormItemByLabel("VolumeType").(*tview.DropDown).
157 | GetCurrentOption()
158 | data["VolumeType"] = volumeType
159 |
160 | isAttach := form.GetFormItemByLabel("Attach").(*tview.Checkbox).IsChecked()
161 |
162 | options, err := docker.Client.NewContainerOptions(data, isAttach)
163 | if err != nil {
164 | common.Logger.Errorf("cannot create container %s", err)
165 | return err
166 | }
167 |
168 | err = docker.Client.CreateContainer(options)
169 | if err != nil {
170 | common.Logger.Errorf("cannot create container %s", err)
171 | return err
172 | }
173 |
174 | g.closeAndSwitchPanel("form", "images")
175 | go g.app.QueueUpdateDraw(func() {
176 | g.containerPanel().setEntries(g)
177 | })
178 |
179 | return nil
180 | })
181 | }
182 |
183 | func (g *Gui) pullImageForm() {
184 | form := tview.NewForm()
185 | form.SetBorder(true)
186 | form.SetTitleAlign(tview.AlignLeft)
187 | form.SetTitle("Pull image")
188 | form.AddInputField("Image", "", inputWidth, nil, nil).
189 | AddButton("Pull", func() {
190 | image := form.GetFormItemByLabel("Image").(*tview.InputField).GetText()
191 | g.pullImage(image, "form", "images")
192 | }).
193 | AddButton("Cancel", func() {
194 | g.closeAndSwitchPanel("form", "images")
195 | })
196 |
197 | g.pages.AddAndSwitchToPage("form", g.modal(form, 80, 7), true).ShowPage("main")
198 | }
199 |
200 | func (g *Gui) pullImage(image, closePanel, switchPanel string) {
201 | g.startTask("Pull image "+image, func(ctx context.Context) error {
202 | g.closeAndSwitchPanel(closePanel, switchPanel)
203 | err := docker.Client.PullImage(image)
204 | if err != nil {
205 | common.Logger.Errorf("cannot pull an image %s", err)
206 | return err
207 | }
208 |
209 | g.imagePanel().updateEntries(g)
210 |
211 | return nil
212 | })
213 | }
214 |
215 | func (g *Gui) displayInspect(data, page string) {
216 | text := tview.NewTextView()
217 | text.SetTitle("Detail").SetTitleAlign(tview.AlignLeft)
218 | text.SetBorder(true)
219 | text.SetText(data)
220 |
221 | text.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
222 | if event.Key() == tcell.KeyEsc || event.Rune() == 'q' {
223 | g.closeAndSwitchPanel("detail", page)
224 | }
225 | return event
226 | })
227 |
228 | g.pages.AddAndSwitchToPage("detail", text, true)
229 | }
230 |
231 | func (g *Gui) inspectImage() {
232 | image := g.selectedImage()
233 |
234 | inspect, err := docker.Client.InspectImage(image.ID)
235 | if err != nil {
236 | common.Logger.Errorf("cannot inspect image %s", err)
237 | return
238 | }
239 |
240 | g.displayInspect(common.StructToJSON(inspect), "images")
241 | }
242 |
243 | func (g *Gui) renameContainerForm() {
244 | form := tview.NewForm()
245 | form.SetBorder(true)
246 | form.SetTitleAlign(tview.AlignLeft)
247 | form.SetTitle("Rename container")
248 | form.AddInputField("NewName", "", inputWidth, nil, nil).
249 | AddButton("Rename", func() {
250 | image := form.GetFormItemByLabel("NewName").(*tview.InputField).GetText()
251 | g.renameContainer(image, "form", "containers")
252 | }).
253 | AddButton("Cancel", func() {
254 | g.closeAndSwitchPanel("form", "containers")
255 | })
256 |
257 | g.pages.AddAndSwitchToPage("form", g.modal(form, 80, 7), true).ShowPage("main")
258 | }
259 |
260 | func (g *Gui) renameContainer(newName, closePanel, switchPanel string) {
261 | g.startTask("Renaming container "+newName, func(ctx context.Context) error {
262 | g.closeAndSwitchPanel(closePanel, switchPanel)
263 | oldContainer := g.selectedContainer()
264 | if oldContainer == nil {
265 | err := errors.New("specified container is nil")
266 | common.Logger.Errorf("cannot rename container %s", err)
267 | return err
268 | }
269 |
270 | err := docker.Client.RenameContainer(oldContainer.ID, newName)
271 | if err != nil {
272 | common.Logger.Errorf("cannot create container %s", err)
273 | return err
274 | }
275 |
276 | g.containerPanel().updateEntries(g)
277 |
278 | return nil
279 | })
280 | }
281 |
282 | func (g *Gui) inspectContainer() {
283 | container := g.selectedContainer()
284 |
285 | inspect, err := docker.Client.InspectContainer(container.ID)
286 | if err != nil {
287 | common.Logger.Errorf("cannot inspect container %s", err)
288 | return
289 | }
290 |
291 | g.displayInspect(common.StructToJSON(inspect), "containers")
292 | }
293 |
294 | func (g *Gui) inspectVolume() {
295 | volume := g.selectedVolume()
296 |
297 | inspect, err := docker.Client.InspectVolume(volume.Name)
298 | if err != nil {
299 | common.Logger.Errorf("cannot inspect volume %s", err)
300 | return
301 | }
302 |
303 | g.displayInspect(common.StructToJSON(inspect), "volumes")
304 | }
305 |
306 | func (g *Gui) inspectNetwork() {
307 | network := g.selectedNetwork()
308 |
309 | inspect, err := docker.Client.InspectNetwork(network.ID)
310 | if err != nil {
311 | common.Logger.Errorf("cannot inspect network %s", err)
312 | return
313 | }
314 |
315 | g.displayInspect(common.StructToJSON(inspect), "networks")
316 | }
317 |
318 | func (g *Gui) removeImage() {
319 | image := g.selectedImage()
320 |
321 | g.confirm("Do you want to remove the image?", "Done", "images", func() {
322 | g.startTask(fmt.Sprintf("remove image %s:%s", image.Repo, image.Tag), func(ctx context.Context) error {
323 | if err := docker.Client.RemoveImage(image.ID); err != nil {
324 | common.Logger.Errorf("cannot remove the image %s", err)
325 | return err
326 | }
327 | g.imagePanel().updateEntries(g)
328 | return nil
329 | })
330 | })
331 | }
332 |
333 | func (g *Gui) removeContainer() {
334 | container := g.selectedContainer()
335 |
336 | g.confirm("Do you want to remove the container?", "Done", "containers", func() {
337 | g.startTask(fmt.Sprintf("remove container %s", container.Name), func(ctx context.Context) error {
338 | if err := docker.Client.RemoveContainer(container.ID); err != nil {
339 | common.Logger.Errorf("cannot remove the container %s", err)
340 | return err
341 | }
342 | g.containerPanel().updateEntries(g)
343 | return nil
344 | })
345 | })
346 | }
347 |
348 | func (g *Gui) removeVolume() {
349 | volume := g.selectedVolume()
350 |
351 | g.confirm("Do you want to remove the volume?", "Done", "volumes", func() {
352 | g.startTask(fmt.Sprintf("remove volume %s", volume.Name), func(ctx context.Context) error {
353 | if err := docker.Client.RemoveVolume(volume.Name); err != nil {
354 | common.Logger.Errorf("cannot remove the volume %s", err)
355 | return err
356 | }
357 | g.volumePanel().updateEntries(g)
358 | return nil
359 | })
360 | })
361 | }
362 |
363 | func (g *Gui) removeNetwork() {
364 | network := g.selectedNetwork()
365 |
366 | g.confirm("Do you want to remove the network?", "Done", "networks", func() {
367 | g.startTask(fmt.Sprintf("remove network %s", network.Name), func(ctx context.Context) error {
368 | if err := docker.Client.RemoveNetwork(network.ID); err != nil {
369 | common.Logger.Errorf("cannot remove the network %s", err)
370 | return err
371 | }
372 | g.networkPanel().updateEntries(g)
373 | return nil
374 | })
375 | })
376 | }
377 |
378 | func (g *Gui) startContainer() {
379 | container := g.selectedContainer()
380 |
381 | g.startTask(fmt.Sprintf("start container %s", container.Name), func(ctx context.Context) error {
382 | if err := docker.Client.StartContainer(container.ID); err != nil {
383 | common.Logger.Errorf("cannot start container %s", err)
384 | return err
385 | }
386 |
387 | g.containerPanel().updateEntries(g)
388 | return nil
389 | })
390 | }
391 |
392 | func (g *Gui) stopContainer() {
393 | container := g.selectedContainer()
394 |
395 | g.startTask(fmt.Sprintf("stop container %s", container.Name), func(ctx context.Context) error {
396 |
397 | if err := docker.Client.StopContainer(container.ID); err != nil {
398 | common.Logger.Errorf("cannot stop container %s", err)
399 | return err
400 | }
401 |
402 | g.containerPanel().updateEntries(g)
403 | return nil
404 | })
405 | }
406 |
407 | func (g *Gui) exportContainerForm() {
408 | inputWidth := 70
409 |
410 | container := g.selectedContainer()
411 | form := tview.NewForm()
412 | form.SetBorder(true)
413 | form.SetTitleAlign(tview.AlignLeft)
414 | form.SetTitle("Export container")
415 | form.AddInputField("Path", "", inputWidth, nil, nil).
416 | AddInputField("Container", container.Name, inputWidth, nil, nil).
417 | AddButton("Create", func() {
418 | path := form.GetFormItemByLabel("Path").(*tview.InputField).GetText()
419 | container := form.GetFormItemByLabel("Container").(*tview.InputField).GetText()
420 |
421 | g.exportContainer(path, container)
422 | }).
423 | AddButton("Cancel", func() {
424 | g.closeAndSwitchPanel("form", "containers")
425 | })
426 |
427 | g.pages.AddAndSwitchToPage("form", g.modal(form, 80, 9), true).ShowPage("main")
428 | }
429 |
430 | func (g *Gui) exportContainer(path, container string) {
431 | g.startTask("export container "+container, func(ctx context.Context) error {
432 | g.closeAndSwitchPanel("form", "containers")
433 | err := docker.Client.ExportContainer(container, path)
434 | if err != nil {
435 | common.Logger.Errorf("cannot export container %s", err)
436 | return err
437 | }
438 |
439 | return nil
440 | })
441 | }
442 |
443 | func (g *Gui) loadImageForm() {
444 | form := tview.NewForm()
445 | form.SetBorder(true)
446 | form.SetTitleAlign(tview.AlignLeft)
447 | form.SetTitle("Load image")
448 | form.AddInputField("Path", "", inputWidth, nil, nil).
449 | AddButton("Load", func() {
450 | path := form.GetFormItemByLabel("Path").(*tview.InputField).GetText()
451 | g.loadImage(path)
452 | }).
453 | AddButton("Cancel", func() {
454 | g.closeAndSwitchPanel("form", "images")
455 | })
456 |
457 | g.pages.AddAndSwitchToPage("form", g.modal(form, 80, 7), true).ShowPage("main")
458 | }
459 |
460 | func (g *Gui) loadImage(path string) {
461 | g.startTask("load image "+filepath.Base(path), func(ctx context.Context) error {
462 | g.closeAndSwitchPanel("form", "images")
463 | if err := docker.Client.LoadImage(path); err != nil {
464 | common.Logger.Errorf("cannot load image %s", err)
465 | return err
466 | }
467 |
468 | g.imagePanel().updateEntries(g)
469 | return nil
470 | })
471 | }
472 |
473 | func (g *Gui) importImageForm() {
474 | form := tview.NewForm()
475 | form.SetBorder(true)
476 | form.SetTitleAlign(tview.AlignLeft)
477 | form.SetTitle("Import image")
478 | form.AddInputField("Repository", "", inputWidth, nil, nil).
479 | AddInputField("Tag", "", inputWidth, nil, nil).
480 | AddInputField("Path", "", inputWidth, nil, nil).
481 | AddButton("Load", func() {
482 | repository := form.GetFormItemByLabel("Repository").(*tview.InputField).GetText()
483 | tag := form.GetFormItemByLabel("Tag").(*tview.InputField).GetText()
484 | path := form.GetFormItemByLabel("Path").(*tview.InputField).GetText()
485 | g.importImage(path, repository, tag)
486 | }).
487 | AddButton("Cancel", func() {
488 | g.closeAndSwitchPanel("form", "images")
489 | })
490 |
491 | g.pages.AddAndSwitchToPage("form", g.modal(form, 80, 11), true).ShowPage("main")
492 | }
493 |
494 | func (g *Gui) importImage(file, repo, tag string) {
495 | g.startTask("import image "+file, func(ctx context.Context) error {
496 | g.closeAndSwitchPanel("form", "images")
497 |
498 | if err := docker.Client.ImportImage(repo, tag, file); err != nil {
499 | common.Logger.Errorf("cannot load image %s", err)
500 | return err
501 | }
502 |
503 | g.imagePanel().updateEntries(g)
504 | return nil
505 | })
506 | }
507 |
508 | func (g *Gui) saveImageForm() {
509 | image := g.selectedImage()
510 | imageName := fmt.Sprintf("%s:%s", image.Repo, image.Tag)
511 |
512 | form := tview.NewForm()
513 | form.SetBorder(true)
514 | form.SetTitleAlign(tview.AlignLeft)
515 | form.SetTitle("Save image")
516 | form.AddInputField("Path", "", inputWidth, nil, nil).
517 | AddInputField("Image", imageName, inputWidth, nil, nil).
518 | AddButton("Save", func() {
519 | image := form.GetFormItemByLabel("Image").(*tview.InputField).GetText()
520 | path := form.GetFormItemByLabel("Path").(*tview.InputField).GetText()
521 | g.saveImage(image, path)
522 | }).
523 | AddButton("Cancel", func() {
524 | g.closeAndSwitchPanel("form", "images")
525 | })
526 |
527 | g.pages.AddAndSwitchToPage("form", g.modal(form, 80, 9), true).ShowPage("main")
528 |
529 | }
530 |
531 | func (g *Gui) saveImage(image, path string) {
532 | g.startTask("save image "+image, func(ctx context.Context) error {
533 | g.closeAndSwitchPanel("form", "images")
534 |
535 | if err := docker.Client.SaveImage([]string{image}, path); err != nil {
536 | common.Logger.Errorf("cannot save image %s", err)
537 | return err
538 | }
539 | return nil
540 | })
541 |
542 | }
543 |
544 | func (g *Gui) commitContainerForm() {
545 | container := g.selectedContainer()
546 |
547 | form := tview.NewForm()
548 | form.SetBorder(true)
549 | form.SetTitleAlign(tview.AlignLeft)
550 | form.SetTitle("Commit container")
551 | form.AddInputField("Repository", "", inputWidth, nil, nil).
552 | AddInputField("Tag", "", inputWidth, nil, nil).
553 | AddInputField("Container", container.Name, inputWidth, nil, nil).
554 | AddButton("Commit", func() {
555 | repo := form.GetFormItemByLabel("Repository").(*tview.InputField).GetText()
556 | tag := form.GetFormItemByLabel("Tag").(*tview.InputField).GetText()
557 | con := form.GetFormItemByLabel("Container").(*tview.InputField).GetText()
558 | g.commitContainer(repo, tag, con)
559 | }).
560 | AddButton("Cancel", func() {
561 | g.closeAndSwitchPanel("form", "containers")
562 | })
563 |
564 | g.pages.AddAndSwitchToPage("form", g.modal(form, 80, 11), true).ShowPage("main")
565 | }
566 |
567 | func (g *Gui) commitContainer(repo, tag, container string) {
568 | g.startTask("commit container "+container, func(ctx context.Context) error {
569 | g.closeAndSwitchPanel("form", "containers")
570 |
571 | if err := docker.Client.CommitContainer(container, types.ContainerCommitOptions{Reference: repo + ":" + tag}); err != nil {
572 | common.Logger.Errorf("cannot commit container %s", err)
573 | return err
574 | }
575 |
576 | g.imagePanel().updateEntries(g)
577 | return nil
578 | })
579 | }
580 |
581 | func (g *Gui) attachContainerForm() {
582 | form := tview.NewForm()
583 | form.SetBorder(true)
584 | form.SetTitleAlign(tview.AlignLeft)
585 | form.SetTitle("Exec container")
586 | form.AddInputField("Cmd", "", inputWidth, nil, nil).
587 | AddButton("Exec", func() {
588 | cmd := form.GetFormItemByLabel("Cmd").(*tview.InputField).GetText()
589 | g.attachContainer(g.selectedContainer().ID, cmd)
590 | }).
591 | AddButton("Cancel", func() {
592 | g.closeAndSwitchPanel("form", "containers")
593 | })
594 |
595 | g.pages.AddAndSwitchToPage("form", g.modal(form, 80, 7), true).ShowPage("main")
596 | }
597 |
598 | func (g *Gui) attachContainer(container, cmd string) {
599 | g.closeAndSwitchPanel("form", "containers")
600 |
601 | if !g.app.Suspend(func() {
602 | g.stopMonitoring()
603 | if err := docker.Client.AttachExecContainer(container, cmd); err != nil {
604 | common.Logger.Errorf("cannot attach container %s", err)
605 | }
606 |
607 | g.startMonitoring()
608 | }) {
609 | common.Logger.Error("cannot suspend tview")
610 | }
611 | }
612 |
613 | func (g *Gui) createVolumeForm() {
614 | form := tview.NewForm()
615 | form.SetBorder(true)
616 | form.SetTitleAlign(tview.AlignLeft)
617 | form.SetTitle("Create volume")
618 | form.AddInputField("Name", "", inputWidth, nil, nil).
619 | AddInputField("Labels", "", inputWidth, nil, nil).
620 | AddInputField("Driver", "", inputWidth, nil, nil).
621 | AddInputField("Options", "", inputWidth, nil, nil).
622 | AddButton("Create", func() {
623 | g.createVolume(form)
624 | }).
625 | AddButton("Cancel", func() {
626 | g.closeAndSwitchPanel("form", "volumes")
627 | })
628 |
629 | g.pages.AddAndSwitchToPage("form", g.modal(form, 80, 13), true).ShowPage("main")
630 | }
631 |
632 | func (g *Gui) createVolume(form *tview.Form) {
633 | var data = make(map[string]string)
634 | inputLabels := []string{
635 | "Name",
636 | "Labels",
637 | "Driver",
638 | "Options",
639 | }
640 |
641 | for _, label := range inputLabels {
642 | data[label] = form.GetFormItemByLabel(label).(*tview.InputField).GetText()
643 | }
644 |
645 | g.startTask("create volume "+data["Name"], func(ctx context.Context) error {
646 | options := docker.Client.NewCreateVolumeOptions(data)
647 |
648 | if err := docker.Client.CreateVolume(options); err != nil {
649 | common.Logger.Errorf("cannot create volume %s", err)
650 | return err
651 | }
652 |
653 | g.closeAndSwitchPanel("form", "volumes")
654 | go g.app.QueueUpdateDraw(func() {
655 | g.volumePanel().setEntries(g)
656 | })
657 |
658 | return nil
659 | })
660 | }
661 |
662 | func (g *Gui) tailContainerLog() {
663 | container := g.selectedContainer()
664 | if container == nil {
665 | common.Logger.Errorf("cannot start tail container: selected container is null")
666 | return
667 | }
668 |
669 | if !g.app.Suspend(func() {
670 | sigint := make(chan os.Signal, 1)
671 | signal.Notify(sigint, os.Interrupt)
672 | errCh := make(chan error)
673 |
674 | var reader io.ReadCloser
675 | var err error
676 |
677 | go func() {
678 | reader, err = docker.Client.ContainerLogStream(container.ID)
679 | if err != nil {
680 | common.Logger.Error(err)
681 | errCh <- err
682 | }
683 | defer reader.Close()
684 |
685 | _, err = stdcopy.StdCopy(os.Stdout, os.Stderr, reader)
686 | if err != nil {
687 | common.Logger.Error(err)
688 | errCh <- err
689 | }
690 | return
691 | }()
692 |
693 | select {
694 | case err := <-errCh:
695 | common.Logger.Error(err)
696 | reader.Close()
697 | return
698 | case <-sigint:
699 | reader.Close()
700 | return
701 | }
702 | }) {
703 | common.Logger.Error("cannot suspend tview")
704 | }
705 | }
706 |
707 | func (g *Gui) killContainer() {
708 | container := g.selectedContainer()
709 | if container == nil {
710 | common.Logger.Errorf("cannot kill container: selected container is null")
711 | return
712 | }
713 |
714 | g.confirm("Do you want to kill the container?", "Done", "containers", func() {
715 | g.startTask(fmt.Sprintf("kill container %s", container.Name), func(ctx context.Context) error {
716 | if err := docker.Client.KillContainer(container.ID); err != nil {
717 | common.Logger.Errorf("cannot kill the container %s", err)
718 | return err
719 | }
720 | g.containerPanel().updateEntries(g)
721 | return nil
722 | })
723 | })
724 | }
725 |
--------------------------------------------------------------------------------
/gui/navigate.go:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import (
4 | "github.com/gdamore/tcell/v2"
5 | "github.com/rivo/tview"
6 | )
7 |
8 | type navigate struct {
9 | *tview.TextView
10 | keybindings map[string]string
11 | }
12 |
13 | func newNavigate() *navigate {
14 | return &navigate{
15 | TextView: tview.NewTextView().SetTextColor(tcell.ColorYellow),
16 | keybindings: map[string]string{
17 | "images": " p: pull image, i: import image, s: save image, Ctrl+l: load image, f: search image, /: filter d: remove image,\n c: create container, Enter: inspect image, Ctrl+r: refresh images list",
18 | "containers": " e: export container, c: commit container, /: filter, Ctrl+e: exec container cmd u: start container, s: stop container,\n Ctrl+k: kill container, d: remove container, Enter: inspect container, Ctrl+r: refresh container list, Ctrl+l: show container logs",
19 | "networks": " d: remove network, Enter: inspect network, /: filter",
20 | "volumes": " c: create volume, d: remove volume\n /: filter, Enter: inspect volume, Ctrl+r: refresh volume list",
21 | },
22 | }
23 | }
24 |
25 | func (n *navigate) update(panel string) {
26 | n.SetText(n.keybindings[panel])
27 | }
28 |
--------------------------------------------------------------------------------
/gui/networks.go:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "time"
7 |
8 | "github.com/docker/docker/api/types"
9 | "github.com/gdamore/tcell/v2"
10 | "github.com/rivo/tview"
11 | "github.com/skanehira/docui/common"
12 | "github.com/skanehira/docui/docker"
13 | )
14 |
15 | type network struct {
16 | ID string
17 | Name string
18 | Driver string
19 | Scope string
20 | containers string
21 | }
22 |
23 | type networks struct {
24 | *tview.Table
25 | filterWord string
26 | }
27 |
28 | func newNetworks(g *Gui) *networks {
29 | networks := &networks{
30 | Table: tview.NewTable().SetSelectable(true, false).Select(0, 0).SetFixed(1, 1),
31 | }
32 |
33 | networks.SetTitle("network list").SetTitleAlign(tview.AlignLeft)
34 | networks.SetBorder(true)
35 | networks.setEntries(g)
36 | networks.setKeybinding(g)
37 | return networks
38 | }
39 |
40 | func (n *networks) name() string {
41 | return "networks"
42 | }
43 |
44 | func (n *networks) setKeybinding(g *Gui) {
45 | n.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
46 | g.setGlobalKeybinding(event)
47 | switch event.Key() {
48 | case tcell.KeyEnter:
49 | g.inspectNetwork()
50 | case tcell.KeyCtrlR:
51 | n.setEntries(g)
52 | }
53 |
54 | switch event.Rune() {
55 | case 'd':
56 | g.removeNetwork()
57 | }
58 |
59 | return event
60 | })
61 | }
62 |
63 | func (n *networks) entries(g *Gui) {
64 | networks, err := docker.Client.Networks(types.NetworkListOptions{})
65 | if err != nil {
66 | common.Logger.Error(err)
67 | return
68 | }
69 |
70 | keys := make([]string, 0, len(networks))
71 | tmpMap := make(map[string]*network)
72 |
73 | for _, net := range networks {
74 | if strings.Index(net.Name, n.filterWord) == -1 {
75 | continue
76 | }
77 |
78 | var containers string
79 |
80 | net, err := docker.Client.InspectNetwork(net.ID)
81 | if err != nil {
82 | common.Logger.Error(err)
83 | continue
84 | }
85 |
86 | for _, endpoint := range net.Containers {
87 | containers += fmt.Sprintf("%s ", endpoint.Name)
88 | }
89 |
90 | tmpMap[net.ID[:12]] = &network{
91 | ID: net.ID,
92 | Name: net.Name,
93 | Driver: net.Driver,
94 | Scope: net.Scope,
95 | containers: containers,
96 | }
97 |
98 | keys = append(keys, net.ID[:12])
99 |
100 | }
101 |
102 | g.state.resources.networks = make([]*network, 0)
103 | for _, key := range common.SortKeys(keys) {
104 | g.state.resources.networks = append(g.state.resources.networks, tmpMap[key])
105 | }
106 | }
107 |
108 | func (n *networks) setEntries(g *Gui) {
109 | n.entries(g)
110 | table := n.Clear()
111 |
112 | headers := []string{
113 | "ID",
114 | "Name",
115 | "Driver",
116 | "Scope",
117 | "Containers",
118 | }
119 |
120 | for i, header := range headers {
121 | table.SetCell(0, i, &tview.TableCell{
122 | Text: header,
123 | NotSelectable: true,
124 | Align: tview.AlignLeft,
125 | Color: tcell.ColorWhite,
126 | BackgroundColor: tcell.ColorDefault,
127 | Attributes: tcell.AttrBold,
128 | })
129 | }
130 |
131 | for i, network := range g.state.resources.networks {
132 | table.SetCell(i+1, 0, tview.NewTableCell(network.ID).
133 | SetTextColor(tcell.ColorLightSkyBlue).
134 | SetMaxWidth(1).
135 | SetExpansion(1))
136 |
137 | table.SetCell(i+1, 1, tview.NewTableCell(network.Name).
138 | SetTextColor(tcell.ColorLightSkyBlue).
139 | SetMaxWidth(1).
140 | SetExpansion(1))
141 |
142 | table.SetCell(i+1, 2, tview.NewTableCell(network.Driver).
143 | SetTextColor(tcell.ColorLightSkyBlue).
144 | SetMaxWidth(1).
145 | SetExpansion(1))
146 |
147 | table.SetCell(i+1, 3, tview.NewTableCell(network.Scope).
148 | SetTextColor(tcell.ColorLightSkyBlue).
149 | SetMaxWidth(1).
150 | SetExpansion(1))
151 |
152 | table.SetCell(i+1, 4, tview.NewTableCell(network.containers).
153 | SetTextColor(tcell.ColorLightSkyBlue).
154 | SetMaxWidth(1).
155 | SetExpansion(1))
156 | }
157 | }
158 |
159 | func (n *networks) focus(g *Gui) {
160 | n.SetSelectable(true, false)
161 | g.app.SetFocus(n)
162 | }
163 |
164 | func (n *networks) unfocus() {
165 | n.SetSelectable(false, false)
166 | }
167 |
168 | func (n *networks) updateEntries(g *Gui) {
169 | go g.app.QueueUpdateDraw(func() {
170 | n.setEntries(g)
171 | })
172 | }
173 |
174 | func (n *networks) setFilterWord(word string) {
175 | n.filterWord = word
176 | }
177 |
178 | func (n *networks) monitoringNetworks(g *Gui) {
179 | common.Logger.Info("start monitoring networks")
180 | ticker := time.NewTicker(5 * time.Second)
181 |
182 | LOOP:
183 | for {
184 | select {
185 | case <-ticker.C:
186 | n.updateEntries(g)
187 | case <-g.state.stopChans["network"]:
188 | ticker.Stop()
189 | break LOOP
190 | }
191 | }
192 | common.Logger.Info("stop monitoring networks")
193 | }
194 |
--------------------------------------------------------------------------------
/gui/panel.go:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | type panel interface {
4 | name() string
5 | entries(*Gui)
6 | setEntries(*Gui)
7 | updateEntries(*Gui)
8 | setKeybinding(*Gui)
9 | focus(*Gui)
10 | unfocus()
11 | setFilterWord(string)
12 | }
13 |
--------------------------------------------------------------------------------
/gui/searchImages.go:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import (
4 | "strconv"
5 |
6 | "github.com/gdamore/tcell/v2"
7 | "github.com/rivo/tview"
8 | "github.com/skanehira/docui/common"
9 | "github.com/skanehira/docui/docker"
10 | )
11 |
12 | type searchImageResult struct {
13 | Name string
14 | Stars string
15 | Official string
16 | Description string
17 | }
18 |
19 | type searchImageResults struct {
20 | keyword string
21 | searchImageResults []*searchImageResult
22 | *tview.Table
23 | }
24 |
25 | func newSearchImageResults(g *Gui, keyword string) *searchImageResults {
26 | searchImageResults := &searchImageResults{
27 | keyword: keyword,
28 | Table: tview.NewTable().SetSelectable(true, false).Select(0, 0).SetFixed(1, 1),
29 | }
30 |
31 | searchImageResults.SetTitle("search result").SetTitleAlign(tview.AlignLeft)
32 | searchImageResults.SetBorder(true)
33 | searchImageResults.setEntries(g)
34 | searchImageResults.setKeybinding(g)
35 | return searchImageResults
36 | }
37 |
38 | func newSearchInputField(g *Gui) {
39 | viewName := "searchImageInput"
40 | searchInput := tview.NewInputField().SetLabel("Image")
41 | searchInput.SetLabelWidth(6)
42 | searchInput.SetBorder(true)
43 |
44 | searchInput.SetDoneFunc(func(key tcell.Key) {
45 | if key == tcell.KeyEnter {
46 | if searchInput.GetText() == "" {
47 | g.message("please input some text", "OK", g.currentPanel().name(), func() {})
48 | return
49 | }
50 | g.pages.AddAndSwitchToPage("searchImageResults", g.modal(newSearchImageResults(g, searchInput.GetText()), 100, 50), true).ShowPage("main")
51 | }
52 | })
53 |
54 | closeSearchInput := func() {
55 | currentPanel := g.state.panels.panel[g.state.panels.currentPanel]
56 | g.closeAndSwitchPanel(viewName, currentPanel.name())
57 | }
58 |
59 | searchInput.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
60 | switch event.Key() {
61 | case tcell.KeyEsc:
62 | closeSearchInput()
63 | }
64 |
65 | return event
66 | })
67 |
68 | g.pages.AddAndSwitchToPage(viewName, g.modal(searchInput, 80, 3), true).ShowPage("main")
69 | }
70 |
71 | func (s *searchImageResults) name() string {
72 | return "searchImageResults"
73 | }
74 |
75 | func (s *searchImageResults) pullImage(g *Gui) {
76 | currentPanel := g.state.panels.panel[g.state.panels.currentPanel]
77 | g.pullImage(s.selectedSearchImageResult().Name, s.name(), currentPanel.name())
78 | }
79 |
80 | func (s *searchImageResults) setKeybinding(g *Gui) {
81 | s.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
82 | switch event.Key() {
83 | case tcell.KeyEsc:
84 | s.closePanel(g)
85 | case tcell.KeyEnter:
86 | s.pullImage(g)
87 | }
88 |
89 | switch event.Rune() {
90 | case 'p':
91 | s.pullImage(g)
92 | case 'q':
93 | s.closePanel(g)
94 | }
95 |
96 | return event
97 | })
98 | }
99 |
100 | func (s *searchImageResults) entries(g *Gui) {
101 | images, err := docker.Client.SearchImage(s.keyword)
102 |
103 | if err != nil {
104 | // TODO display error message
105 | return
106 | }
107 |
108 | if len(images) == 0 {
109 | // TODO display message "not found message"
110 | return
111 | }
112 |
113 | s.searchImageResults = make([]*searchImageResult, 0)
114 |
115 | var official string
116 | for _, image := range images {
117 | if image.IsOfficial {
118 | official = "[OK]"
119 | }
120 |
121 | s.searchImageResults = append(s.searchImageResults, &searchImageResult{
122 | Name: image.Name,
123 | Stars: strconv.Itoa(image.StarCount),
124 | Official: official,
125 | Description: common.CutNewline(image.Description),
126 | })
127 | }
128 |
129 | }
130 |
131 | func (s *searchImageResults) setEntries(g *Gui) {
132 | s.entries(g)
133 | table := s.Clear()
134 |
135 | headers := []string{
136 | "Name",
137 | "Star",
138 | "Official",
139 | "Description",
140 | }
141 |
142 | for i, header := range headers {
143 | table.SetCell(0, i, &tview.TableCell{
144 | Text: header,
145 | NotSelectable: true,
146 | Align: tview.AlignLeft,
147 | Color: tcell.ColorWhite,
148 | BackgroundColor: tcell.ColorDefault,
149 | Attributes: tcell.AttrBold,
150 | })
151 | }
152 |
153 | for i, image := range s.searchImageResults {
154 | table.SetCell(i+1, 0, tview.NewTableCell(image.Name).
155 | SetTextColor(tcell.ColorLightYellow).
156 | SetMaxWidth(1).
157 | SetExpansion(1))
158 |
159 | table.SetCell(i+1, 1, tview.NewTableCell(image.Stars).
160 | SetTextColor(tcell.ColorLightYellow))
161 |
162 | table.SetCell(i+1, 2, tview.NewTableCell(image.Official).
163 | SetTextColor(tcell.ColorLightYellow))
164 |
165 | table.SetCell(i+1, 3, tview.NewTableCell(image.Description).
166 | SetTextColor(tcell.ColorLightYellow).
167 | SetMaxWidth(1).
168 | SetExpansion(1))
169 |
170 | }
171 | }
172 |
173 | func (s *searchImageResults) updateEntries(g *Gui) {
174 | go g.app.QueueUpdateDraw(func() {
175 | s.setEntries(g)
176 | })
177 | }
178 |
179 | func (s *searchImageResults) focus(g *Gui) {
180 | s.SetSelectable(true, false)
181 | g.app.SetFocus(s)
182 | }
183 |
184 | func (s *searchImageResults) unfocus() {
185 | s.SetSelectable(false, false)
186 | }
187 |
188 | func (s *searchImageResults) closePanel(g *Gui) {
189 | currentPanel := g.state.panels.panel[g.state.panels.currentPanel]
190 | g.closeAndSwitchPanel(s.name(), currentPanel.name())
191 | }
192 |
193 | func (s *searchImageResults) selectedSearchImageResult() *searchImageResult {
194 | row, _ := s.GetSelection()
195 |
196 | if len(s.searchImageResults) == 0 || row-1 < 0 {
197 | return nil
198 | }
199 |
200 | return s.searchImageResults[row-1]
201 | }
202 |
--------------------------------------------------------------------------------
/gui/tasks.go:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/gdamore/tcell/v2"
7 | "github.com/rivo/tview"
8 | )
9 |
10 | var (
11 | success = "Success"
12 | executing = "Executing"
13 | cancel = "canceled"
14 | )
15 |
16 | type task struct {
17 | Name string
18 | Status string
19 | Created string
20 | Func func(ctx context.Context) error
21 | Ctx context.Context
22 | Cancel context.CancelFunc
23 | }
24 |
25 | type tasks struct {
26 | *tview.Table
27 | tasks chan *task
28 | }
29 |
30 | func newTasks(g *Gui) *tasks {
31 | tasks := &tasks{
32 | Table: tview.NewTable().SetSelectable(true, false).Select(0, 0).SetFixed(1, 1),
33 | tasks: make(chan *task),
34 | }
35 |
36 | tasks.SetTitle("tasks").SetTitleAlign(tview.AlignLeft)
37 | tasks.SetBorder(true)
38 | tasks.setEntries(g)
39 | tasks.setKeybinding(g)
40 | return tasks
41 | }
42 |
43 | func (t *tasks) name() string {
44 | return "tasks"
45 | }
46 |
47 | func (t *tasks) setKeybinding(g *Gui) {
48 | t.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
49 | g.setGlobalKeybinding(event)
50 |
51 | // TODO cancel task
52 | switch event.Key() {
53 | }
54 |
55 | switch event.Rune() {
56 | }
57 |
58 | return event
59 | })
60 | }
61 |
62 | func (t *tasks) entries(g *Gui) {
63 | // do nothing
64 | }
65 |
66 | func (t *tasks) setEntries(g *Gui) {
67 | t.entries(g)
68 | table := t.Clear()
69 |
70 | headers := []string{
71 | "Name",
72 | "Status",
73 | "Created",
74 | }
75 |
76 | for i, header := range headers {
77 | table.SetCell(0, i, &tview.TableCell{
78 | Text: header,
79 | NotSelectable: true,
80 | Align: tview.AlignLeft,
81 | Color: tcell.ColorWhite,
82 | BackgroundColor: tcell.ColorDefault,
83 | Attributes: tcell.AttrBold,
84 | })
85 | }
86 |
87 | for i, task := range g.state.resources.tasks {
88 | table.SetCell(i+1, 0, tview.NewTableCell(task.Name).
89 | SetTextColor(tcell.ColorLightGreen).
90 | SetMaxWidth(1).
91 | SetExpansion(1))
92 |
93 | table.SetCell(i+1, 1, tview.NewTableCell(task.Status).
94 | SetTextColor(tcell.ColorLightGreen).
95 | SetMaxWidth(1).
96 | SetExpansion(1))
97 |
98 | table.SetCell(i+1, 2, tview.NewTableCell(task.Created).
99 | SetTextColor(tcell.ColorLightGreen).
100 | SetMaxWidth(1).
101 | SetExpansion(1))
102 |
103 | }
104 | }
105 |
106 | func (t *tasks) focus(g *Gui) {
107 | t.SetSelectable(true, false)
108 | g.app.SetFocus(t)
109 | }
110 |
111 | func (t *tasks) unfocus() {
112 | t.SetSelectable(false, false)
113 | }
114 |
115 | func (t *tasks) setFilterWord(word string) {
116 | // do nothings
117 | }
118 |
119 | func (t *tasks) updateEntries(g *Gui) {
120 | // do nothings
121 | }
122 |
--------------------------------------------------------------------------------
/gui/volumes.go:
--------------------------------------------------------------------------------
1 | package gui
2 |
3 | import (
4 | "strings"
5 | "time"
6 |
7 | "github.com/gdamore/tcell/v2"
8 | "github.com/rivo/tview"
9 | "github.com/skanehira/docui/common"
10 | "github.com/skanehira/docui/docker"
11 | )
12 |
13 | var replacer = strings.NewReplacer("T", " ", "Z", "")
14 |
15 | type volume struct {
16 | Name string
17 | MountPoint string
18 | Driver string
19 | Created string
20 | }
21 |
22 | type volumes struct {
23 | *tview.Table
24 | filterWord string
25 | }
26 |
27 | func newVolumes(g *Gui) *volumes {
28 | volumes := &volumes{
29 | Table: tview.NewTable().SetSelectable(true, false).Select(0, 0).SetFixed(1, 1),
30 | }
31 |
32 | volumes.SetTitle("volume list").SetTitleAlign(tview.AlignLeft)
33 | volumes.SetBorder(true)
34 | volumes.setEntries(g)
35 | volumes.setKeybinding(g)
36 | return volumes
37 | }
38 |
39 | func (v *volumes) name() string {
40 | return "volumes"
41 | }
42 |
43 | func (v *volumes) setKeybinding(g *Gui) {
44 | v.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
45 | g.setGlobalKeybinding(event)
46 | switch event.Key() {
47 | case tcell.KeyEnter:
48 | g.inspectVolume()
49 | case tcell.KeyCtrlR:
50 | v.setEntries(g)
51 | }
52 |
53 | switch event.Rune() {
54 | case 'd':
55 | g.removeVolume()
56 | case 'c':
57 | g.createVolumeForm()
58 | }
59 |
60 | return event
61 | })
62 | }
63 |
64 | func (v *volumes) entries(g *Gui) {
65 | volumes, err := docker.Client.Volumes()
66 | if err != nil {
67 | common.Logger.Error(err)
68 | return
69 | }
70 |
71 | keys := make([]string, 0, len(volumes))
72 | tmpMap := make(map[string]*volume)
73 |
74 | for _, vo := range volumes {
75 | if strings.Index(vo.Name, v.filterWord) == -1 {
76 | continue
77 | }
78 |
79 | tmpMap[vo.Name] = &volume{
80 | Name: vo.Name,
81 | MountPoint: vo.Mountpoint,
82 | Driver: vo.Driver,
83 | Created: replacer.Replace(vo.CreatedAt),
84 | }
85 |
86 | keys = append(keys, vo.Name)
87 | }
88 |
89 | g.state.resources.volumes = make([]*volume, 0)
90 | for _, key := range common.SortKeys(keys) {
91 | g.state.resources.volumes = append(g.state.resources.volumes, tmpMap[key])
92 | }
93 | }
94 |
95 | func (v *volumes) setEntries(g *Gui) {
96 | v.entries(g)
97 | table := v.Clear()
98 |
99 | headers := []string{
100 | "Name",
101 | "MountPoint",
102 | "Driver",
103 | "Created",
104 | }
105 |
106 | for i, header := range headers {
107 | table.SetCell(0, i, &tview.TableCell{
108 | Text: header,
109 | NotSelectable: true,
110 | Align: tview.AlignLeft,
111 | Color: tcell.ColorWhite,
112 | BackgroundColor: tcell.ColorDefault,
113 | Attributes: tcell.AttrBold,
114 | })
115 | }
116 |
117 | for i, network := range g.state.resources.volumes {
118 | table.SetCell(i+1, 0, tview.NewTableCell(network.Name).
119 | SetTextColor(tcell.ColorLightPink).
120 | SetMaxWidth(1).
121 | SetExpansion(1))
122 |
123 | table.SetCell(i+1, 1, tview.NewTableCell(network.MountPoint).
124 | SetTextColor(tcell.ColorLightPink).
125 | SetMaxWidth(1).
126 | SetExpansion(1))
127 |
128 | table.SetCell(i+1, 2, tview.NewTableCell(network.Driver).
129 | SetTextColor(tcell.ColorLightPink).
130 | SetMaxWidth(1).
131 | SetExpansion(1))
132 |
133 | table.SetCell(i+1, 3, tview.NewTableCell(network.Created).
134 | SetTextColor(tcell.ColorLightPink).
135 | SetMaxWidth(1).
136 | SetExpansion(1))
137 | }
138 | }
139 |
140 | func (v *volumes) focus(g *Gui) {
141 | v.SetSelectable(true, false)
142 | g.app.SetFocus(v)
143 | }
144 |
145 | func (v *volumes) unfocus() {
146 | v.SetSelectable(false, false)
147 | }
148 |
149 | func (v *volumes) updateEntries(g *Gui) {
150 | go g.app.QueueUpdateDraw(func() {
151 | v.setEntries(g)
152 | })
153 | }
154 |
155 | func (v *volumes) setFilterWord(word string) {
156 | v.filterWord = word
157 | }
158 |
159 | func (v *volumes) monitoringVolumes(g *Gui) {
160 | common.Logger.Info("start monitoring volumes")
161 | ticker := time.NewTicker(5 * time.Second)
162 |
163 | LOOP:
164 | for {
165 | select {
166 | case <-ticker.C:
167 | v.updateEntries(g)
168 | case <-g.state.stopChans["volume"]:
169 | ticker.Stop()
170 | break LOOP
171 | }
172 | }
173 | common.Logger.Info("stop monitoring volumes")
174 | }
175 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "fmt"
7 | "os"
8 | "runtime"
9 |
10 | "github.com/docker/docker/client"
11 | "github.com/mattn/go-runewidth"
12 | "github.com/rivo/tview"
13 | "github.com/skanehira/docui/common"
14 | "github.com/skanehira/docui/docker"
15 | "github.com/skanehira/docui/gui"
16 | )
17 |
18 | var (
19 | endpoint = flag.String("endpoint", client.DefaultDockerHost, "Docker endpoint")
20 | cert = flag.String("cert", "", "cert.pem file path")
21 | key = flag.String("key", "", "key.pem file path")
22 | ca = flag.String("ca", "", "ca.pem file path")
23 | api = flag.String("api", "1.39", "api version")
24 | logFile = flag.String("log", "", "log file path")
25 | logLevel = flag.String("log-level", "info", "log level")
26 | )
27 |
28 | func init() {
29 | if runtime.GOOS == "windows" && runewidth.IsEastAsian() {
30 | tview.Borders.Horizontal = '-'
31 | tview.Borders.Vertical = '|'
32 | tview.Borders.TopLeft = '+'
33 | tview.Borders.TopRight = '+'
34 | tview.Borders.BottomLeft = '+'
35 | tview.Borders.BottomRight = '+'
36 | tview.Borders.LeftT = '|'
37 | tview.Borders.RightT = '|'
38 | tview.Borders.TopT = '-'
39 | tview.Borders.BottomT = '-'
40 | tview.Borders.Cross = '+'
41 | tview.Borders.HorizontalFocus = '='
42 | tview.Borders.VerticalFocus = '|'
43 | tview.Borders.TopLeftFocus = '+'
44 | tview.Borders.TopRightFocus = '+'
45 | tview.Borders.BottomLeftFocus = '+'
46 | tview.Borders.BottomRightFocus = '+'
47 | }
48 | }
49 |
50 | func run() int {
51 | common.NewLogger(*logLevel, *logFile)
52 |
53 | docker.NewDocker(docker.NewClientConfig(*endpoint, *cert, *key, *ca, *api))
54 | if _, err := docker.Client.Info(context.TODO()); err != nil {
55 | fmt.Fprintln(os.Stderr, err)
56 | return 1
57 | }
58 |
59 | gui := gui.New()
60 |
61 | if err := gui.Start(); err != nil {
62 | common.Logger.Errorf("cannot start docui: %s", err)
63 | return 1
64 | }
65 |
66 | return 0
67 | }
68 |
69 | func main() {
70 | flag.Parse()
71 | os.Exit(run())
72 | }
73 |
--------------------------------------------------------------------------------
/wiki.md:
--------------------------------------------------------------------------------
1 | # About docui
2 | docui is a simple TUI Client for docker.
3 | Supported OS is Linux/Mac only.
4 |
5 | Also, it supports UNIX domain socket, TCP, http/https.
6 |
7 | # Installation
8 | You have to install golang and set $GOPATH and $GOBIN to ~/.bashrc.
9 |
10 | ## 1. Install go
11 |
12 | ### Mac
13 | ```sh
14 | brew intall golang
15 | ```
16 | If you use [`Linuxbrew`](https://docs.brew.sh/Homebrew-on-Linux) on Linux, you can install via the command above.
17 |
18 | ### CentOS
19 | ```sh
20 | yum install golang
21 | ```
22 |
23 | ### Debian/Ubuntu
24 | ```sh
25 | apt install golang
26 | ```
27 |
28 | ### Fedora
29 | ```sh
30 | dnf install golang
31 | ```
32 |
33 | ### Add ~/.bashrc
34 | ```sh
35 | # add the following to ~/.bashrc
36 | export GOPATH=/to/your/path
37 | export GOBIN=$GOPATH/bin
38 | export PATH=$PATH:$GOBIN
39 | ```
40 |
41 | ### Reload ~/.bashrc
42 | ```sh
43 | resource ~/.bashrc
44 | ```
45 |
46 | ## 2. Install Docker
47 | If you don't have docker installed,
48 | please see docker official install guide for installion instructions.
49 |
50 | https://www.docker.com/get-started
51 |
52 | ## 3. Install Git
53 | ### Mac
54 | ```sh
55 | brew install git
56 | ```
57 | If you use [`Linuxbrew`](https://docs.brew.sh/Homebrew-on-Linux) on Linux, you can install via the above command.
58 |
59 | ### CentOS
60 | ```sh
61 | yum install git
62 | ```
63 |
64 | ### Debian/Ubuntu
65 | ```sh
66 | apt install git
67 | ```
68 |
69 | ### Fedora
70 | ```sh
71 | dnf install git
72 | ```
73 |
74 | ## 3. Install docui
75 |
76 | This project uses [Go Modules](https://github.com/golang/go/wiki/Modules).
77 | Please use Go version 1.11.4 or higher.
78 |
79 | Use go get or git clone:
80 |
81 | ```sh
82 | $ go get -d github.com/skanehira/docui
83 | $ cd $GOPATH/src/github.com/skanehira/docui
84 | $ GO111MODULE=on go install
85 | ```
86 |
87 | ```sh
88 | $ git clone https://github.com/skanehira/docui.git
89 | $ cd docui/
90 | $ GO111MODULE=on go install
91 | ```
92 |
93 | Make sure your PATH includes the $GOPATH/bin directory so your commands can be easily used:
94 |
95 | ```sh
96 | export PATH=$PATH:$GOPATH/bin
97 | ```
98 |
99 | ## 4. Update docui
100 |
101 | Use git pull:
102 |
103 | ```sh
104 | $ git pull
105 | $ GO111MODULE=on go install
106 | ```
107 |
108 |
109 | ## 5. Use on Docker
110 | If you want to use docui on docker.
111 |
112 | ```sh
113 | $ docker run --rm -itv /var/run/docker.sock:/var/run/docker.sock skanehira/docui
114 | ```
115 |
116 | ## 6. Build Docker Image
117 | If you want to customize image.
118 |
119 | ```sh
120 | $ make docker-build
121 | ```
122 |
123 | # How to use
124 | Refer to the [keybinding](https://github.com/skanehira/docui#Keybindings) for panel operations.
125 | I will explain the items of each panel here.
126 |
127 | ## pull image panel
128 |
129 | - Name
130 | Please enter the docker image name you want to pull.
131 | If you want to specify the version, please input as below.
132 |
133 | ```
134 | mysql:5.7
135 | ```
136 |
137 | ## search images panel
138 |
139 | Please enter the image name on the Docker Hub you want to search.
140 | This operation works like `docker search`
141 |
142 | ## save image panel
143 |
144 | Please enter the file path to save the selected image.
145 | It must be absolute path or relative path.
146 |
147 | ## import image panel
148 |
149 | Please enter the path of the image you want to import.
150 | It must be absolute path or relative path.
151 |
152 | ## load image panel
153 |
154 | Please enter the path of the image you want to load.
155 | It must be absolute path or relative path.
156 |
157 | ## create container panel
158 | - Name
159 | Container name.
160 |
161 | - HostPort
162 | Port of container to be mapping.
163 |
164 | - Port
165 | Port of the host OS to be mapped.
166 |
167 | - VolumeType
168 | Specify VolumeType bind or volume.
169 |
170 | - HostVolume
171 | If VolumeType is bind, path of the host OS that you want to mount.
172 | It's must be absolute path.
173 | It's similar to docker command `docker -v /to/host/path:/to/container/path`.
174 |
175 | If VolumeType is volume, specify the docker volume.
176 | It's similar to docker command `docker -v docekr/volume:/to/container/path`.
177 |
178 | - Volume
179 | Path of the container that you want to mount.
180 | It must be an absolute path.
181 |
182 | - Image
183 | Selected image id.
184 |
185 | - Attach
186 | If you want to attach container, please Enter.
187 |
188 | - User
189 | If you want to attach container, please input user name.
190 |
191 | - Env
192 | The environment variable setting value can be defined by variables like `$PATH`.
193 | In that case, we will obtain the value from the OS environment variable.
194 | If you want to add multiple environment variables, please input as below.
195 |
196 | ```
197 | GOPATH=~/go,GOBIN=~/go/bin,PATH=$PATH
198 | ```
199 |
200 | - Cmd
201 | If you want to add command arguments,
202 | please input as below.
203 |
204 | ```
205 | /bin/bash,hello
206 | ```
207 |
208 | ## export container panel
209 | Please enter the file path to save the selected container.
210 | It must be an absolute or a relative path.
211 |
212 | ## commit container panel
213 | - Container
214 | Selected container id.
215 |
216 | - Repository
217 | Please enter the image name of the committed container.
218 |
219 | - Tag
220 | If tag is empty, latest will be used.
221 |
222 | ## create volume panel
223 | - Name
224 | Specify volume name.
225 |
226 | - Driver
227 | Specify volume driver name.
228 |
229 | - Labels
230 | Set metadata for a volume.
231 | If you want to specify multiple labels, please enter as below.
232 |
233 | ```
234 | OS=Linux TYPE=nfs
235 | ```
236 |
237 | - Options
238 | Set driver specific options.
239 | If you want to specify multiple options, please enter as below.
240 |
241 | ```
242 | type=nfs o=addr=192.168.1.1,rw device=:/path/to/dir
243 | ```
244 |
245 | ## Configuration
246 |
247 | ### Command-Line Options
248 |
249 | Support custom endpoint:
250 |
251 | ```sh
252 | $ docui -h
253 | Usage of docui:
254 | -api string
255 | api version (default "1.39")
256 | -ca string
257 | ca.pem file path
258 | -cert string
259 | cert.pem file path
260 | -endpoint string
261 | Docker endpoint (default "unix:///var/run/docker.sock")
262 | -key string
263 | key.pem file path
264 | -log string
265 | log file path
266 | -log-level string
267 | log level (default "info")
268 | ```
269 |
270 | Or set environment variables:
271 |
272 | - `DOCKER_HOST`
273 | - `DOCKER_TLS_VERIFY`
274 | - `DOCKER_CERT_PATH`
275 |
276 | These environment variables take precedence over command-line options.
277 |
--------------------------------------------------------------------------------