helper-cli 0.1.16__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 +345 -181
- 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 +4 -5
- helper/commands/system_info.py +161 -111
- helper/commands/venv.py +130 -0
- helper/main.py +57 -31
- helper/table.py +29 -0
- {helper_cli-0.1.16.dist-info → helper_cli-0.1.21.dist-info}/METADATA +2 -2
- helper_cli-0.1.21.dist-info/RECORD +19 -0
- helper_cli-0.1.16.dist-info/RECORD +0 -15
- {helper_cli-0.1.16.dist-info → helper_cli-0.1.21.dist-info}/WHEEL +0 -0
- {helper_cli-0.1.16.dist-info → helper_cli-0.1.21.dist-info}/entry_points.txt +0 -0
- {helper_cli-0.1.16.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,79 +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.
|
|
198
|
+
"""Docker management commands (v{}).
|
|
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__)
|
|
149
209
|
ctx.ensure_object(dict)
|
|
150
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
|
-
|
|
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
|
+
)
|
|
169
229
|
|
|
170
|
-
logger.debug(
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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")
|
|
183
262
|
@click.pass_context
|
|
184
|
-
def ps(ctx,
|
|
263
|
+
def ps(ctx, all_containers, output_format): # pylint: disable=redefined-builtin
|
|
185
264
|
"""List containers."""
|
|
186
|
-
verbosity = ctx.obj[
|
|
187
|
-
cmd = [
|
|
188
|
-
if
|
|
189
|
-
cmd.append(
|
|
265
|
+
verbosity = ctx.obj["verbosity"]
|
|
266
|
+
cmd = ["docker", "ps"]
|
|
267
|
+
if all_containers:
|
|
268
|
+
cmd.append("-a")
|
|
190
269
|
|
|
191
270
|
try:
|
|
192
271
|
verbosity.debug(f"Running command: {' '.join(cmd)}")
|
|
193
272
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
194
273
|
|
|
195
274
|
if result.returncode == 0:
|
|
196
|
-
if
|
|
275
|
+
if output_format == "json":
|
|
197
276
|
# Try to parse and pretty-print JSON output
|
|
198
277
|
try:
|
|
199
278
|
data = json.loads(result.stdout)
|
|
@@ -203,27 +282,42 @@ def ps(ctx, all, format):
|
|
|
203
282
|
click.echo(result.stdout)
|
|
204
283
|
else:
|
|
205
284
|
# For table format, try to align columns
|
|
206
|
-
lines = result.stdout.strip().split(
|
|
285
|
+
lines = result.stdout.strip().split("\n")
|
|
207
286
|
if len(lines) > 1:
|
|
208
287
|
# Parse as JSON to handle special characters in values
|
|
209
288
|
try:
|
|
210
289
|
data = [json.loads(line) for line in lines[1:]]
|
|
211
290
|
headers = data[0].keys()
|
|
212
|
-
rows = [
|
|
291
|
+
rows = [
|
|
292
|
+
[item.get(header, "") for header in headers]
|
|
293
|
+
for item in data
|
|
294
|
+
]
|
|
213
295
|
|
|
214
296
|
# Calculate column widths
|
|
215
|
-
col_widths = [
|
|
216
|
-
|
|
217
|
-
|
|
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
|
+
]
|
|
218
304
|
|
|
219
305
|
# Print header
|
|
220
|
-
header_row = " ".join(
|
|
306
|
+
header_row = " ".join(
|
|
307
|
+
header.ljust(width)
|
|
308
|
+
for header, width in zip(headers, col_widths)
|
|
309
|
+
)
|
|
221
310
|
click.echo(header_row)
|
|
222
311
|
click.echo("-" * len(header_row))
|
|
223
312
|
|
|
224
313
|
# Print rows
|
|
225
314
|
for row in rows:
|
|
226
|
-
click.echo(
|
|
315
|
+
click.echo(
|
|
316
|
+
" ".join(
|
|
317
|
+
str(cell).ljust(width)
|
|
318
|
+
for cell, width in zip(row, col_widths)
|
|
319
|
+
)
|
|
320
|
+
)
|
|
227
321
|
except Exception as e:
|
|
228
322
|
verbosity.debug(f"Error formatting table: {str(e)}")
|
|
229
323
|
# Fall back to raw output if processing fails
|
|
@@ -243,37 +337,47 @@ def ps(ctx, all, format):
|
|
|
243
337
|
verbosity.error(error_msg)
|
|
244
338
|
click.echo(error_msg, err=True)
|
|
245
339
|
|
|
246
|
-
|
|
247
|
-
@
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
@click.
|
|
251
|
-
@click.option(
|
|
252
|
-
@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")
|
|
253
357
|
@click.pass_context
|
|
254
358
|
def run(ctx, image, name, port, detach, env, volume):
|
|
255
359
|
"""Run a command in a new container."""
|
|
256
|
-
verbosity = ctx.obj[
|
|
257
|
-
cmd = [
|
|
360
|
+
verbosity = ctx.obj["verbosity"]
|
|
361
|
+
cmd = ["docker", "run"]
|
|
258
362
|
|
|
259
363
|
if name:
|
|
260
|
-
cmd.extend([
|
|
364
|
+
cmd.extend(["--name", name])
|
|
261
365
|
verbosity.debug(f"Setting container name: {name}")
|
|
262
366
|
|
|
263
367
|
for p in port:
|
|
264
|
-
cmd.extend([
|
|
368
|
+
cmd.extend(["-p", p])
|
|
265
369
|
verbosity.debug(f"Adding port mapping: {p}")
|
|
266
370
|
|
|
267
371
|
if detach:
|
|
268
|
-
cmd.append(
|
|
372
|
+
cmd.append("-d")
|
|
269
373
|
verbosity.debug("Running container in detached mode")
|
|
270
374
|
|
|
271
375
|
for e in env:
|
|
272
|
-
cmd.extend([
|
|
376
|
+
cmd.extend(["-e", e])
|
|
273
377
|
verbosity.debug(f"Setting environment variable: {e}")
|
|
274
378
|
|
|
275
379
|
for v in volume:
|
|
276
|
-
cmd.extend([
|
|
380
|
+
cmd.extend(["-v", v])
|
|
277
381
|
verbosity.debug(f"Mounting volume: {v}")
|
|
278
382
|
|
|
279
383
|
if image:
|
|
@@ -281,7 +385,7 @@ def run(ctx, image, name, port, detach, env, volume):
|
|
|
281
385
|
verbosity.debug(f"Using image: {image}")
|
|
282
386
|
|
|
283
387
|
# Add any remaining arguments
|
|
284
|
-
if hasattr(ctx,
|
|
388
|
+
if hasattr(ctx, "args") and ctx.args:
|
|
285
389
|
cmd.extend(ctx.args)
|
|
286
390
|
verbosity.debug(f"Additional arguments: {' '.join(ctx.args)}")
|
|
287
391
|
|
|
@@ -305,26 +409,39 @@ def run(ctx, image, name, port, detach, env, volume):
|
|
|
305
409
|
click.echo(error_msg, err=True)
|
|
306
410
|
ctx.exit(1)
|
|
307
411
|
|
|
308
|
-
|
|
309
|
-
@
|
|
310
|
-
|
|
311
|
-
|
|
412
|
+
|
|
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
|
+
)
|
|
312
429
|
@click.pass_context
|
|
313
430
|
def rm(ctx, containers, force, volumes):
|
|
314
431
|
"""Remove one or more containers."""
|
|
315
|
-
verbosity = ctx.obj[
|
|
316
|
-
cmd = [
|
|
432
|
+
verbosity = ctx.obj["verbosity"]
|
|
433
|
+
cmd = ["docker", "rm"]
|
|
317
434
|
|
|
318
435
|
if force:
|
|
319
|
-
cmd.append(
|
|
436
|
+
cmd.append("-f")
|
|
320
437
|
verbosity.debug("Force removal enabled")
|
|
321
438
|
if volumes:
|
|
322
|
-
cmd.append(
|
|
439
|
+
cmd.append("-v")
|
|
323
440
|
verbosity.debug("Volume removal enabled")
|
|
324
441
|
|
|
325
442
|
# Get containers from both the containers argument and any remaining args
|
|
326
443
|
all_containers = list(containers)
|
|
327
|
-
if hasattr(ctx,
|
|
444
|
+
if hasattr(ctx, "args") and ctx.args:
|
|
328
445
|
all_containers.extend(ctx.args)
|
|
329
446
|
|
|
330
447
|
if not all_containers:
|
|
@@ -356,20 +473,32 @@ def rm(ctx, containers, force, volumes):
|
|
|
356
473
|
click.echo(error_msg, err=True)
|
|
357
474
|
ctx.exit(1)
|
|
358
475
|
|
|
359
|
-
|
|
360
|
-
@
|
|
361
|
-
|
|
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
|
+
)
|
|
362
489
|
@click.pass_context
|
|
363
490
|
def url(ctx, show_all, http_only):
|
|
364
491
|
"""Show containers with their HTTP/HTTPS URLs."""
|
|
365
|
-
verbosity = ctx.obj[
|
|
366
|
-
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
|
+
)
|
|
367
496
|
|
|
368
497
|
try:
|
|
369
498
|
# Get all containers
|
|
370
|
-
cmd = [
|
|
499
|
+
cmd = ["docker", "ps", "--format", "{{.ID}}|{{.Names}}|{{.Status}}|{{.Ports}}"]
|
|
371
500
|
if show_all:
|
|
372
|
-
cmd.append(
|
|
501
|
+
cmd.append("-a")
|
|
373
502
|
|
|
374
503
|
verbosity.debug(f"Running command: {' '.join(cmd)}")
|
|
375
504
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
@@ -385,7 +514,7 @@ def url(ctx, show_all, http_only):
|
|
|
385
514
|
running_containers = []
|
|
386
515
|
stopped_containers = []
|
|
387
516
|
|
|
388
|
-
container_lines = result.stdout.strip().split(
|
|
517
|
+
container_lines = result.stdout.strip().split("\n")
|
|
389
518
|
verbosity.info(f"Found {len(container_lines)} container(s)")
|
|
390
519
|
|
|
391
520
|
for line in container_lines:
|
|
@@ -395,16 +524,18 @@ def url(ctx, show_all, http_only):
|
|
|
395
524
|
|
|
396
525
|
try:
|
|
397
526
|
verbosity.debug(f"Processing container line: {line}")
|
|
398
|
-
container_id, name, status, ports = line.split(
|
|
399
|
-
is_running =
|
|
400
|
-
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
|
+
)
|
|
401
532
|
|
|
402
533
|
# Get container details
|
|
403
534
|
container_info = {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
535
|
+
"id": container_id[:12], # Short ID
|
|
536
|
+
"name": name,
|
|
537
|
+
"status": status,
|
|
538
|
+
"urls": [],
|
|
408
539
|
}
|
|
409
540
|
|
|
410
541
|
# Get exposed ports and their mappings
|
|
@@ -412,59 +543,63 @@ def url(ctx, show_all, http_only):
|
|
|
412
543
|
verbosity.debug(f"Found {len(port_mappings)} port mappings for {name}")
|
|
413
544
|
|
|
414
545
|
for port in port_mappings:
|
|
415
|
-
if not port.get(
|
|
546
|
+
if not port.get("host_port") or not port.get("container_port"):
|
|
416
547
|
verbosity.debug(f"Skipping incomplete port mapping: {port}")
|
|
417
548
|
continue
|
|
418
549
|
|
|
419
550
|
verbosity.debug(f"Checking port mapping: {port}")
|
|
420
|
-
verbosity.debug(
|
|
551
|
+
verbosity.debug(
|
|
552
|
+
f"Container name: {name}, Port: {port['container_port']}"
|
|
553
|
+
)
|
|
421
554
|
|
|
422
555
|
# Handle different port string formats (e.g., '8069/tcp', '0.0.0.0:8080->80/tcp')
|
|
423
|
-
port_str = port[
|
|
556
|
+
port_str = port["container_port"]
|
|
424
557
|
|
|
425
558
|
# Extract port number and protocol
|
|
426
559
|
port_num = None
|
|
427
|
-
protocol =
|
|
560
|
+
protocol = "tcp" # default protocol
|
|
428
561
|
|
|
429
562
|
# Handle format like '8069/tcp' or '80/http'
|
|
430
|
-
if
|
|
431
|
-
port_num, protocol = port_str.split(
|
|
563
|
+
if "/" in port_str:
|
|
564
|
+
port_num, protocol = port_str.split("/", 1)
|
|
432
565
|
# Handle format like '0.0.0.0:8080->80/tcp'
|
|
433
|
-
elif
|
|
434
|
-
_, port_mapping = port_str.split(
|
|
435
|
-
if
|
|
436
|
-
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)
|
|
437
570
|
else:
|
|
438
571
|
port_num = port_mapping
|
|
439
572
|
else:
|
|
440
573
|
port_num = port_str
|
|
441
574
|
|
|
442
575
|
# Clean up port number (remove any non-numeric characters)
|
|
443
|
-
port_num =
|
|
576
|
+
port_num = "".join(c for c in port_num if c.isdigit())
|
|
444
577
|
|
|
445
578
|
# Map all ports to HTTP URLs
|
|
446
579
|
if port_num: # Process all ports regardless of protocol
|
|
447
|
-
scheme =
|
|
580
|
+
scheme = "http"
|
|
448
581
|
|
|
449
582
|
# Handle IPv6 addresses (add brackets if needed)
|
|
450
|
-
host = port[
|
|
451
|
-
if
|
|
452
|
-
host = f
|
|
453
|
-
verbosity.info(
|
|
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
|
+
)
|
|
454
589
|
continue
|
|
455
590
|
|
|
456
591
|
url = f"{scheme}://{host}:{port['host_port']}"
|
|
457
592
|
|
|
458
|
-
container_info[
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
+
)
|
|
464
599
|
verbosity.info(f"Added URL for {name}: {url} (port {port_num})")
|
|
465
600
|
|
|
466
601
|
# If http_only is True and no HTTP URLs, skip this container
|
|
467
|
-
if http_only and not container_info[
|
|
602
|
+
if http_only and not container_info["urls"]:
|
|
468
603
|
continue
|
|
469
604
|
|
|
470
605
|
if is_running:
|
|
@@ -478,69 +613,95 @@ def url(ctx, show_all, http_only):
|
|
|
478
613
|
|
|
479
614
|
# Display running containers
|
|
480
615
|
if running_containers:
|
|
481
|
-
click.secho("\n🚀 Running Containers:", fg=
|
|
616
|
+
click.secho("\n🚀 Running Containers:", fg="green", bold=True)
|
|
482
617
|
for container in running_containers:
|
|
483
618
|
verbosity.debug(f"Displaying running container: {container['name']}")
|
|
484
|
-
click.echo(
|
|
619
|
+
click.echo(
|
|
620
|
+
f"\n{click.style('●', fg='green')} {click.style(container['name'], bold=True)} ({container['id']})"
|
|
621
|
+
)
|
|
485
622
|
|
|
486
|
-
if container[
|
|
487
|
-
verbosity.info(
|
|
488
|
-
|
|
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"]:
|
|
489
628
|
verbosity.debug(f"Displaying URL: {url_info['url']}")
|
|
490
|
-
click.echo(
|
|
629
|
+
click.echo(
|
|
630
|
+
f" {click.style('→', fg='blue')} {click.style(url_info['url'], fg='blue', underline=True)}"
|
|
631
|
+
)
|
|
491
632
|
else:
|
|
492
633
|
verbosity.debug(f"No URLs found for {container['name']}")
|
|
493
634
|
|
|
494
635
|
# Display stopped containers
|
|
495
636
|
if stopped_containers and (show_all or not http_only):
|
|
496
|
-
click.secho("\n⏸️ Stopped Containers:", fg=
|
|
637
|
+
click.secho("\n⏸️ Stopped Containers:", fg="yellow", bold=True)
|
|
497
638
|
for container in stopped_containers:
|
|
498
639
|
verbosity.debug(f"Displaying stopped container: {container['name']}")
|
|
499
|
-
click.echo(
|
|
640
|
+
click.echo(
|
|
641
|
+
f"\n{click.style('●', fg='yellow')} {click.style(container['name'], dim=True)} ({container['id']})"
|
|
642
|
+
)
|
|
500
643
|
|
|
501
|
-
if container[
|
|
502
|
-
verbosity.info(
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
+
)
|
|
506
655
|
else:
|
|
507
|
-
verbosity.debug(
|
|
656
|
+
verbosity.debug(
|
|
657
|
+
f"No URLs found for stopped container {container['name']}"
|
|
658
|
+
)
|
|
508
659
|
|
|
509
660
|
if not running_containers and not stopped_containers:
|
|
510
661
|
msg = "No containers found."
|
|
511
662
|
verbosity.info(msg)
|
|
512
663
|
click.echo(msg)
|
|
513
664
|
else:
|
|
514
|
-
verbosity.info(
|
|
665
|
+
verbosity.info(
|
|
666
|
+
f"Displayed {len(running_containers)} running and {len(stopped_containers)} stopped containers"
|
|
667
|
+
)
|
|
515
668
|
|
|
516
669
|
except Exception as e:
|
|
517
670
|
error_msg = f"Error in url command: {str(e)}"
|
|
518
671
|
verbosity.error(error_msg, exc_info=verbosity.verbosity >= 3)
|
|
519
672
|
click.echo(error_msg, err=True)
|
|
520
673
|
|
|
521
|
-
|
|
522
|
-
@
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
@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")
|
|
526
687
|
@click.pass_context
|
|
527
688
|
def rmi(ctx, image, all_tags, force, no_prune):
|
|
528
689
|
"""Remove one or more images."""
|
|
529
|
-
verbosity = ctx.obj[
|
|
530
|
-
cmd = [
|
|
690
|
+
verbosity = ctx.obj["verbosity"]
|
|
691
|
+
cmd = ["docker", "rmi"]
|
|
531
692
|
|
|
532
693
|
if force:
|
|
533
|
-
cmd.append(
|
|
694
|
+
cmd.append("-f")
|
|
534
695
|
verbosity.debug("Force removal enabled")
|
|
535
696
|
if no_prune:
|
|
536
|
-
cmd.append(
|
|
697
|
+
cmd.append("--no-prune")
|
|
537
698
|
verbosity.debug("Pruning of untagged parents disabled")
|
|
538
699
|
|
|
539
700
|
# Get images from both the image argument and any remaining args
|
|
540
701
|
images = []
|
|
541
702
|
if image:
|
|
542
703
|
images.append(image)
|
|
543
|
-
if hasattr(ctx,
|
|
704
|
+
if hasattr(ctx, "args") and ctx.args:
|
|
544
705
|
images.extend(ctx.args)
|
|
545
706
|
|
|
546
707
|
if not images and not all_tags:
|
|
@@ -562,20 +723,23 @@ def rmi(ctx, image, all_tags, force, no_prune):
|
|
|
562
723
|
verbosity.debug(f"Finding all tags for image: {img}")
|
|
563
724
|
try:
|
|
564
725
|
result = subprocess.run(
|
|
565
|
-
[
|
|
726
|
+
["docker", "images", "--format", "{{.Repository}}:{{.Tag}}", img],
|
|
566
727
|
capture_output=True,
|
|
567
|
-
text=True
|
|
728
|
+
text=True,
|
|
568
729
|
)
|
|
569
730
|
|
|
570
731
|
if result.returncode == 0 and result.stdout.strip():
|
|
571
|
-
tags = [line for line in result.stdout.split(
|
|
732
|
+
tags = [line for line in result.stdout.split("\n") if line]
|
|
572
733
|
verbosity.debug(f"Found {len(tags)} tags for {img}")
|
|
573
734
|
all_tags_to_remove.extend(tags)
|
|
574
735
|
else:
|
|
575
736
|
verbosity.warning(f"No images found matching '{img}'")
|
|
576
737
|
|
|
577
738
|
except Exception as e:
|
|
578
|
-
verbosity.error(
|
|
739
|
+
verbosity.error(
|
|
740
|
+
f"Error finding tags for {img}: {str(e)}",
|
|
741
|
+
exc_info=verbosity.verbosity >= 3,
|
|
742
|
+
)
|
|
579
743
|
continue
|
|
580
744
|
|
|
581
745
|
if not all_tags_to_remove:
|