cave-cli 3.5.0__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.
- cave_cli/__init__.py +3 -0
- cave_cli/cli.py +469 -0
- cave_cli/commands/__init__.py +0 -0
- cave_cli/commands/create.py +168 -0
- cave_cli/commands/kill.py +23 -0
- cave_cli/commands/list_cmd.py +37 -0
- cave_cli/commands/list_versions.py +93 -0
- cave_cli/commands/prettify.py +27 -0
- cave_cli/commands/purge.py +70 -0
- cave_cli/commands/reset.py +43 -0
- cave_cli/commands/run.py +189 -0
- cave_cli/commands/sync_cmd.py +79 -0
- cave_cli/commands/test.py +38 -0
- cave_cli/commands/uninstall.py +39 -0
- cave_cli/commands/update.py +38 -0
- cave_cli/commands/upgrade.py +70 -0
- cave_cli/commands/version.py +64 -0
- cave_cli/utils/__init__.py +0 -0
- cave_cli/utils/cache.py +235 -0
- cave_cli/utils/constants.py +43 -0
- cave_cli/utils/docker.py +530 -0
- cave_cli/utils/env.py +267 -0
- cave_cli/utils/git.py +240 -0
- cave_cli/utils/logger.py +77 -0
- cave_cli/utils/net.py +85 -0
- cave_cli/utils/subprocess.py +129 -0
- cave_cli/utils/sync.py +89 -0
- cave_cli/utils/validate.py +218 -0
- cave_cli-3.5.0.dist-info/METADATA +149 -0
- cave_cli-3.5.0.dist-info/RECORD +35 -0
- cave_cli-3.5.0.dist-info/WHEEL +5 -0
- cave_cli-3.5.0.dist-info/entry_points.txt +2 -0
- cave_cli-3.5.0.dist-info/licenses/LICENSE +201 -0
- cave_cli-3.5.0.dist-info/licenses/NOTICE.md +7 -0
- cave_cli-3.5.0.dist-info/top_level.txt +1 -0
cave_cli/utils/docker.py
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from cave_cli.utils.logger import logger
|
|
5
|
+
from cave_cli.utils.subprocess import run, run_and_log, version_tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def check_docker() -> None:
|
|
9
|
+
"""
|
|
10
|
+
Usage:
|
|
11
|
+
|
|
12
|
+
- Validates that Docker is installed, running, and meets the minimum version
|
|
13
|
+
|
|
14
|
+
Notes:
|
|
15
|
+
|
|
16
|
+
- Exits with code 1 if Docker is not installed, not running, or too old
|
|
17
|
+
"""
|
|
18
|
+
from cave_cli.utils.constants import MIN_DOCKER_VERSION
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
result = run(["docker", "--version"])
|
|
22
|
+
except FileNotFoundError:
|
|
23
|
+
logger.error(
|
|
24
|
+
f"Docker is not installed. "
|
|
25
|
+
f"Please install Docker version {MIN_DOCKER_VERSION} or greater.\n"
|
|
26
|
+
f"For more information see: https://docs.docker.com/get-docker/"
|
|
27
|
+
)
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
|
|
30
|
+
if result.returncode != 0 or not result.stdout:
|
|
31
|
+
logger.error(
|
|
32
|
+
f"Could not determine Docker version. "
|
|
33
|
+
f"Please install Docker version {MIN_DOCKER_VERSION} or greater."
|
|
34
|
+
)
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
|
|
37
|
+
version_str = result.stdout.strip()
|
|
38
|
+
import re
|
|
39
|
+
|
|
40
|
+
match = re.search(r"(\d+\.\d+\.\d+)", version_str)
|
|
41
|
+
if not match:
|
|
42
|
+
logger.error(f"Could not parse Docker version from: {version_str}")
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
|
|
45
|
+
current = match.group(1)
|
|
46
|
+
if version_tuple(current) < version_tuple(MIN_DOCKER_VERSION):
|
|
47
|
+
logger.error(
|
|
48
|
+
f"Your current Docker version ({current}) is too old.\n"
|
|
49
|
+
f"Please install Docker version {MIN_DOCKER_VERSION} or greater.\n"
|
|
50
|
+
f"For more information see: https://docs.docker.com/get-docker/"
|
|
51
|
+
)
|
|
52
|
+
sys.exit(1)
|
|
53
|
+
|
|
54
|
+
info_result = run(["docker", "info"])
|
|
55
|
+
if info_result.returncode != 0:
|
|
56
|
+
logger.error(
|
|
57
|
+
"Docker not running... Please start Docker and try again!"
|
|
58
|
+
)
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
|
|
61
|
+
logger.debug("Docker Check Passed!")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def build_image(app_name: str, path: str) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Usage:
|
|
67
|
+
|
|
68
|
+
- Builds a Docker image for the CAVE app
|
|
69
|
+
|
|
70
|
+
Requires:
|
|
71
|
+
|
|
72
|
+
- ``app_name``:
|
|
73
|
+
- Type: str
|
|
74
|
+
- What: The app name used for tagging the image
|
|
75
|
+
|
|
76
|
+
- ``path``:
|
|
77
|
+
- Type: str
|
|
78
|
+
- What: The build context directory
|
|
79
|
+
|
|
80
|
+
Notes:
|
|
81
|
+
|
|
82
|
+
- Streams build output and checks for ERROR lines
|
|
83
|
+
- Exits with code 1 if an error is detected during the build
|
|
84
|
+
"""
|
|
85
|
+
remove_containers(app_name)
|
|
86
|
+
logger.info("Getting Docker setup... (this may take a while)")
|
|
87
|
+
has_error = False
|
|
88
|
+
process = subprocess.Popen(
|
|
89
|
+
["docker", "build", ".", "--tag", f"cave-app:{app_name}"],
|
|
90
|
+
cwd=path,
|
|
91
|
+
stdout=subprocess.PIPE,
|
|
92
|
+
stderr=subprocess.STDOUT,
|
|
93
|
+
text=True,
|
|
94
|
+
encoding="utf-8",
|
|
95
|
+
errors="replace",
|
|
96
|
+
)
|
|
97
|
+
for line in process.stdout:
|
|
98
|
+
line = line.rstrip()
|
|
99
|
+
if "ERROR" in line:
|
|
100
|
+
has_error = True
|
|
101
|
+
logger.debug(line)
|
|
102
|
+
process.wait()
|
|
103
|
+
if has_error or process.returncode != 0:
|
|
104
|
+
logger.error(
|
|
105
|
+
"An ERROR was returned during the Docker container build process."
|
|
106
|
+
)
|
|
107
|
+
logger.error(
|
|
108
|
+
"The CAVE CLI command is exiting early due to this ERROR."
|
|
109
|
+
)
|
|
110
|
+
logger.error(
|
|
111
|
+
"Consider running your command again in verbose mode "
|
|
112
|
+
"to get more information."
|
|
113
|
+
)
|
|
114
|
+
logger.error("EG: 'cave reset --verbose' or 'cave run --verbose'")
|
|
115
|
+
sys.exit(1)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def create_network(app_name: str) -> None:
|
|
119
|
+
"""
|
|
120
|
+
Usage:
|
|
121
|
+
|
|
122
|
+
- Creates a Docker network for the CAVE app
|
|
123
|
+
|
|
124
|
+
Requires:
|
|
125
|
+
|
|
126
|
+
- ``app_name``:
|
|
127
|
+
- Type: str
|
|
128
|
+
- What: The app name used in the network name
|
|
129
|
+
"""
|
|
130
|
+
run_and_log(["docker", "network", "create", f"cave-net:{app_name}"])
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def remove_network(app_name: str) -> None:
|
|
134
|
+
"""
|
|
135
|
+
Usage:
|
|
136
|
+
|
|
137
|
+
- Removes the Docker network for the CAVE app
|
|
138
|
+
|
|
139
|
+
Requires:
|
|
140
|
+
|
|
141
|
+
- ``app_name``:
|
|
142
|
+
- Type: str
|
|
143
|
+
- What: The app name used in the network name
|
|
144
|
+
"""
|
|
145
|
+
run_and_log(["docker", "network", "rm", f"cave-net:{app_name}"])
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def run_detached(
|
|
149
|
+
name: str,
|
|
150
|
+
image: str,
|
|
151
|
+
network: str | None = None,
|
|
152
|
+
volumes: list[str] | None = None,
|
|
153
|
+
env_vars: dict[str, str] | None = None,
|
|
154
|
+
extra_args: list[str] | None = None,
|
|
155
|
+
command: list[str] | None = None,
|
|
156
|
+
) -> None:
|
|
157
|
+
"""
|
|
158
|
+
Usage:
|
|
159
|
+
|
|
160
|
+
- Runs a Docker container in detached mode
|
|
161
|
+
|
|
162
|
+
Requires:
|
|
163
|
+
|
|
164
|
+
- ``name``:
|
|
165
|
+
- Type: str
|
|
166
|
+
- What: The container name
|
|
167
|
+
|
|
168
|
+
- ``image``:
|
|
169
|
+
- Type: str
|
|
170
|
+
- What: The Docker image to run
|
|
171
|
+
|
|
172
|
+
Optional:
|
|
173
|
+
|
|
174
|
+
- ``network``:
|
|
175
|
+
- Type: str | None
|
|
176
|
+
- What: Docker network to attach to
|
|
177
|
+
- Default: None
|
|
178
|
+
|
|
179
|
+
- ``volumes``:
|
|
180
|
+
- Type: list[str] | None
|
|
181
|
+
- What: Volume mount specifications
|
|
182
|
+
- Default: None
|
|
183
|
+
|
|
184
|
+
- ``env_vars``:
|
|
185
|
+
- Type: dict[str, str] | None
|
|
186
|
+
- What: Environment variables to set
|
|
187
|
+
- Default: None
|
|
188
|
+
|
|
189
|
+
- ``extra_args``:
|
|
190
|
+
- Type: list[str] | None
|
|
191
|
+
- What: Additional docker run arguments
|
|
192
|
+
- Default: None
|
|
193
|
+
|
|
194
|
+
- ``command``:
|
|
195
|
+
- Type: list[str] | None
|
|
196
|
+
- What: Command to run in the container
|
|
197
|
+
- Default: None
|
|
198
|
+
"""
|
|
199
|
+
cmd = ["docker", "run", "-d"]
|
|
200
|
+
if extra_args:
|
|
201
|
+
cmd.extend(extra_args)
|
|
202
|
+
if network:
|
|
203
|
+
cmd.extend(["--network", network])
|
|
204
|
+
for vol in volumes or []:
|
|
205
|
+
cmd.extend(["--volume", vol])
|
|
206
|
+
for key, val in (env_vars or {}).items():
|
|
207
|
+
cmd.extend(["-e", f"{key}={val}"])
|
|
208
|
+
cmd.extend(["--name", name])
|
|
209
|
+
cmd.append(image)
|
|
210
|
+
if command:
|
|
211
|
+
cmd.extend(command)
|
|
212
|
+
run_and_log(cmd)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def run_interactive(
|
|
216
|
+
name: str,
|
|
217
|
+
image: str,
|
|
218
|
+
network: str | None = None,
|
|
219
|
+
ports: list[str] | None = None,
|
|
220
|
+
volumes: list[str] | None = None,
|
|
221
|
+
env_vars: dict[str, str] | None = None,
|
|
222
|
+
extra_args: list[str] | None = None,
|
|
223
|
+
command: list[str] | None = None,
|
|
224
|
+
) -> None:
|
|
225
|
+
"""
|
|
226
|
+
Usage:
|
|
227
|
+
|
|
228
|
+
- Runs a Docker container interactively with TTY passthrough
|
|
229
|
+
|
|
230
|
+
Requires:
|
|
231
|
+
|
|
232
|
+
- ``name``:
|
|
233
|
+
- Type: str
|
|
234
|
+
- What: The container name
|
|
235
|
+
|
|
236
|
+
- ``image``:
|
|
237
|
+
- Type: str
|
|
238
|
+
- What: The Docker image to run
|
|
239
|
+
|
|
240
|
+
Optional:
|
|
241
|
+
|
|
242
|
+
- ``network``:
|
|
243
|
+
- Type: str | None
|
|
244
|
+
- What: Docker network to attach to
|
|
245
|
+
- Default: None
|
|
246
|
+
|
|
247
|
+
- ``ports``:
|
|
248
|
+
- Type: list[str] | None
|
|
249
|
+
- What: Port mapping specifications
|
|
250
|
+
- Default: None
|
|
251
|
+
|
|
252
|
+
- ``volumes``:
|
|
253
|
+
- Type: list[str] | None
|
|
254
|
+
- What: Volume mount specifications
|
|
255
|
+
- Default: None
|
|
256
|
+
|
|
257
|
+
- ``env_vars``:
|
|
258
|
+
- Type: dict[str, str] | None
|
|
259
|
+
- What: Environment variables to set
|
|
260
|
+
- Default: None
|
|
261
|
+
|
|
262
|
+
- ``extra_args``:
|
|
263
|
+
- Type: list[str] | None
|
|
264
|
+
- What: Additional docker run arguments
|
|
265
|
+
- Default: None
|
|
266
|
+
|
|
267
|
+
- ``command``:
|
|
268
|
+
- Type: list[str] | None
|
|
269
|
+
- What: Command to run in the container
|
|
270
|
+
- Default: None
|
|
271
|
+
"""
|
|
272
|
+
cmd = ["docker", "run", "-it"]
|
|
273
|
+
if extra_args:
|
|
274
|
+
cmd.extend(extra_args)
|
|
275
|
+
for port in ports or []:
|
|
276
|
+
cmd.extend(["-p", port])
|
|
277
|
+
if network:
|
|
278
|
+
cmd.extend(["--network", network])
|
|
279
|
+
for vol in volumes or []:
|
|
280
|
+
cmd.extend(["--volume", vol])
|
|
281
|
+
for key, val in (env_vars or {}).items():
|
|
282
|
+
cmd.extend(["-e", f"{key}={val}"])
|
|
283
|
+
cmd.extend(["--name", name])
|
|
284
|
+
cmd.append(image)
|
|
285
|
+
if command:
|
|
286
|
+
cmd.extend(command)
|
|
287
|
+
run(cmd, inherit_io=True)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def save_redis(app_name: str) -> None:
|
|
291
|
+
"""
|
|
292
|
+
Usage:
|
|
293
|
+
|
|
294
|
+
- Persists Redis data before container termination
|
|
295
|
+
|
|
296
|
+
Requires:
|
|
297
|
+
|
|
298
|
+
- ``app_name``:
|
|
299
|
+
- Type: str
|
|
300
|
+
- What: The app name
|
|
301
|
+
"""
|
|
302
|
+
logger.debug(
|
|
303
|
+
"Persisting Redis Data prior to Redis Container Termination..."
|
|
304
|
+
)
|
|
305
|
+
run_and_log(
|
|
306
|
+
["docker", "exec", f"{app_name}_redis_host", "redis-cli", "save"]
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def remove_containers(app_name: str) -> None:
|
|
311
|
+
"""
|
|
312
|
+
Usage:
|
|
313
|
+
|
|
314
|
+
- Removes all Docker containers for a CAVE app
|
|
315
|
+
|
|
316
|
+
Requires:
|
|
317
|
+
|
|
318
|
+
- ``app_name``:
|
|
319
|
+
- Type: str
|
|
320
|
+
- What: The app name
|
|
321
|
+
"""
|
|
322
|
+
save_redis(app_name)
|
|
323
|
+
logger.debug(f"Killing Running App ({app_name})...")
|
|
324
|
+
containers = [
|
|
325
|
+
f"{app_name}_django",
|
|
326
|
+
f"{app_name}_nginx_host",
|
|
327
|
+
f"{app_name}_db_host",
|
|
328
|
+
f"{app_name}_redis_host",
|
|
329
|
+
]
|
|
330
|
+
run_and_log(["docker", "rm", "--force"] + containers)
|
|
331
|
+
remove_network(app_name)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def remove_volume(app_name: str, suffix: str = "pg_volume") -> None:
|
|
335
|
+
"""
|
|
336
|
+
Usage:
|
|
337
|
+
|
|
338
|
+
- Removes a Docker volume for a CAVE app
|
|
339
|
+
|
|
340
|
+
Requires:
|
|
341
|
+
|
|
342
|
+
- ``app_name``:
|
|
343
|
+
- Type: str
|
|
344
|
+
- What: The app name
|
|
345
|
+
|
|
346
|
+
Optional:
|
|
347
|
+
|
|
348
|
+
- ``suffix``:
|
|
349
|
+
- Type: str
|
|
350
|
+
- What: The volume name suffix
|
|
351
|
+
- Default: "pg_volume"
|
|
352
|
+
"""
|
|
353
|
+
logger.info(f"Removing Docker DB Volume for App ({app_name})...")
|
|
354
|
+
run_and_log(["docker", "volume", "rm", f"{app_name}_{suffix}"])
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def remove_image(app_name: str) -> None:
|
|
358
|
+
"""
|
|
359
|
+
Usage:
|
|
360
|
+
|
|
361
|
+
- Removes the Docker image for a CAVE app
|
|
362
|
+
|
|
363
|
+
Requires:
|
|
364
|
+
|
|
365
|
+
- ``app_name``:
|
|
366
|
+
- Type: str
|
|
367
|
+
- What: The app name
|
|
368
|
+
"""
|
|
369
|
+
logger.info(f"Removing Docker Images for App ({app_name})...")
|
|
370
|
+
run_and_log(["docker", "rmi", f"cave-app:{app_name}"])
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def get_running_apps() -> list[str]:
|
|
374
|
+
"""
|
|
375
|
+
Usage:
|
|
376
|
+
|
|
377
|
+
- Lists all running CAVE app names
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
|
|
381
|
+
- ``apps``:
|
|
382
|
+
- Type: list[str]
|
|
383
|
+
- What: A list of app names derived from container names
|
|
384
|
+
"""
|
|
385
|
+
result = run(
|
|
386
|
+
["docker", "ps", "-a", "--format", "{{.Names}}"]
|
|
387
|
+
)
|
|
388
|
+
apps: list[str] = []
|
|
389
|
+
if result.returncode != 0 or not result.stdout:
|
|
390
|
+
return apps
|
|
391
|
+
for line in result.stdout.strip().splitlines():
|
|
392
|
+
if line.endswith("_django"):
|
|
393
|
+
apps.append(line.replace("_django", ""))
|
|
394
|
+
return apps
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def get_container_env(container: str, var: str) -> str:
|
|
398
|
+
"""
|
|
399
|
+
Usage:
|
|
400
|
+
|
|
401
|
+
- Gets an environment variable value from a running container
|
|
402
|
+
|
|
403
|
+
Requires:
|
|
404
|
+
|
|
405
|
+
- ``container``:
|
|
406
|
+
- Type: str
|
|
407
|
+
- What: The container name
|
|
408
|
+
|
|
409
|
+
- ``var``:
|
|
410
|
+
- Type: str
|
|
411
|
+
- What: The environment variable name
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
|
|
415
|
+
- ``value``:
|
|
416
|
+
- Type: str
|
|
417
|
+
- What: The variable value, or empty string if not found
|
|
418
|
+
"""
|
|
419
|
+
import re
|
|
420
|
+
|
|
421
|
+
result = run(
|
|
422
|
+
[
|
|
423
|
+
"docker",
|
|
424
|
+
"inspect",
|
|
425
|
+
"-f",
|
|
426
|
+
"{{.Config.Env}}",
|
|
427
|
+
container,
|
|
428
|
+
]
|
|
429
|
+
)
|
|
430
|
+
if result.returncode != 0 or not result.stdout:
|
|
431
|
+
return ""
|
|
432
|
+
match = re.search(rf"{var}=([^\s\]]*)", result.stdout)
|
|
433
|
+
return match.group(1) if match else ""
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def get_container_host_port(container: str) -> str:
|
|
437
|
+
"""
|
|
438
|
+
Usage:
|
|
439
|
+
|
|
440
|
+
- Gets the host port mapping for a container's port 8000
|
|
441
|
+
|
|
442
|
+
Requires:
|
|
443
|
+
|
|
444
|
+
- ``container``:
|
|
445
|
+
- Type: str
|
|
446
|
+
- What: The container name
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
|
|
450
|
+
- ``port``:
|
|
451
|
+
- Type: str
|
|
452
|
+
- What: The host port, or empty string if not found
|
|
453
|
+
"""
|
|
454
|
+
result = run(
|
|
455
|
+
[
|
|
456
|
+
"docker",
|
|
457
|
+
"inspect",
|
|
458
|
+
"-f",
|
|
459
|
+
"{{(index (index .NetworkSettings.Ports \"8000/tcp\") 0).HostPort}}",
|
|
460
|
+
container,
|
|
461
|
+
]
|
|
462
|
+
)
|
|
463
|
+
if result.returncode != 0 or not result.stdout:
|
|
464
|
+
return ""
|
|
465
|
+
return result.stdout.strip()
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def get_all_containers(pattern: str) -> list[str]:
|
|
469
|
+
"""
|
|
470
|
+
Usage:
|
|
471
|
+
|
|
472
|
+
- Lists all container names matching a pattern suffix
|
|
473
|
+
|
|
474
|
+
Requires:
|
|
475
|
+
|
|
476
|
+
- ``pattern``:
|
|
477
|
+
- Type: str
|
|
478
|
+
- What: The suffix pattern to match (e.g. "_django")
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
|
|
482
|
+
- ``containers``:
|
|
483
|
+
- Type: list[str]
|
|
484
|
+
- What: A list of matching container names
|
|
485
|
+
"""
|
|
486
|
+
result = run(
|
|
487
|
+
["docker", "ps", "-a", "--format", "{{.Names}}"]
|
|
488
|
+
)
|
|
489
|
+
containers: list[str] = []
|
|
490
|
+
if result.returncode != 0 or not result.stdout:
|
|
491
|
+
return containers
|
|
492
|
+
for line in result.stdout.strip().splitlines():
|
|
493
|
+
if line.endswith(pattern):
|
|
494
|
+
containers.append(line)
|
|
495
|
+
return containers
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def generate_secret_key(app_name: str) -> str | None:
|
|
499
|
+
"""
|
|
500
|
+
Usage:
|
|
501
|
+
|
|
502
|
+
- Generates a Django secret key using the app's Docker image
|
|
503
|
+
|
|
504
|
+
Requires:
|
|
505
|
+
|
|
506
|
+
- ``app_name``:
|
|
507
|
+
- Type: str
|
|
508
|
+
- What: The app name (used as the image tag)
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
|
|
512
|
+
- ``key``:
|
|
513
|
+
- Type: str | None
|
|
514
|
+
- What: The generated secret key, or None if generation failed
|
|
515
|
+
"""
|
|
516
|
+
result = run(
|
|
517
|
+
[
|
|
518
|
+
"docker",
|
|
519
|
+
"run",
|
|
520
|
+
"--rm",
|
|
521
|
+
f"cave-app:{app_name}",
|
|
522
|
+
"python",
|
|
523
|
+
"-c",
|
|
524
|
+
"from django.core.management.utils import get_random_secret_key; "
|
|
525
|
+
"print(get_random_secret_key())",
|
|
526
|
+
]
|
|
527
|
+
)
|
|
528
|
+
if result.returncode != 0 or not result.stdout:
|
|
529
|
+
return None
|
|
530
|
+
return result.stdout.strip()
|