dkview 3.0.0b5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dkview/__init__.py +3 -0
- dkview/__main__.py +3 -0
- dkview/ansi.py +124 -0
- dkview/cli.py +261 -0
- dkview/docker.py +190 -0
- dkview/engine.py +34 -0
- dkview/features/__init__.py +0 -0
- dkview/features/clean.py +150 -0
- dkview/features/dashboard.py +168 -0
- dkview/features/doctor.py +259 -0
- dkview/features/errors.py +456 -0
- dkview/features/images.py +213 -0
- dkview/features/inspect.py +333 -0
- dkview/features/live.py +52 -0
- dkview/features/logs.py +115 -0
- dkview/features/shell.py +52 -0
- dkview/features/tables.py +102 -0
- dkview/formats.py +140 -0
- dkview/layout.py +184 -0
- dkview/options.py +28 -0
- dkview/runner.py +46 -0
- dkview/styles.py +91 -0
- dkview/table.py +108 -0
- dkview/transform.py +184 -0
- dkview/units.py +52 -0
- dkview-3.0.0b5.dist-info/METADATA +1191 -0
- dkview-3.0.0b5.dist-info/RECORD +31 -0
- dkview-3.0.0b5.dist-info/WHEEL +5 -0
- dkview-3.0.0b5.dist-info/entry_points.txt +2 -0
- dkview-3.0.0b5.dist-info/licenses/LICENSE +21 -0
- dkview-3.0.0b5.dist-info/top_level.txt +1 -0
dkview/__init__.py
ADDED
dkview/__main__.py
ADDED
dkview/ansi.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Terminal colors and helpers that measure text without escape codes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
RESET = "\033[0m"
|
|
11
|
+
BOLD = "\033[1m"
|
|
12
|
+
DIM = "\033[2m"
|
|
13
|
+
RED = "\033[31m"
|
|
14
|
+
GREEN = "\033[32m"
|
|
15
|
+
YELLOW = "\033[33m"
|
|
16
|
+
BLUE = "\033[34m"
|
|
17
|
+
MAGENTA = "\033[35m"
|
|
18
|
+
CYAN = "\033[36m"
|
|
19
|
+
REVERSE = "\033[7m"
|
|
20
|
+
|
|
21
|
+
ANSI_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _enable_windows_ansi() -> bool:
|
|
25
|
+
"""Windows 10+ consoles understand color codes once asked to.
|
|
26
|
+
|
|
27
|
+
Returns False when they can't (old Windows), so colors stay off there.
|
|
28
|
+
"""
|
|
29
|
+
if os.name != "nt":
|
|
30
|
+
return True
|
|
31
|
+
try:
|
|
32
|
+
import ctypes
|
|
33
|
+
kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
|
|
34
|
+
handle = kernel32.GetStdHandle(-11) # stdout
|
|
35
|
+
mode = ctypes.c_uint32()
|
|
36
|
+
if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
|
|
37
|
+
return False
|
|
38
|
+
return bool(kernel32.SetConsoleMode(handle, mode.value | 0x0004))
|
|
39
|
+
except Exception:
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Plain-ASCII stand-ins, for outputs that can't show box drawing or symbols
|
|
44
|
+
# (a pipe or an old console on Windows that uses a legacy code page).
|
|
45
|
+
# Characters that can sit inside a table cell map to one character.
|
|
46
|
+
ASCII = str.maketrans({
|
|
47
|
+
"┌": "+", "┬": "+", "┐": "+", "├": "+", "┼": "+", "┤": "+", "└": "+", "┴": "+",
|
|
48
|
+
"┘": "+", "─": "-", "│": "|", "●": "*", "○": "o", "✓": "OK", "✗": "X",
|
|
49
|
+
"·": "-", "…": "~", "×": "x", "→": "->", "⚠": "!",
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class _AsciiWriter:
|
|
54
|
+
"""Wraps a text stream, writing ASCII stand-ins for what it can't encode."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, stream) -> None:
|
|
57
|
+
self._stream = stream
|
|
58
|
+
|
|
59
|
+
def write(self, text: str) -> int:
|
|
60
|
+
return self._stream.write(text.translate(ASCII))
|
|
61
|
+
|
|
62
|
+
def __getattr__(self, name: str):
|
|
63
|
+
return getattr(self._stream, name)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _can_show(stream, sample: str = "┌─●✓") -> bool:
|
|
67
|
+
try:
|
|
68
|
+
sample.encode(getattr(stream, "encoding", None) or "ascii")
|
|
69
|
+
return True
|
|
70
|
+
except (LookupError, UnicodeEncodeError):
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def prepare_output() -> None:
|
|
75
|
+
"""Never crash on a character the output's encoding lacks: use ASCII
|
|
76
|
+
stand-ins for box lines and symbols, and '?' for anything else."""
|
|
77
|
+
for name in ("stdout", "stderr"):
|
|
78
|
+
stream = getattr(sys, name)
|
|
79
|
+
if stream is None or _can_show(stream):
|
|
80
|
+
continue
|
|
81
|
+
if hasattr(stream, "reconfigure"):
|
|
82
|
+
try:
|
|
83
|
+
stream.reconfigure(errors="replace")
|
|
84
|
+
except (ValueError, OSError):
|
|
85
|
+
pass
|
|
86
|
+
setattr(sys, name, _AsciiWriter(stream))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class _Colors:
|
|
90
|
+
"""Process-wide switch for colored output."""
|
|
91
|
+
|
|
92
|
+
def __init__(self) -> None:
|
|
93
|
+
self.enabled = (sys.stdout.isatty() and "NO_COLOR" not in os.environ
|
|
94
|
+
and _enable_windows_ansi())
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
colors = _Colors()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def paint(text: str, *codes: str) -> str:
|
|
101
|
+
if not colors.enabled or not text or not codes:
|
|
102
|
+
return text
|
|
103
|
+
return "".join(codes) + text + RESET
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def strip(text: str) -> str:
|
|
107
|
+
return ANSI_RE.sub("", text)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def visible_len(text: str) -> int:
|
|
111
|
+
return len(strip(text))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def pad(text: str, width: int) -> str:
|
|
115
|
+
"""ljust that ignores ANSI escape codes when measuring."""
|
|
116
|
+
return text + " " * max(0, width - visible_len(text))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def terminal_size() -> "os.terminal_size":
|
|
120
|
+
return shutil.get_terminal_size((120, 40))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def terminal_width() -> int:
|
|
124
|
+
return terminal_size().columns
|
dkview/cli.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Command-line entry point: decide what to do with the command given."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import shlex
|
|
9
|
+
import sys
|
|
10
|
+
from typing import List, Optional, Sequence
|
|
11
|
+
|
|
12
|
+
from . import __version__, docker, engine
|
|
13
|
+
from .ansi import colors, prepare_output
|
|
14
|
+
from .features import clean, dashboard, doctor, errors, images, inspect, live, logs, shell, tables
|
|
15
|
+
from .options import Options
|
|
16
|
+
from .runner import passthrough
|
|
17
|
+
|
|
18
|
+
EPILOG = """\
|
|
19
|
+
examples:
|
|
20
|
+
dkview docker ps formatted, colored table
|
|
21
|
+
dkview ps -a same; "docker" can be left out
|
|
22
|
+
dkview --short --cols name,status,image ps
|
|
23
|
+
dkview --sort cpu --desc stats live stats, busiest first (Ctrl+C quits)
|
|
24
|
+
dkview --watch service ls redraw every 2 seconds
|
|
25
|
+
dkview inspect web short summary; add --full for every field
|
|
26
|
+
dkview --grep timeout logs -f api
|
|
27
|
+
dkview dash overview of the whole host (--watch for live)
|
|
28
|
+
dkview images --group one row per repository with total sizes
|
|
29
|
+
dkview clean --dry-run old unused image tags that would be removed
|
|
30
|
+
dkview clean --keep 5 remove them, keeping the newest 5 per repository
|
|
31
|
+
dkview doctor failing swarm services with the real error
|
|
32
|
+
dkview errors errors and warnings in all logs, last 30 minutes
|
|
33
|
+
dkview errors orders --since 2d one stack, service or container
|
|
34
|
+
dkview shell-init make plain "docker ps" use dkview
|
|
35
|
+
|
|
36
|
+
dkview's own options go before the command. Default options can be set in
|
|
37
|
+
the DKVIEW_OPTS environment variable, e.g. DKVIEW_OPTS="--short".
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Commands that are dkview's own rather than docker's.
|
|
42
|
+
OWN_COMMANDS = {"dash", "clean", "doctor", "errors", "shell-init"}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
46
|
+
p = argparse.ArgumentParser(
|
|
47
|
+
prog="dkview",
|
|
48
|
+
description="Readable, colored output for docker commands.",
|
|
49
|
+
epilog=EPILOG,
|
|
50
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
51
|
+
)
|
|
52
|
+
view = p.add_argument_group("table view")
|
|
53
|
+
view.add_argument("--cols", metavar="A,B,...",
|
|
54
|
+
help="show only these columns, in this order (names can be shortened)")
|
|
55
|
+
view.add_argument("--sort", metavar="COL", help="sort rows by a column (numbers, sizes and ages sort by value)")
|
|
56
|
+
view.add_argument("--desc", action="store_true", help="sort largest first")
|
|
57
|
+
view.add_argument("--grep", metavar="REGEX",
|
|
58
|
+
help="keep only rows (or log lines) matching this, case-insensitive")
|
|
59
|
+
view.add_argument("--short", action="store_true",
|
|
60
|
+
help="hide the registry host in image names")
|
|
61
|
+
view.add_argument("--long-times", action="store_true",
|
|
62
|
+
help="keep docker's '4 minutes ago' instead of '4m ago'")
|
|
63
|
+
view.add_argument("--trunc", action="store_true",
|
|
64
|
+
help="keep docker's own truncation of long values")
|
|
65
|
+
view.add_argument("--width", type=int, help="table width (default: terminal width)")
|
|
66
|
+
|
|
67
|
+
mode = p.add_argument_group("modes")
|
|
68
|
+
mode.add_argument("-w", "--watch", action="store_true",
|
|
69
|
+
help="redraw the output every few seconds until Ctrl+C")
|
|
70
|
+
mode.add_argument("-n", "--interval", type=float, default=2.0, metavar="SEC",
|
|
71
|
+
help="seconds between redraws (default: 2)")
|
|
72
|
+
mode.add_argument("--once", action="store_true",
|
|
73
|
+
help="docker stats: print one snapshot instead of a live view")
|
|
74
|
+
mode.add_argument("--full", action="store_true",
|
|
75
|
+
help="inspect: show every field as a tree instead of a summary")
|
|
76
|
+
mode.add_argument("--raw", action="store_true",
|
|
77
|
+
help="run the command untouched")
|
|
78
|
+
mode.add_argument("--group", action="store_true",
|
|
79
|
+
help="images: one row per repository with total and unused size")
|
|
80
|
+
|
|
81
|
+
cleaning = p.add_argument_group("dkview clean")
|
|
82
|
+
cleaning.add_argument("--keep", type=int, default=3, metavar="N",
|
|
83
|
+
help="keep the newest N tags of each repository (default: 3)")
|
|
84
|
+
cleaning.add_argument("--dry-run", action="store_true",
|
|
85
|
+
help="only show what would be deleted")
|
|
86
|
+
cleaning.add_argument("-y", "--yes", action="store_true",
|
|
87
|
+
help="delete without asking")
|
|
88
|
+
|
|
89
|
+
logs_group = p.add_argument_group("dkview errors")
|
|
90
|
+
logs_group.add_argument("--since", metavar="TIME",
|
|
91
|
+
help="how far back to read logs: 30m, 6h, 2d, 1w or a date "
|
|
92
|
+
f"(default: {errors.DEFAULT_SINCE})")
|
|
93
|
+
|
|
94
|
+
out = p.add_argument_group("output")
|
|
95
|
+
out.add_argument("--no-color", action="store_true", help="disable colors")
|
|
96
|
+
out.add_argument("--color", action="store_true",
|
|
97
|
+
help="force colors even when not printing to a terminal")
|
|
98
|
+
out.add_argument("-V", "--version", action="version", version=f"dkview {__version__}")
|
|
99
|
+
|
|
100
|
+
p.add_argument("command", nargs=argparse.REMAINDER, help="the command to run")
|
|
101
|
+
return p
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def to_options(args: argparse.Namespace) -> Options:
|
|
105
|
+
return Options(
|
|
106
|
+
width=args.width,
|
|
107
|
+
cols=[c.strip() for c in (args.cols or "").split(",") if c.strip()],
|
|
108
|
+
sort=args.sort,
|
|
109
|
+
desc=args.desc,
|
|
110
|
+
grep=re.compile(args.grep, re.IGNORECASE) if args.grep else None,
|
|
111
|
+
short=args.short,
|
|
112
|
+
humanize=not args.long_times,
|
|
113
|
+
trunc=args.trunc,
|
|
114
|
+
watch=args.watch,
|
|
115
|
+
once=args.once,
|
|
116
|
+
interval=max(0.5, args.interval),
|
|
117
|
+
full=args.full,
|
|
118
|
+
group=args.group,
|
|
119
|
+
keep=max(0, args.keep),
|
|
120
|
+
dry_run=args.dry_run,
|
|
121
|
+
yes=args.yes,
|
|
122
|
+
since=args.since,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def run(argv: Sequence[str]) -> int:
|
|
127
|
+
parser = build_parser()
|
|
128
|
+
args = parser.parse_args(shlex.split(os.environ.get("DKVIEW_OPTS", "")) + list(argv))
|
|
129
|
+
|
|
130
|
+
if args.no_color:
|
|
131
|
+
colors.enabled = False
|
|
132
|
+
elif args.color:
|
|
133
|
+
colors.enabled = True
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
opts = to_options(args)
|
|
137
|
+
except re.error as error:
|
|
138
|
+
parser.error(f"--grep: {error}")
|
|
139
|
+
|
|
140
|
+
command: List[str] = list(args.command)
|
|
141
|
+
if command[:1] == ["--"]:
|
|
142
|
+
command = command[1:]
|
|
143
|
+
if not command:
|
|
144
|
+
parser.print_help()
|
|
145
|
+
return 2
|
|
146
|
+
|
|
147
|
+
if command[0] == "errors":
|
|
148
|
+
return run_errors(parser, command[1:], opts)
|
|
149
|
+
|
|
150
|
+
if command[0] in OWN_COMMANDS and len(command) > 1 and command[0] != "shell-init":
|
|
151
|
+
# `dkview clean --keep 5`: options after dkview's own commands are dkview's.
|
|
152
|
+
before = list(argv)[:len(argv) - len(args.command)]
|
|
153
|
+
return run(before + command[1:] + [command[0]])
|
|
154
|
+
|
|
155
|
+
if command[0] == "clean":
|
|
156
|
+
return clean.run([engine.name()], opts)
|
|
157
|
+
if command[0] == "doctor":
|
|
158
|
+
return doctor.run([engine.name()], opts)
|
|
159
|
+
if command[0] == "dash":
|
|
160
|
+
frame = lambda width: dashboard.frame(opts, width) # noqa: E731
|
|
161
|
+
if opts.watch:
|
|
162
|
+
return live.run(frame, "dkview dash", opts.interval)
|
|
163
|
+
sys.stdout.write(frame(opts.width))
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
if command[0] == "shell-init":
|
|
167
|
+
return shell_init(command[1:])
|
|
168
|
+
|
|
169
|
+
command = docker.expand_shortcut(command)
|
|
170
|
+
|
|
171
|
+
if args.raw:
|
|
172
|
+
return passthrough(command)
|
|
173
|
+
|
|
174
|
+
if not docker.is_docker(command):
|
|
175
|
+
return run_table(command, opts)
|
|
176
|
+
|
|
177
|
+
kind = docker.classify(command).name
|
|
178
|
+
if kind == "logs":
|
|
179
|
+
return logs.run(command, opts)
|
|
180
|
+
if kind == "inspect":
|
|
181
|
+
return inspect.run(command, opts)
|
|
182
|
+
if kind == "stats":
|
|
183
|
+
live_stats = not opts.once and sys.stdout.isatty() and "--no-stream" not in command
|
|
184
|
+
if live_stats or opts.watch:
|
|
185
|
+
return live.run(tables.snapshot(command, opts), " ".join(command), opts.interval)
|
|
186
|
+
return tables.run(command, opts)
|
|
187
|
+
if kind == "table" and images.is_image_list(command):
|
|
188
|
+
if "--group" in command: # also accepted after the command
|
|
189
|
+
command = [a for a in command if a != "--group"]
|
|
190
|
+
opts.group = True
|
|
191
|
+
if opts.group:
|
|
192
|
+
return images.run_group(command, opts)
|
|
193
|
+
if kind == "table":
|
|
194
|
+
return run_table(command, opts)
|
|
195
|
+
return passthrough(command)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def run_errors(parser: argparse.ArgumentParser, rest: List[str], opts: Options) -> int:
|
|
199
|
+
"""`dkview errors [TARGET...]`: targets and options can be mixed freely."""
|
|
200
|
+
sub = argparse.ArgumentParser(prog="dkview errors", description=errors.__doc__,
|
|
201
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
202
|
+
sub.add_argument("targets", nargs="*", metavar="NAME",
|
|
203
|
+
help="stack, service or container name or ID (default: everything)")
|
|
204
|
+
sub.add_argument("--since", metavar="TIME",
|
|
205
|
+
help=f"30m, 6h, 2d, 1w or a date (default: {errors.DEFAULT_SINCE})")
|
|
206
|
+
sub.add_argument("--grep", metavar="REGEX", help="count only lines matching this")
|
|
207
|
+
sub.add_argument("--full", action="store_true", help="show every kind of message")
|
|
208
|
+
sub.add_argument("--width", type=int, help="table width")
|
|
209
|
+
sub.add_argument("--no-color", action="store_true", help="disable colors")
|
|
210
|
+
sub.add_argument("--color", action="store_true", help="force colors")
|
|
211
|
+
args = sub.parse_intermixed_args(rest)
|
|
212
|
+
|
|
213
|
+
if args.since:
|
|
214
|
+
opts.since = args.since
|
|
215
|
+
if args.grep:
|
|
216
|
+
try:
|
|
217
|
+
opts.grep = re.compile(args.grep, re.IGNORECASE)
|
|
218
|
+
except re.error as error:
|
|
219
|
+
sub.error(f"--grep: {error}")
|
|
220
|
+
opts.full = opts.full or args.full
|
|
221
|
+
opts.width = args.width or opts.width
|
|
222
|
+
if args.no_color:
|
|
223
|
+
colors.enabled = False
|
|
224
|
+
elif args.color:
|
|
225
|
+
colors.enabled = True
|
|
226
|
+
return errors.run([engine.name()], args.targets, opts)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def run_table(command: List[str], opts: Options) -> int:
|
|
230
|
+
if opts.watch:
|
|
231
|
+
return live.run(tables.snapshot(command, opts), " ".join(command), opts.interval)
|
|
232
|
+
return tables.run(command, opts)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def shell_init(rest: List[str]) -> int:
|
|
236
|
+
name: Optional[str] = rest[0] if rest else None
|
|
237
|
+
if name is None:
|
|
238
|
+
sys.stdout.write(shell.USAGE)
|
|
239
|
+
return 0
|
|
240
|
+
if name not in shell.SCRIPTS:
|
|
241
|
+
sys.stderr.write(f"dkview: unsupported shell '{name}' (bash, zsh, fish, powershell)\n")
|
|
242
|
+
return 2
|
|
243
|
+
sys.stdout.write(shell.script(name))
|
|
244
|
+
return 0
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
248
|
+
prepare_output()
|
|
249
|
+
try:
|
|
250
|
+
return run(sys.argv[1:] if argv is None else argv)
|
|
251
|
+
except KeyboardInterrupt:
|
|
252
|
+
return 130
|
|
253
|
+
except BrokenPipeError:
|
|
254
|
+
# Output piped into `head` or `less` that quit early.
|
|
255
|
+
devnull = os.open(os.devnull, os.O_WRONLY)
|
|
256
|
+
os.dup2(devnull, sys.stdout.fileno())
|
|
257
|
+
return 0
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
if __name__ == "__main__":
|
|
261
|
+
raise SystemExit(main())
|
dkview/docker.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Understanding docker command lines: which subcommand, and how to run it."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import List, Sequence
|
|
7
|
+
|
|
8
|
+
from . import engine
|
|
9
|
+
|
|
10
|
+
# Options that take a separate value (`--context prod`, `-f file.yml`,
|
|
11
|
+
# `--filter status=exited`). The value is skipped when looking for the
|
|
12
|
+
# subcommand words.
|
|
13
|
+
VALUE_OPTIONS = {
|
|
14
|
+
"-c", "--context", "-H", "--host", "--config", "-l", "--log-level",
|
|
15
|
+
"--tlscacert", "--tlscert", "--tlskey",
|
|
16
|
+
"-f", "--file", "--filter", "-p", "--project-name",
|
|
17
|
+
"--project-directory", "--env-file", "--profile",
|
|
18
|
+
"-n", "--tail", "--since", "--until",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
LIST_VERBS = {"ls", "list"}
|
|
22
|
+
|
|
23
|
+
# Commands whose output is a table. Anything not listed here (run, exec,
|
|
24
|
+
# build, events, typos, --help...) runs straight through untouched.
|
|
25
|
+
TABLE_COMMANDS = {
|
|
26
|
+
"ps": None, "images": None, "stats": None, "top": None,
|
|
27
|
+
"history": None, "search": None,
|
|
28
|
+
"container": LIST_VERBS | {"ps", "stats", "top"},
|
|
29
|
+
"image": LIST_VERBS | {"history"},
|
|
30
|
+
"volume": LIST_VERBS,
|
|
31
|
+
"network": LIST_VERBS,
|
|
32
|
+
"service": LIST_VERBS | {"ps"},
|
|
33
|
+
"node": LIST_VERBS | {"ps"},
|
|
34
|
+
"stack": LIST_VERBS | {"ps", "services"},
|
|
35
|
+
"secret": LIST_VERBS,
|
|
36
|
+
"config": LIST_VERBS,
|
|
37
|
+
"context": LIST_VERBS,
|
|
38
|
+
"plugin": LIST_VERBS,
|
|
39
|
+
"system": {"df"},
|
|
40
|
+
"compose": {"ps", "ls", "images", "top", "stats"},
|
|
41
|
+
"buildx": LIST_VERBS,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
INSPECT_OBJECTS = {"container", "image", "service", "network", "volume",
|
|
45
|
+
"node", "secret", "config", "plugin", "context"}
|
|
46
|
+
|
|
47
|
+
LOG_OBJECTS = {"container", "service", "compose"}
|
|
48
|
+
|
|
49
|
+
# Every top-level docker command, so `dkview ps` can mean `docker ps`.
|
|
50
|
+
DOCKER_COMMANDS = {
|
|
51
|
+
"attach", "build", "builder", "buildx", "commit", "compose", "config",
|
|
52
|
+
"container", "context", "cp", "create", "diff", "events", "exec",
|
|
53
|
+
"export", "history", "image", "images", "import", "info", "inspect",
|
|
54
|
+
"kill", "load", "login", "logout", "logs", "manifest", "network",
|
|
55
|
+
"node", "pause", "plugin", "port", "ps", "pull", "push", "rename",
|
|
56
|
+
"restart", "rm", "rmi", "run", "save", "search", "secret", "service",
|
|
57
|
+
"stack", "start", "stats", "stop", "swarm", "system", "tag", "top",
|
|
58
|
+
"trust", "unpause", "update", "version", "volume", "wait",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def is_docker(argv: Sequence[str]) -> bool:
|
|
63
|
+
"""docker, or podman, which takes the same commands."""
|
|
64
|
+
return bool(argv) and engine.kind(argv[0]) != ""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def expand_shortcut(argv: List[str]) -> List[str]:
|
|
68
|
+
"""`dkview ps -a` -> `docker ps -a` (or `podman ps -a`)."""
|
|
69
|
+
if argv and argv[0] in DOCKER_COMMANDS:
|
|
70
|
+
return [engine.name()] + argv
|
|
71
|
+
return argv
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def words(argv: Sequence[str], limit: int = 2) -> List[str]:
|
|
75
|
+
"""First non-option words after `docker`.
|
|
76
|
+
|
|
77
|
+
`docker --context prod service ls -q` -> ['service', 'ls']
|
|
78
|
+
"""
|
|
79
|
+
found: List[str] = []
|
|
80
|
+
skip = False
|
|
81
|
+
for arg in argv[1:]:
|
|
82
|
+
if skip:
|
|
83
|
+
skip = False
|
|
84
|
+
elif arg in VALUE_OPTIONS:
|
|
85
|
+
skip = True
|
|
86
|
+
elif not arg.startswith("-"):
|
|
87
|
+
found.append(arg)
|
|
88
|
+
if len(found) == limit:
|
|
89
|
+
break
|
|
90
|
+
return found
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def has_option(argv: Sequence[str], *names: str) -> bool:
|
|
94
|
+
return any(a in names or any(a.startswith(n + "=") for n in names
|
|
95
|
+
if n.startswith("--"))
|
|
96
|
+
for a in argv)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def user_chose_format(argv: Sequence[str]) -> bool:
|
|
100
|
+
return has_option(argv, "--format", "-q", "--quiet", "--pretty")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class Kind:
|
|
105
|
+
"""What dkview should do with a docker command."""
|
|
106
|
+
|
|
107
|
+
name: str # "table", "stats", "logs", "inspect" or "passthrough"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def classify(argv: Sequence[str]) -> Kind:
|
|
111
|
+
w = words(argv)
|
|
112
|
+
if not w or has_option(argv, "-h", "--help"):
|
|
113
|
+
return Kind("passthrough")
|
|
114
|
+
|
|
115
|
+
first = w[0]
|
|
116
|
+
second = w[1] if len(w) > 1 else None
|
|
117
|
+
|
|
118
|
+
if first == "logs" or (first in LOG_OBJECTS and second == "logs"):
|
|
119
|
+
return Kind("logs")
|
|
120
|
+
|
|
121
|
+
if first == "inspect" or (first in INSPECT_OBJECTS and second == "inspect"):
|
|
122
|
+
if user_chose_format(argv):
|
|
123
|
+
return Kind("passthrough")
|
|
124
|
+
return Kind("inspect")
|
|
125
|
+
|
|
126
|
+
if user_chose_format(argv) and not _is_table_format(argv):
|
|
127
|
+
return Kind("passthrough")
|
|
128
|
+
|
|
129
|
+
if first not in TABLE_COMMANDS:
|
|
130
|
+
return Kind("passthrough")
|
|
131
|
+
verbs = TABLE_COMMANDS[first]
|
|
132
|
+
if verbs is not None and second not in verbs:
|
|
133
|
+
return Kind("passthrough")
|
|
134
|
+
|
|
135
|
+
# Layouts that are not a single table.
|
|
136
|
+
if [first, second] == ["system", "df"] and has_option(argv, "-v", "--verbose"):
|
|
137
|
+
return Kind("passthrough")
|
|
138
|
+
if has_option(argv, "--tree"):
|
|
139
|
+
return Kind("passthrough")
|
|
140
|
+
|
|
141
|
+
if first == "stats" or (first in ("container", "compose") and second == "stats"):
|
|
142
|
+
return Kind("stats")
|
|
143
|
+
return Kind("table")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _is_table_format(argv: Sequence[str]) -> bool:
|
|
147
|
+
"""`--format 'table {{.Names}}\t{{.Status}}'` still prints a table."""
|
|
148
|
+
for i, arg in enumerate(argv):
|
|
149
|
+
value = None
|
|
150
|
+
if arg == "--format" and i + 1 < len(argv):
|
|
151
|
+
value = argv[i + 1]
|
|
152
|
+
elif arg.startswith("--format="):
|
|
153
|
+
value = arg.split("=", 1)[1]
|
|
154
|
+
if value is not None:
|
|
155
|
+
return value.strip().startswith("table")
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# Commands that accept --no-trunc. dkview asks for full values and wraps
|
|
160
|
+
# them itself, then shortens IDs back to docker's usual 12 characters.
|
|
161
|
+
NO_TRUNC = {
|
|
162
|
+
("ps",), ("images",), ("history",), ("search",),
|
|
163
|
+
("container", "ls"), ("container", "ps"), ("container", "list"),
|
|
164
|
+
("image", "ls"), ("image", "list"), ("image", "history"),
|
|
165
|
+
("service", "ps"), ("stack", "ps"), ("node", "ps"),
|
|
166
|
+
("network", "ls"), ("network", "list"), ("compose", "ps"),
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def supports_no_trunc(argv: Sequence[str]) -> bool:
|
|
171
|
+
w = words(argv)
|
|
172
|
+
return tuple(w[:1]) in NO_TRUNC or tuple(w[:2]) in NO_TRUNC
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def prepare(argv: Sequence[str], trunc: bool) -> List[str]:
|
|
176
|
+
"""Flags that make docker print one complete snapshot we can format."""
|
|
177
|
+
argv = list(argv)
|
|
178
|
+
kind = classify(argv)
|
|
179
|
+
|
|
180
|
+
# `docker stats` refreshes forever by default; take a single sample.
|
|
181
|
+
if kind.name == "stats" and "--no-stream" not in argv:
|
|
182
|
+
argv.append("--no-stream")
|
|
183
|
+
|
|
184
|
+
# Full values so COMMAND or ERROR are not cut short; dkview wraps
|
|
185
|
+
# long values itself.
|
|
186
|
+
if (kind.name == "table" and supports_no_trunc(argv) and not trunc
|
|
187
|
+
and not has_option(argv, "--no-trunc", "--format", "-q", "--quiet")):
|
|
188
|
+
argv.append("--no-trunc")
|
|
189
|
+
|
|
190
|
+
return argv
|
dkview/engine.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Which container engine to run: docker, or podman where docker is missing.
|
|
2
|
+
|
|
3
|
+
DKVIEW_ENGINE=podman (or a full path) picks one explicitly.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
|
|
11
|
+
ENGINES = ("docker", "podman")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def name() -> str:
|
|
15
|
+
chosen = os.environ.get("DKVIEW_ENGINE", "").strip()
|
|
16
|
+
if chosen:
|
|
17
|
+
return chosen
|
|
18
|
+
for engine in ENGINES:
|
|
19
|
+
if shutil.which(engine):
|
|
20
|
+
return engine
|
|
21
|
+
return "docker" # not installed: let docker's own "not found" error show
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def kind(program: str) -> str:
|
|
25
|
+
"""'docker', 'podman' or '' for /usr/bin/podman, docker.exe, ..."""
|
|
26
|
+
base = program.replace("\\", "/").rsplit("/", 1)[-1].lower()
|
|
27
|
+
for ext in (".exe", ".cmd", ".bat"):
|
|
28
|
+
if base.endswith(ext):
|
|
29
|
+
base = base[:-len(ext)]
|
|
30
|
+
return base if base in ENGINES else ""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_podman(argv) -> bool:
|
|
34
|
+
return bool(argv) and kind(argv[0]) == "podman"
|
|
File without changes
|