helper-cli 0.1.11__py3-none-any.whl → 0.1.21__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.
- helper/__init__.py +7 -0
- helper/commands/arch.py +8 -2
- helper/commands/docker.py +378 -210
- helper/commands/file.py +202 -0
- helper/commands/internal_ip.py +10 -2
- helper/commands/nixos.py +57 -31
- helper/commands/public_ip.py +11 -2
- helper/commands/speed.py +38 -0
- helper/commands/system_info.py +326 -0
- helper/commands/venv.py +130 -0
- helper/main.py +68 -27
- helper/table.py +29 -0
- helper_cli-0.1.21.dist-info/METADATA +61 -0
- helper_cli-0.1.21.dist-info/RECORD +19 -0
- {helper_cli-0.1.11.dist-info → helper_cli-0.1.21.dist-info}/entry_points.txt +1 -0
- helper_cli-0.1.11.dist-info/METADATA +0 -6
- helper_cli-0.1.11.dist-info/RECORD +0 -13
- {helper_cli-0.1.11.dist-info → helper_cli-0.1.21.dist-info}/WHEEL +0 -0
- {helper_cli-0.1.11.dist-info → helper_cli-0.1.21.dist-info}/top_level.txt +0 -0
helper/commands/docker.py
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"""
|
|
2
|
+
Docker management commands for the helper CLI.
|
|
3
|
+
|
|
4
|
+
This module provides commands to manage Docker containers and images,
|
|
5
|
+
including listing, running, and removing containers and images.
|
|
6
|
+
"""
|
|
7
|
+
|
|
3
8
|
import json
|
|
4
|
-
import re
|
|
5
9
|
import logging
|
|
10
|
+
import subprocess
|
|
6
11
|
import sys
|
|
7
|
-
from typing import Dict, List
|
|
12
|
+
from typing import Dict, List
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
from helper import __version__
|
|
8
16
|
|
|
9
17
|
# Configure logging
|
|
10
18
|
logging.basicConfig(
|
|
11
19
|
level=logging.WARNING,
|
|
12
|
-
format=
|
|
13
|
-
stream=sys.stderr
|
|
20
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
21
|
+
stream=sys.stderr,
|
|
14
22
|
)
|
|
15
|
-
logger = logging.getLogger(
|
|
23
|
+
logger = logging.getLogger("docker-helper")
|
|
24
|
+
|
|
16
25
|
|
|
17
26
|
class Verbosity:
|
|
18
27
|
"""Handle verbosity levels for logging."""
|
|
28
|
+
|
|
19
29
|
def __init__(self, verbosity: int = 0):
|
|
20
30
|
self.verbosity = verbosity
|
|
21
31
|
self.set_level()
|
|
@@ -50,68 +60,97 @@ class Verbosity:
|
|
|
50
60
|
"""Log error message regardless of verbosity."""
|
|
51
61
|
logger.error(msg, *args, **kwargs)
|
|
52
62
|
|
|
63
|
+
|
|
53
64
|
def get_container_ports(container_id: str, verbosity: Verbosity) -> List[Dict]:
|
|
54
|
-
"""Get exposed ports and IPs for a container.
|
|
55
|
-
|
|
65
|
+
"""Get exposed ports and IPs for a container.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
container_id: The ID of the container to inspect
|
|
69
|
+
verbosity: Verbosity level for logging
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
List of dictionaries containing port mappings
|
|
73
|
+
"""
|
|
74
|
+
verbosity.debug("Getting ports for container %s", container_id)
|
|
56
75
|
try:
|
|
57
76
|
result = subprocess.run(
|
|
58
|
-
[
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
77
|
+
[
|
|
78
|
+
"docker",
|
|
79
|
+
"inspect",
|
|
80
|
+
"--format",
|
|
81
|
+
"{{range $p, $conf := .NetworkSettings.Ports}}" # noqa: E501
|
|
82
|
+
"{{range $h, $hosts := $conf}}"
|
|
83
|
+
"{{$p}}|{{$hosts.HostIp}}|{{$hosts.HostPort}};"
|
|
84
|
+
"{{end}}{{end}}",
|
|
85
|
+
container_id,
|
|
86
|
+
],
|
|
64
87
|
capture_output=True,
|
|
65
|
-
text=True
|
|
88
|
+
text=True,
|
|
89
|
+
check=True,
|
|
66
90
|
)
|
|
67
91
|
|
|
68
|
-
if result.returncode != 0:
|
|
69
|
-
verbosity.error(f"Failed to get container info: {result.stderr}")
|
|
70
|
-
return []
|
|
71
|
-
|
|
72
92
|
ports = []
|
|
73
93
|
raw_output = result.stdout.strip()
|
|
74
|
-
verbosity.debug(
|
|
94
|
+
verbosity.debug("Raw port mappings: %s", raw_output)
|
|
75
95
|
|
|
76
|
-
if raw_output:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
if not raw_output:
|
|
97
|
+
return ports
|
|
98
|
+
|
|
99
|
+
for mapping in raw_output.split(";"):
|
|
100
|
+
if not mapping:
|
|
101
|
+
continue
|
|
102
|
+
try:
|
|
103
|
+
container_port, host_ip, host_port = mapping.split("|")
|
|
104
|
+
verbosity.debug(
|
|
105
|
+
"Processing mapping: container=%s, host_ip=%s, host_port=%s",
|
|
106
|
+
container_port,
|
|
107
|
+
host_ip,
|
|
108
|
+
host_port,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if container_port and host_port:
|
|
112
|
+
port_info = {
|
|
113
|
+
"container_port": container_port.split("/")[
|
|
114
|
+
0
|
|
115
|
+
], # Remove /tcp or /udp
|
|
116
|
+
"host_ip": (
|
|
117
|
+
host_ip if host_ip not in ("0.0.0.0", "") else "localhost"
|
|
118
|
+
),
|
|
119
|
+
"host_port": host_port,
|
|
120
|
+
}
|
|
121
|
+
verbosity.info("Added port mapping: %s", port_info)
|
|
122
|
+
ports.append(port_info)
|
|
123
|
+
else:
|
|
124
|
+
verbosity.debug("Skipping incomplete mapping: %s", mapping)
|
|
125
|
+
except ValueError as e:
|
|
126
|
+
verbosity.warning("Failed to parse mapping '%s': %s", mapping, e)
|
|
127
|
+
|
|
128
|
+
verbosity.debug("Final port mappings: %s", ports)
|
|
98
129
|
return ports
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
130
|
+
|
|
131
|
+
except subprocess.CalledProcessError as e:
|
|
132
|
+
verbosity.error("Failed to get container info: %s", e.stderr)
|
|
133
|
+
except Exception as e: # pylint: disable=broad-except
|
|
134
|
+
verbosity.error(
|
|
135
|
+
"Unexpected error in get_container_ports: %s",
|
|
136
|
+
str(e),
|
|
137
|
+
exc_info=verbosity.verbosity >= 3,
|
|
138
|
+
)
|
|
139
|
+
return []
|
|
140
|
+
|
|
102
141
|
|
|
103
142
|
def check_docker(verbosity: Verbosity) -> bool:
|
|
104
143
|
"""Check if Docker is installed and running."""
|
|
105
144
|
verbosity.info("Checking if Docker is installed and running...")
|
|
106
145
|
try:
|
|
107
|
-
result = subprocess.run([
|
|
108
|
-
capture_output=True,
|
|
109
|
-
text=True)
|
|
146
|
+
result = subprocess.run(["docker", "info"], capture_output=True, text=True)
|
|
110
147
|
|
|
111
148
|
verbosity.debug(f"Docker info command output:\n{result.stdout}")
|
|
112
149
|
|
|
113
150
|
if result.returncode != 0:
|
|
114
|
-
verbosity.error(
|
|
151
|
+
verbosity.error(
|
|
152
|
+
f"Docker is not running or not accessible. Error: {result.stderr}"
|
|
153
|
+
)
|
|
115
154
|
return False
|
|
116
155
|
|
|
117
156
|
verbosity.info("Docker is running and accessible")
|
|
@@ -121,77 +160,119 @@ def check_docker(verbosity: Verbosity) -> bool:
|
|
|
121
160
|
verbosity.error("Docker command not found. Is Docker installed?")
|
|
122
161
|
return False
|
|
123
162
|
except Exception as e:
|
|
124
|
-
verbosity.error(
|
|
163
|
+
verbosity.error(
|
|
164
|
+
f"Unexpected error checking Docker: {str(e)}",
|
|
165
|
+
exc_info=verbosity.verbosity >= 3,
|
|
166
|
+
)
|
|
125
167
|
return False
|
|
126
168
|
|
|
127
|
-
|
|
169
|
+
|
|
170
|
+
def format_output(output, output_format="table"):
|
|
128
171
|
"""Format command output based on the specified format."""
|
|
129
|
-
if output_format ==
|
|
172
|
+
if output_format == "json":
|
|
130
173
|
try:
|
|
131
174
|
return json.dumps(json.loads(output), indent=2)
|
|
132
175
|
except json.JSONDecodeError:
|
|
133
176
|
return output
|
|
134
177
|
return output
|
|
135
178
|
|
|
179
|
+
|
|
136
180
|
def get_verbosity(ctx: click.Context) -> Verbosity:
|
|
137
181
|
"""Get verbosity level from context."""
|
|
138
182
|
# Count the number of 'v's in the --verbose flag
|
|
139
|
-
verbose = ctx.params.get(
|
|
183
|
+
verbose = ctx.params.get("verbose", 0)
|
|
140
184
|
verbosity = Verbosity(verbosity=verbose)
|
|
141
185
|
verbosity.info(f"Verbosity level set to {verbose}")
|
|
142
186
|
return verbosity
|
|
143
187
|
|
|
188
|
+
|
|
144
189
|
@click.group()
|
|
145
|
-
@click.option(
|
|
190
|
+
@click.option(
|
|
191
|
+
"-v",
|
|
192
|
+
"--verbose",
|
|
193
|
+
count=True,
|
|
194
|
+
help="Increase verbosity (can be used multiple times)",
|
|
195
|
+
)
|
|
146
196
|
@click.pass_context
|
|
147
197
|
def docker(ctx, verbose):
|
|
148
|
-
"""Docker management commands.
|
|
149
|
-
ctx.ensure_object(dict)
|
|
198
|
+
"""Docker management commands (v{}).
|
|
150
199
|
|
|
200
|
+
This command provides various Docker management subcommands including:
|
|
201
|
+
- ps: List containers
|
|
202
|
+
- run: Run a command in a new container
|
|
203
|
+
- rm: Remove one or more containers
|
|
204
|
+
- rmi: Remove one or more images
|
|
205
|
+
- url: Show containers with their HTTP/HTTPS URLs
|
|
206
|
+
|
|
207
|
+
Use --help with any subcommand for more information.
|
|
208
|
+
""".format(__version__)
|
|
209
|
+
ctx.ensure_object(dict)
|
|
210
|
+
|
|
151
211
|
# Get verbosity from parent context if it exists, otherwise use the flag value
|
|
152
|
-
parent_verbosity = ctx.obj.get(
|
|
212
|
+
parent_verbosity = ctx.obj.get("verbosity", 0) if hasattr(ctx, "obj") else 0
|
|
153
213
|
verbosity_level = max(verbose, parent_verbosity)
|
|
154
|
-
|
|
214
|
+
|
|
155
215
|
# Initialize verbosity
|
|
156
216
|
verbosity = Verbosity(verbosity=verbosity_level)
|
|
157
|
-
ctx.obj[
|
|
158
|
-
|
|
159
|
-
#
|
|
160
|
-
logger
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
logger.debug(
|
|
217
|
+
ctx.obj["verbosity"] = verbosity
|
|
218
|
+
|
|
219
|
+
# Configure logger with verbosity level
|
|
220
|
+
logger.setLevel(
|
|
221
|
+
logging.DEBUG
|
|
222
|
+
if verbosity_level >= 3
|
|
223
|
+
else (
|
|
224
|
+
logging.INFO
|
|
225
|
+
if verbosity_level == 2
|
|
226
|
+
else logging.WARNING if verbosity_level == 1 else logging.ERROR
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
logger.debug(
|
|
231
|
+
"Docker command group initialized with verbosity level: %s", verbosity_level
|
|
232
|
+
)
|
|
171
233
|
|
|
172
234
|
verbosity.debug("Initializing Docker command group")
|
|
173
235
|
if not check_docker(verbosity):
|
|
174
|
-
click.echo(
|
|
236
|
+
click.echo(
|
|
237
|
+
"Error: Docker is not installed or not running. Please start Docker and try again.",
|
|
238
|
+
err=True,
|
|
239
|
+
)
|
|
175
240
|
ctx.exit(1)
|
|
176
241
|
|
|
177
|
-
|
|
178
|
-
@
|
|
179
|
-
|
|
180
|
-
|
|
242
|
+
|
|
243
|
+
@docker.command(
|
|
244
|
+
context_settings={"ignore_unknown_options": True, "allow_extra_args": True}
|
|
245
|
+
)
|
|
246
|
+
@click.option(
|
|
247
|
+
"--all", "-a", is_flag=True, help="Show all containers (default shows just running)"
|
|
248
|
+
)
|
|
249
|
+
@click.option(
|
|
250
|
+
"--all-containers",
|
|
251
|
+
is_flag=True,
|
|
252
|
+
help="Show all containers (default shows just running)",
|
|
253
|
+
)
|
|
254
|
+
@click.option(
|
|
255
|
+
"--format",
|
|
256
|
+
"-f",
|
|
257
|
+
type=click.Choice(["table", "json"], case_sensitive=False),
|
|
258
|
+
default="table",
|
|
259
|
+
help="Output format",
|
|
260
|
+
)
|
|
261
|
+
@click.help_option("--help", "-h")
|
|
181
262
|
@click.pass_context
|
|
182
|
-
def ps(ctx,
|
|
263
|
+
def ps(ctx, all_containers, output_format): # pylint: disable=redefined-builtin
|
|
183
264
|
"""List containers."""
|
|
184
|
-
verbosity = ctx.obj[
|
|
185
|
-
cmd = [
|
|
186
|
-
if
|
|
187
|
-
cmd.append(
|
|
265
|
+
verbosity = ctx.obj["verbosity"]
|
|
266
|
+
cmd = ["docker", "ps"]
|
|
267
|
+
if all_containers:
|
|
268
|
+
cmd.append("-a")
|
|
188
269
|
|
|
189
270
|
try:
|
|
190
271
|
verbosity.debug(f"Running command: {' '.join(cmd)}")
|
|
191
272
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
192
|
-
|
|
273
|
+
|
|
193
274
|
if result.returncode == 0:
|
|
194
|
-
if
|
|
275
|
+
if output_format == "json":
|
|
195
276
|
# Try to parse and pretty-print JSON output
|
|
196
277
|
try:
|
|
197
278
|
data = json.loads(result.stdout)
|
|
@@ -201,27 +282,42 @@ def ps(ctx, all, format):
|
|
|
201
282
|
click.echo(result.stdout)
|
|
202
283
|
else:
|
|
203
284
|
# For table format, try to align columns
|
|
204
|
-
lines = result.stdout.strip().split(
|
|
285
|
+
lines = result.stdout.strip().split("\n")
|
|
205
286
|
if len(lines) > 1:
|
|
206
287
|
# Parse as JSON to handle special characters in values
|
|
207
288
|
try:
|
|
208
289
|
data = [json.loads(line) for line in lines[1:]]
|
|
209
290
|
headers = data[0].keys()
|
|
210
|
-
rows = [
|
|
291
|
+
rows = [
|
|
292
|
+
[item.get(header, "") for header in headers]
|
|
293
|
+
for item in data
|
|
294
|
+
]
|
|
211
295
|
|
|
212
296
|
# Calculate column widths
|
|
213
|
-
col_widths = [
|
|
214
|
-
|
|
215
|
-
|
|
297
|
+
col_widths = [
|
|
298
|
+
max(
|
|
299
|
+
len(str(header)),
|
|
300
|
+
max((len(str(row[i])) for row in rows), default=0),
|
|
301
|
+
)
|
|
302
|
+
for i, header in enumerate(headers)
|
|
303
|
+
]
|
|
216
304
|
|
|
217
305
|
# Print header
|
|
218
|
-
header_row = " ".join(
|
|
306
|
+
header_row = " ".join(
|
|
307
|
+
header.ljust(width)
|
|
308
|
+
for header, width in zip(headers, col_widths)
|
|
309
|
+
)
|
|
219
310
|
click.echo(header_row)
|
|
220
311
|
click.echo("-" * len(header_row))
|
|
221
312
|
|
|
222
313
|
# Print rows
|
|
223
314
|
for row in rows:
|
|
224
|
-
click.echo(
|
|
315
|
+
click.echo(
|
|
316
|
+
" ".join(
|
|
317
|
+
str(cell).ljust(width)
|
|
318
|
+
for cell, width in zip(row, col_widths)
|
|
319
|
+
)
|
|
320
|
+
)
|
|
225
321
|
except Exception as e:
|
|
226
322
|
verbosity.debug(f"Error formatting table: {str(e)}")
|
|
227
323
|
# Fall back to raw output if processing fails
|
|
@@ -241,52 +337,62 @@ def ps(ctx, all, format):
|
|
|
241
337
|
verbosity.error(error_msg)
|
|
242
338
|
click.echo(error_msg, err=True)
|
|
243
339
|
|
|
244
|
-
|
|
245
|
-
@
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
@click.
|
|
249
|
-
@click.option(
|
|
250
|
-
@click.option(
|
|
340
|
+
|
|
341
|
+
@docker.command(
|
|
342
|
+
context_settings={"ignore_unknown_options": True, "allow_extra_args": True}
|
|
343
|
+
)
|
|
344
|
+
@click.argument("image", required=False)
|
|
345
|
+
@click.option("--name", help="Assign a name to the container")
|
|
346
|
+
@click.option(
|
|
347
|
+
"--port", "-p", multiple=True, help="Publish a container's port(s) to the host"
|
|
348
|
+
)
|
|
349
|
+
@click.option(
|
|
350
|
+
"--detach",
|
|
351
|
+
"-d",
|
|
352
|
+
is_flag=True,
|
|
353
|
+
help="Run container in background and print container ID",
|
|
354
|
+
)
|
|
355
|
+
@click.option("--env", "-e", multiple=True, help="Set environment variables")
|
|
356
|
+
@click.option("--volume", "-v", multiple=True, help="Bind mount a volume")
|
|
251
357
|
@click.pass_context
|
|
252
358
|
def run(ctx, image, name, port, detach, env, volume):
|
|
253
359
|
"""Run a command in a new container."""
|
|
254
|
-
verbosity = ctx.obj[
|
|
255
|
-
cmd = [
|
|
360
|
+
verbosity = ctx.obj["verbosity"]
|
|
361
|
+
cmd = ["docker", "run"]
|
|
256
362
|
|
|
257
363
|
if name:
|
|
258
|
-
cmd.extend([
|
|
364
|
+
cmd.extend(["--name", name])
|
|
259
365
|
verbosity.debug(f"Setting container name: {name}")
|
|
260
366
|
|
|
261
367
|
for p in port:
|
|
262
|
-
cmd.extend([
|
|
368
|
+
cmd.extend(["-p", p])
|
|
263
369
|
verbosity.debug(f"Adding port mapping: {p}")
|
|
264
370
|
|
|
265
371
|
if detach:
|
|
266
|
-
cmd.append(
|
|
372
|
+
cmd.append("-d")
|
|
267
373
|
verbosity.debug("Running container in detached mode")
|
|
268
374
|
|
|
269
375
|
for e in env:
|
|
270
|
-
cmd.extend([
|
|
376
|
+
cmd.extend(["-e", e])
|
|
271
377
|
verbosity.debug(f"Setting environment variable: {e}")
|
|
272
378
|
|
|
273
379
|
for v in volume:
|
|
274
|
-
cmd.extend([
|
|
380
|
+
cmd.extend(["-v", v])
|
|
275
381
|
verbosity.debug(f"Mounting volume: {v}")
|
|
276
382
|
|
|
277
383
|
if image:
|
|
278
384
|
cmd.append(image)
|
|
279
385
|
verbosity.debug(f"Using image: {image}")
|
|
280
|
-
|
|
386
|
+
|
|
281
387
|
# Add any remaining arguments
|
|
282
|
-
if hasattr(ctx,
|
|
388
|
+
if hasattr(ctx, "args") and ctx.args:
|
|
283
389
|
cmd.extend(ctx.args)
|
|
284
390
|
verbosity.debug(f"Additional arguments: {' '.join(ctx.args)}")
|
|
285
391
|
|
|
286
392
|
try:
|
|
287
393
|
verbosity.debug(f"Running command: {' '.join(cmd)}")
|
|
288
394
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
289
|
-
|
|
395
|
+
|
|
290
396
|
if result.returncode == 0:
|
|
291
397
|
if result.stdout:
|
|
292
398
|
click.echo(result.stdout.strip())
|
|
@@ -296,35 +402,48 @@ def run(ctx, image, name, port, detach, env, volume):
|
|
|
296
402
|
verbosity.error(error_msg)
|
|
297
403
|
click.echo(error_msg, err=True)
|
|
298
404
|
ctx.exit(1)
|
|
299
|
-
|
|
405
|
+
|
|
300
406
|
except Exception as e:
|
|
301
407
|
error_msg = f"Failed to run container: {str(e)}"
|
|
302
408
|
verbosity.error(error_msg, exc_info=verbosity.verbosity >= 3)
|
|
303
409
|
click.echo(error_msg, err=True)
|
|
304
|
-
ctx.exit(1)
|
|
410
|
+
ctx.exit(1)
|
|
411
|
+
|
|
305
412
|
|
|
306
|
-
@docker.command(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
@click.
|
|
413
|
+
@docker.command(
|
|
414
|
+
context_settings={"ignore_unknown_options": True, "allow_extra_args": True}
|
|
415
|
+
)
|
|
416
|
+
@click.argument("containers", nargs=-1, required=False)
|
|
417
|
+
@click.option(
|
|
418
|
+
"--force",
|
|
419
|
+
"-f",
|
|
420
|
+
is_flag=True,
|
|
421
|
+
help="Force the removal of a running container (uses SIGKILL)",
|
|
422
|
+
)
|
|
423
|
+
@click.option(
|
|
424
|
+
"--volumes",
|
|
425
|
+
"-v",
|
|
426
|
+
is_flag=True,
|
|
427
|
+
help="Remove anonymous volumes associated with the container",
|
|
428
|
+
)
|
|
310
429
|
@click.pass_context
|
|
311
430
|
def rm(ctx, containers, force, volumes):
|
|
312
431
|
"""Remove one or more containers."""
|
|
313
|
-
verbosity = ctx.obj[
|
|
314
|
-
cmd = [
|
|
432
|
+
verbosity = ctx.obj["verbosity"]
|
|
433
|
+
cmd = ["docker", "rm"]
|
|
315
434
|
|
|
316
435
|
if force:
|
|
317
|
-
cmd.append(
|
|
436
|
+
cmd.append("-f")
|
|
318
437
|
verbosity.debug("Force removal enabled")
|
|
319
438
|
if volumes:
|
|
320
|
-
cmd.append(
|
|
439
|
+
cmd.append("-v")
|
|
321
440
|
verbosity.debug("Volume removal enabled")
|
|
322
441
|
|
|
323
442
|
# Get containers from both the containers argument and any remaining args
|
|
324
443
|
all_containers = list(containers)
|
|
325
|
-
if hasattr(ctx,
|
|
444
|
+
if hasattr(ctx, "args") and ctx.args:
|
|
326
445
|
all_containers.extend(ctx.args)
|
|
327
|
-
|
|
446
|
+
|
|
328
447
|
if not all_containers:
|
|
329
448
|
error_msg = "Error: You must specify at least one container"
|
|
330
449
|
verbosity.error(error_msg)
|
|
@@ -337,7 +456,7 @@ def rm(ctx, containers, force, volumes):
|
|
|
337
456
|
try:
|
|
338
457
|
verbosity.debug(f"Running command: {' '.join(cmd)}")
|
|
339
458
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
340
|
-
|
|
459
|
+
|
|
341
460
|
if result.returncode == 0:
|
|
342
461
|
if result.stdout.strip():
|
|
343
462
|
click.echo(result.stdout.strip())
|
|
@@ -347,27 +466,39 @@ def rm(ctx, containers, force, volumes):
|
|
|
347
466
|
verbosity.error(error_msg)
|
|
348
467
|
click.echo(error_msg, err=True)
|
|
349
468
|
ctx.exit(1)
|
|
350
|
-
|
|
469
|
+
|
|
351
470
|
except Exception as e:
|
|
352
471
|
error_msg = f"Failed to remove containers: {str(e)}"
|
|
353
472
|
verbosity.error(error_msg, exc_info=verbosity.verbosity >= 3)
|
|
354
473
|
click.echo(error_msg, err=True)
|
|
355
|
-
ctx.exit(1)
|
|
474
|
+
ctx.exit(1)
|
|
356
475
|
|
|
357
|
-
|
|
358
|
-
@
|
|
359
|
-
|
|
476
|
+
|
|
477
|
+
@docker.command(
|
|
478
|
+
context_settings={"ignore_unknown_options": True, "allow_extra_args": True}
|
|
479
|
+
)
|
|
480
|
+
@click.option(
|
|
481
|
+
"--show-all",
|
|
482
|
+
"-a",
|
|
483
|
+
is_flag=True,
|
|
484
|
+
help="Show all containers (default shows just running)",
|
|
485
|
+
)
|
|
486
|
+
@click.option(
|
|
487
|
+
"--http-only", "-h", is_flag=True, help="Show only containers with HTTP/HTTPS ports"
|
|
488
|
+
)
|
|
360
489
|
@click.pass_context
|
|
361
490
|
def url(ctx, show_all, http_only):
|
|
362
491
|
"""Show containers with their HTTP/HTTPS URLs."""
|
|
363
|
-
verbosity = ctx.obj[
|
|
364
|
-
verbosity.info(
|
|
492
|
+
verbosity = ctx.obj["verbosity"]
|
|
493
|
+
verbosity.info(
|
|
494
|
+
f"Starting url command with show_all={show_all}, http_only={http_only}"
|
|
495
|
+
)
|
|
365
496
|
|
|
366
497
|
try:
|
|
367
498
|
# Get all containers
|
|
368
|
-
cmd = [
|
|
499
|
+
cmd = ["docker", "ps", "--format", "{{.ID}}|{{.Names}}|{{.Status}}|{{.Ports}}"]
|
|
369
500
|
if show_all:
|
|
370
|
-
cmd.append(
|
|
501
|
+
cmd.append("-a")
|
|
371
502
|
|
|
372
503
|
verbosity.debug(f"Running command: {' '.join(cmd)}")
|
|
373
504
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
@@ -383,7 +514,7 @@ def url(ctx, show_all, http_only):
|
|
|
383
514
|
running_containers = []
|
|
384
515
|
stopped_containers = []
|
|
385
516
|
|
|
386
|
-
container_lines = result.stdout.strip().split(
|
|
517
|
+
container_lines = result.stdout.strip().split("\n")
|
|
387
518
|
verbosity.info(f"Found {len(container_lines)} container(s)")
|
|
388
519
|
|
|
389
520
|
for line in container_lines:
|
|
@@ -393,16 +524,18 @@ def url(ctx, show_all, http_only):
|
|
|
393
524
|
|
|
394
525
|
try:
|
|
395
526
|
verbosity.debug(f"Processing container line: {line}")
|
|
396
|
-
container_id, name, status, ports = line.split(
|
|
397
|
-
is_running =
|
|
398
|
-
verbosity.info(
|
|
527
|
+
container_id, name, status, ports = line.split("|", 3)
|
|
528
|
+
is_running = "Up" in status
|
|
529
|
+
verbosity.info(
|
|
530
|
+
f"Container: ID={container_id[:12]}, Name={name}, Status={status}, Running={is_running}"
|
|
531
|
+
)
|
|
399
532
|
|
|
400
533
|
# Get container details
|
|
401
534
|
container_info = {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
535
|
+
"id": container_id[:12], # Short ID
|
|
536
|
+
"name": name,
|
|
537
|
+
"status": status,
|
|
538
|
+
"urls": [],
|
|
406
539
|
}
|
|
407
540
|
|
|
408
541
|
# Get exposed ports and their mappings
|
|
@@ -410,57 +543,63 @@ def url(ctx, show_all, http_only):
|
|
|
410
543
|
verbosity.debug(f"Found {len(port_mappings)} port mappings for {name}")
|
|
411
544
|
|
|
412
545
|
for port in port_mappings:
|
|
413
|
-
if not port.get(
|
|
546
|
+
if not port.get("host_port") or not port.get("container_port"):
|
|
414
547
|
verbosity.debug(f"Skipping incomplete port mapping: {port}")
|
|
415
548
|
continue
|
|
416
|
-
|
|
549
|
+
|
|
417
550
|
verbosity.debug(f"Checking port mapping: {port}")
|
|
418
|
-
verbosity.debug(
|
|
419
|
-
|
|
551
|
+
verbosity.debug(
|
|
552
|
+
f"Container name: {name}, Port: {port['container_port']}"
|
|
553
|
+
)
|
|
554
|
+
|
|
420
555
|
# Handle different port string formats (e.g., '8069/tcp', '0.0.0.0:8080->80/tcp')
|
|
421
|
-
port_str = port[
|
|
422
|
-
|
|
556
|
+
port_str = port["container_port"]
|
|
557
|
+
|
|
423
558
|
# Extract port number and protocol
|
|
424
559
|
port_num = None
|
|
425
|
-
protocol =
|
|
426
|
-
|
|
560
|
+
protocol = "tcp" # default protocol
|
|
561
|
+
|
|
427
562
|
# Handle format like '8069/tcp' or '80/http'
|
|
428
|
-
if
|
|
429
|
-
port_num, protocol = port_str.split(
|
|
563
|
+
if "/" in port_str:
|
|
564
|
+
port_num, protocol = port_str.split("/", 1)
|
|
430
565
|
# Handle format like '0.0.0.0:8080->80/tcp'
|
|
431
|
-
elif
|
|
432
|
-
_, port_mapping = port_str.split(
|
|
433
|
-
if
|
|
434
|
-
port_num, protocol = port_mapping.split(
|
|
566
|
+
elif "->" in port_str:
|
|
567
|
+
_, port_mapping = port_str.split("->", 1)
|
|
568
|
+
if "/" in port_mapping:
|
|
569
|
+
port_num, protocol = port_mapping.split("/", 1)
|
|
435
570
|
else:
|
|
436
571
|
port_num = port_mapping
|
|
437
572
|
else:
|
|
438
573
|
port_num = port_str
|
|
439
|
-
|
|
574
|
+
|
|
440
575
|
# Clean up port number (remove any non-numeric characters)
|
|
441
|
-
port_num =
|
|
442
|
-
|
|
576
|
+
port_num = "".join(c for c in port_num if c.isdigit())
|
|
577
|
+
|
|
443
578
|
# Map all ports to HTTP URLs
|
|
444
579
|
if port_num: # Process all ports regardless of protocol
|
|
445
|
-
scheme =
|
|
446
|
-
|
|
580
|
+
scheme = "http"
|
|
581
|
+
|
|
447
582
|
# Handle IPv6 addresses (add brackets if needed)
|
|
448
|
-
host = port[
|
|
449
|
-
if
|
|
450
|
-
host = f
|
|
451
|
-
|
|
583
|
+
host = port["host_ip"]
|
|
584
|
+
if ":" in host and not host.startswith("["):
|
|
585
|
+
host = f"[{host}]"
|
|
586
|
+
verbosity.info(
|
|
587
|
+
f"Skipping non-HTTP port: {port['container_port']}"
|
|
588
|
+
)
|
|
589
|
+
continue
|
|
590
|
+
|
|
452
591
|
url = f"{scheme}://{host}:{port['host_port']}"
|
|
453
|
-
|
|
454
|
-
container_info[
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
592
|
+
|
|
593
|
+
container_info["urls"].append(
|
|
594
|
+
{"url": url, "port": port_num, "protocol": protocol}
|
|
595
|
+
)
|
|
596
|
+
verbosity.info(
|
|
597
|
+
f"Added URL for {name}: {url} (port {port_num}/{protocol})"
|
|
598
|
+
)
|
|
460
599
|
verbosity.info(f"Added URL for {name}: {url} (port {port_num})")
|
|
461
600
|
|
|
462
601
|
# If http_only is True and no HTTP URLs, skip this container
|
|
463
|
-
if http_only and not container_info[
|
|
602
|
+
if http_only and not container_info["urls"]:
|
|
464
603
|
continue
|
|
465
604
|
|
|
466
605
|
if is_running:
|
|
@@ -474,71 +613,97 @@ def url(ctx, show_all, http_only):
|
|
|
474
613
|
|
|
475
614
|
# Display running containers
|
|
476
615
|
if running_containers:
|
|
477
|
-
click.secho("\n🚀 Running Containers:", fg=
|
|
616
|
+
click.secho("\n🚀 Running Containers:", fg="green", bold=True)
|
|
478
617
|
for container in running_containers:
|
|
479
618
|
verbosity.debug(f"Displaying running container: {container['name']}")
|
|
480
|
-
click.echo(
|
|
619
|
+
click.echo(
|
|
620
|
+
f"\n{click.style('●', fg='green')} {click.style(container['name'], bold=True)} ({container['id']})"
|
|
621
|
+
)
|
|
481
622
|
|
|
482
|
-
if container[
|
|
483
|
-
verbosity.info(
|
|
484
|
-
|
|
623
|
+
if container["urls"]:
|
|
624
|
+
verbosity.info(
|
|
625
|
+
f"Found {len(container['urls'])} URLs for {container['name']}"
|
|
626
|
+
)
|
|
627
|
+
for url_info in container["urls"]:
|
|
485
628
|
verbosity.debug(f"Displaying URL: {url_info['url']}")
|
|
486
|
-
click.echo(
|
|
629
|
+
click.echo(
|
|
630
|
+
f" {click.style('→', fg='blue')} {click.style(url_info['url'], fg='blue', underline=True)}"
|
|
631
|
+
)
|
|
487
632
|
else:
|
|
488
633
|
verbosity.debug(f"No URLs found for {container['name']}")
|
|
489
634
|
|
|
490
635
|
# Display stopped containers
|
|
491
636
|
if stopped_containers and (show_all or not http_only):
|
|
492
|
-
click.secho("\n⏸️ Stopped Containers:", fg=
|
|
637
|
+
click.secho("\n⏸️ Stopped Containers:", fg="yellow", bold=True)
|
|
493
638
|
for container in stopped_containers:
|
|
494
639
|
verbosity.debug(f"Displaying stopped container: {container['name']}")
|
|
495
|
-
click.echo(
|
|
640
|
+
click.echo(
|
|
641
|
+
f"\n{click.style('●', fg='yellow')} {click.style(container['name'], dim=True)} ({container['id']})"
|
|
642
|
+
)
|
|
496
643
|
|
|
497
|
-
if container[
|
|
498
|
-
verbosity.info(
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
644
|
+
if container["urls"]:
|
|
645
|
+
verbosity.info(
|
|
646
|
+
f"Found {len(container['urls'])} URLs for stopped container {container['name']}"
|
|
647
|
+
)
|
|
648
|
+
for url_info in container["urls"]:
|
|
649
|
+
verbosity.debug(
|
|
650
|
+
f"Displaying URL for stopped container: {url_info['url']}"
|
|
651
|
+
)
|
|
652
|
+
click.echo(
|
|
653
|
+
f" {click.style('→', fg='blue')} {click.style(url_info['url'], fg='blue', underline=True, dim=True)}"
|
|
654
|
+
)
|
|
502
655
|
else:
|
|
503
|
-
verbosity.debug(
|
|
656
|
+
verbosity.debug(
|
|
657
|
+
f"No URLs found for stopped container {container['name']}"
|
|
658
|
+
)
|
|
504
659
|
|
|
505
660
|
if not running_containers and not stopped_containers:
|
|
506
661
|
msg = "No containers found."
|
|
507
662
|
verbosity.info(msg)
|
|
508
663
|
click.echo(msg)
|
|
509
664
|
else:
|
|
510
|
-
verbosity.info(
|
|
665
|
+
verbosity.info(
|
|
666
|
+
f"Displayed {len(running_containers)} running and {len(stopped_containers)} stopped containers"
|
|
667
|
+
)
|
|
511
668
|
|
|
512
669
|
except Exception as e:
|
|
513
670
|
error_msg = f"Error in url command: {str(e)}"
|
|
514
671
|
verbosity.error(error_msg, exc_info=verbosity.verbosity >= 3)
|
|
515
672
|
click.echo(error_msg, err=True)
|
|
516
673
|
|
|
517
|
-
|
|
518
|
-
@
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
@click.
|
|
674
|
+
|
|
675
|
+
@docker.command(
|
|
676
|
+
context_settings={"ignore_unknown_options": True, "allow_extra_args": True}
|
|
677
|
+
)
|
|
678
|
+
@click.argument("image", required=False)
|
|
679
|
+
@click.option(
|
|
680
|
+
"--all-tags",
|
|
681
|
+
"-a",
|
|
682
|
+
is_flag=True,
|
|
683
|
+
help="Remove all versions of the image with the given name",
|
|
684
|
+
)
|
|
685
|
+
@click.option("--force", "-f", is_flag=True, help="Force removal of the image")
|
|
686
|
+
@click.option("--no-prune", is_flag=True, help="Do not delete untagged parents")
|
|
522
687
|
@click.pass_context
|
|
523
688
|
def rmi(ctx, image, all_tags, force, no_prune):
|
|
524
689
|
"""Remove one or more images."""
|
|
525
|
-
verbosity = ctx.obj[
|
|
526
|
-
cmd = [
|
|
690
|
+
verbosity = ctx.obj["verbosity"]
|
|
691
|
+
cmd = ["docker", "rmi"]
|
|
527
692
|
|
|
528
693
|
if force:
|
|
529
|
-
cmd.append(
|
|
694
|
+
cmd.append("-f")
|
|
530
695
|
verbosity.debug("Force removal enabled")
|
|
531
696
|
if no_prune:
|
|
532
|
-
cmd.append(
|
|
697
|
+
cmd.append("--no-prune")
|
|
533
698
|
verbosity.debug("Pruning of untagged parents disabled")
|
|
534
699
|
|
|
535
700
|
# Get images from both the image argument and any remaining args
|
|
536
701
|
images = []
|
|
537
702
|
if image:
|
|
538
703
|
images.append(image)
|
|
539
|
-
if hasattr(ctx,
|
|
704
|
+
if hasattr(ctx, "args") and ctx.args:
|
|
540
705
|
images.extend(ctx.args)
|
|
541
|
-
|
|
706
|
+
|
|
542
707
|
if not images and not all_tags:
|
|
543
708
|
error_msg = "Error: You must specify at least one image"
|
|
544
709
|
verbosity.error(error_msg)
|
|
@@ -551,38 +716,41 @@ def rmi(ctx, image, all_tags, force, no_prune):
|
|
|
551
716
|
verbosity.error(error_msg)
|
|
552
717
|
click.echo(error_msg, err=True)
|
|
553
718
|
ctx.exit(1)
|
|
554
|
-
|
|
719
|
+
|
|
555
720
|
# Get all tags for the specified images
|
|
556
721
|
all_tags_to_remove = []
|
|
557
722
|
for img in images:
|
|
558
723
|
verbosity.debug(f"Finding all tags for image: {img}")
|
|
559
724
|
try:
|
|
560
725
|
result = subprocess.run(
|
|
561
|
-
[
|
|
562
|
-
capture_output=True,
|
|
563
|
-
text=True
|
|
726
|
+
["docker", "images", "--format", "{{.Repository}}:{{.Tag}}", img],
|
|
727
|
+
capture_output=True,
|
|
728
|
+
text=True,
|
|
564
729
|
)
|
|
565
|
-
|
|
730
|
+
|
|
566
731
|
if result.returncode == 0 and result.stdout.strip():
|
|
567
|
-
tags = [line for line in result.stdout.split(
|
|
732
|
+
tags = [line for line in result.stdout.split("\n") if line]
|
|
568
733
|
verbosity.debug(f"Found {len(tags)} tags for {img}")
|
|
569
734
|
all_tags_to_remove.extend(tags)
|
|
570
735
|
else:
|
|
571
736
|
verbosity.warning(f"No images found matching '{img}'")
|
|
572
|
-
|
|
737
|
+
|
|
573
738
|
except Exception as e:
|
|
574
|
-
verbosity.error(
|
|
739
|
+
verbosity.error(
|
|
740
|
+
f"Error finding tags for {img}: {str(e)}",
|
|
741
|
+
exc_info=verbosity.verbosity >= 3,
|
|
742
|
+
)
|
|
575
743
|
continue
|
|
576
|
-
|
|
744
|
+
|
|
577
745
|
if not all_tags_to_remove:
|
|
578
746
|
error_msg = "No matching images found to remove"
|
|
579
747
|
verbosity.error(error_msg)
|
|
580
748
|
click.echo(error_msg, err=True)
|
|
581
749
|
ctx.exit(1)
|
|
582
|
-
|
|
750
|
+
|
|
583
751
|
cmd.extend(all_tags_to_remove)
|
|
584
752
|
verbosity.info(f"Removing {len(all_tags_to_remove)} image(s) with all tags")
|
|
585
|
-
|
|
753
|
+
|
|
586
754
|
else:
|
|
587
755
|
cmd.extend(images)
|
|
588
756
|
verbosity.info(f"Removing {len(images)} image(s)")
|
|
@@ -590,7 +758,7 @@ def rmi(ctx, image, all_tags, force, no_prune):
|
|
|
590
758
|
try:
|
|
591
759
|
verbosity.debug(f"Running command: {' '.join(cmd)}")
|
|
592
760
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
593
|
-
|
|
761
|
+
|
|
594
762
|
if result.returncode == 0:
|
|
595
763
|
if result.stdout.strip():
|
|
596
764
|
click.echo(result.stdout.strip())
|
|
@@ -601,9 +769,9 @@ def rmi(ctx, image, all_tags, force, no_prune):
|
|
|
601
769
|
verbosity.error(error_msg)
|
|
602
770
|
click.echo(error_msg, err=True)
|
|
603
771
|
ctx.exit(1)
|
|
604
|
-
|
|
772
|
+
|
|
605
773
|
except Exception as e:
|
|
606
774
|
error_msg = f"Failed to remove images: {str(e)}"
|
|
607
775
|
verbosity.error(error_msg, exc_info=verbosity.verbosity >= 3)
|
|
608
776
|
click.echo(error_msg, err=True)
|
|
609
|
-
ctx.exit(1)
|
|
777
|
+
ctx.exit(1)
|