├── .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 | [![Go Report Card](https://goreportcard.com/badge/github.com/skanehira/docui?)](https://goreportcard.com/report/github.com/skanehira/docui) 4 | [![CircleCI](https://img.shields.io/circleci/project/github/skanehira/docui.svg?style=flat-square)](https://goreportcard.com/report/github.com/skanehira/docui) 5 | [![CircleCI](https://img.shields.io/github/release/skanehira/docui.svg?style=flat-square)](https://github.com/skanehira/docui/releases) 6 | ![GitHub All Releases](https://img.shields.io/github/downloads/skanehira/docui/total.svg?style=flat) 7 | ![GitHub commits](https://img.shields.io/github/commits-since/skanehira/docui/1.0.0.svg?style=flat-square) 8 | 9 | # This repository is no longer maintenance. Please use [lazydocker](https://github.com/jesseduffield/lazydocker) instead. 10 | 11 | ## About docui 12 | ![demo](https://github.com/skanehira/docui/blob/images/images/docui.v2-demo.gif?raw=true) 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 | --------------------------------------------------------------------------------