mantis-cli 22.2.0__tar.gz → 22.4.0__tar.gz
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.
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/PKG-INFO +39 -7
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/README.md +38 -6
- mantis_cli-22.4.0/mantis/__init__.py +1 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/commands/containers.py +6 -6
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/managers.py +208 -13
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis_cli.egg-info/PKG-INFO +39 -7
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/tests/test_managers.py +144 -0
- mantis_cli-22.2.0/mantis/__init__.py +0 -1
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/LICENSE +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/MANIFEST.in +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/__main__.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/app.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/command_line.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/commands/__init__.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/commands/compose.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/commands/configuration.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/commands/connection.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/commands/core.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/commands/django.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/commands/images.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/commands/nginx.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/commands/postgres.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/commands/secrets.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/commands/services.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/commands/volumes.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/config.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/cryptography.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/environment.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/extensions/__init__.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/extensions/django.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/extensions/nginx.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/extensions/postgres.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/helpers.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/mantis.tpl +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis/schema.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis_cli.egg-info/SOURCES.txt +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis_cli.egg-info/dependency_links.txt +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis_cli.egg-info/entry_points.txt +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis_cli.egg-info/requires.txt +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/mantis_cli.egg-info/top_level.txt +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/setup.cfg +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/setup.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/tests/__init__.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/tests/test_build_config.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/tests/test_command_line.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/tests/test_config.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/tests/test_helpers.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.4.0}/tests/test_tunnel.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mantis_cli
|
|
3
|
-
Version: 22.
|
|
3
|
+
Version: 22.4.0
|
|
4
4
|
Summary: Management command to build and deploy webapps, especially based on Django
|
|
5
5
|
Home-page: https://github.com/PragmaticMates/mantis-cli
|
|
6
6
|
Author: Erik Telepovský
|
|
@@ -325,18 +325,18 @@ Run `mantis --help` to see all available commands with their descriptions.
|
|
|
325
325
|
|
|
326
326
|
| Command / Shortcut | Description |
|
|
327
327
|
|---------------------------------------|-----------------------------------------------------------|
|
|
328
|
-
| logs [CONTAINER] / l
|
|
328
|
+
| logs [CONTAINER\|SERVICE] / l | Prints logs of containers |
|
|
329
329
|
| networks / n | Prints docker networks |
|
|
330
|
-
| healthcheck [CONTAINER] / hc
|
|
330
|
+
| healthcheck [CONTAINER\|SERVICE] / hc | Execute health-check of container |
|
|
331
331
|
| stop [CONTAINERS...] | Stops containers |
|
|
332
332
|
| start [CONTAINERS...] | Starts containers |
|
|
333
333
|
| kill [CONTAINERS...] | Kills containers |
|
|
334
334
|
| remove [CONTAINERS...] [--force] | Removes containers |
|
|
335
335
|
| rename CONTAINER NEW_NAME | Rename container |
|
|
336
|
-
| bash CONTAINER
|
|
337
|
-
| sh CONTAINER
|
|
338
|
-
| exec CONTAINER COMMAND...
|
|
339
|
-
| exec-it CONTAINER COMMAND...
|
|
336
|
+
| bash CONTAINER\|SERVICE | Runs bash in container |
|
|
337
|
+
| sh CONTAINER\|SERVICE | Runs sh in container |
|
|
338
|
+
| exec CONTAINER\|SERVICE COMMAND... | Executes command in container |
|
|
339
|
+
| exec-it CONTAINER\|SERVICE COMMAND... | Executes command in container (interactive) |
|
|
340
340
|
| get-container-name SERVICE | Gets container name for service |
|
|
341
341
|
| remove-suffixes [PREFIX] | Removes numerical suffixes from container names |
|
|
342
342
|
|
|
@@ -520,6 +520,38 @@ If you need to follow logs of a specific container, you can do it by passing con
|
|
|
520
520
|
mantis -e <ENVIRONMENT> logs <container-name>
|
|
521
521
|
```
|
|
522
522
|
|
|
523
|
+
#### Container or service name
|
|
524
|
+
|
|
525
|
+
Commands taking a container (`logs`, `healthcheck`, `bash`, `sh`, `exec`, `exec-it`) also accept a
|
|
526
|
+
plain service name and mantis finds its container(s) for you, including scaled ones with numerical
|
|
527
|
+
suffixes and services declaring their own `container_name`:
|
|
528
|
+
|
|
529
|
+
```bash
|
|
530
|
+
mantis -e production logs app # reads logs of <project>-app container
|
|
531
|
+
mantis -e production bash app # same container, no need to type the project prefix
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
A name which is not a service is still matched against container names without their project
|
|
535
|
+
prefix, which covers services numbered in the compose file:
|
|
536
|
+
|
|
537
|
+
```bash
|
|
538
|
+
mantis -e production logs htmltopdf # reads <project>-htmltopdf-1 and <project>-htmltopdf-2
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
An exact container name always takes precedence: if a container literally named `app` exists,
|
|
542
|
+
`logs app` reads that one instead of `<project>-app`. Names matching no container at all are
|
|
543
|
+
passed to docker as they are.
|
|
544
|
+
|
|
545
|
+
`logs` follows all matching containers at once, prefixing every line with a container name:
|
|
546
|
+
|
|
547
|
+
```
|
|
548
|
+
itfitness-htmltopdf-1 | POST /generate-pdf 200 3355.969 ms - 837045
|
|
549
|
+
itfitness-htmltopdf-2 | POST /generate-pdf 200 889.662 ms - 747746
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
Commands which can only operate on one container (`bash`, `sh`, `exec`, `exec-it`, `healthcheck`)
|
|
553
|
+
use the first match and warn about the rest.
|
|
554
|
+
|
|
523
555
|
### 5. Another useful commands
|
|
524
556
|
|
|
525
557
|
Sometimes, instead of calling whole deployment process, you just need to call compose commands directly:
|
|
@@ -303,18 +303,18 @@ Run `mantis --help` to see all available commands with their descriptions.
|
|
|
303
303
|
|
|
304
304
|
| Command / Shortcut | Description |
|
|
305
305
|
|---------------------------------------|-----------------------------------------------------------|
|
|
306
|
-
| logs [CONTAINER] / l
|
|
306
|
+
| logs [CONTAINER\|SERVICE] / l | Prints logs of containers |
|
|
307
307
|
| networks / n | Prints docker networks |
|
|
308
|
-
| healthcheck [CONTAINER] / hc
|
|
308
|
+
| healthcheck [CONTAINER\|SERVICE] / hc | Execute health-check of container |
|
|
309
309
|
| stop [CONTAINERS...] | Stops containers |
|
|
310
310
|
| start [CONTAINERS...] | Starts containers |
|
|
311
311
|
| kill [CONTAINERS...] | Kills containers |
|
|
312
312
|
| remove [CONTAINERS...] [--force] | Removes containers |
|
|
313
313
|
| rename CONTAINER NEW_NAME | Rename container |
|
|
314
|
-
| bash CONTAINER
|
|
315
|
-
| sh CONTAINER
|
|
316
|
-
| exec CONTAINER COMMAND...
|
|
317
|
-
| exec-it CONTAINER COMMAND...
|
|
314
|
+
| bash CONTAINER\|SERVICE | Runs bash in container |
|
|
315
|
+
| sh CONTAINER\|SERVICE | Runs sh in container |
|
|
316
|
+
| exec CONTAINER\|SERVICE COMMAND... | Executes command in container |
|
|
317
|
+
| exec-it CONTAINER\|SERVICE COMMAND... | Executes command in container (interactive) |
|
|
318
318
|
| get-container-name SERVICE | Gets container name for service |
|
|
319
319
|
| remove-suffixes [PREFIX] | Removes numerical suffixes from container names |
|
|
320
320
|
|
|
@@ -498,6 +498,38 @@ If you need to follow logs of a specific container, you can do it by passing con
|
|
|
498
498
|
mantis -e <ENVIRONMENT> logs <container-name>
|
|
499
499
|
```
|
|
500
500
|
|
|
501
|
+
#### Container or service name
|
|
502
|
+
|
|
503
|
+
Commands taking a container (`logs`, `healthcheck`, `bash`, `sh`, `exec`, `exec-it`) also accept a
|
|
504
|
+
plain service name and mantis finds its container(s) for you, including scaled ones with numerical
|
|
505
|
+
suffixes and services declaring their own `container_name`:
|
|
506
|
+
|
|
507
|
+
```bash
|
|
508
|
+
mantis -e production logs app # reads logs of <project>-app container
|
|
509
|
+
mantis -e production bash app # same container, no need to type the project prefix
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
A name which is not a service is still matched against container names without their project
|
|
513
|
+
prefix, which covers services numbered in the compose file:
|
|
514
|
+
|
|
515
|
+
```bash
|
|
516
|
+
mantis -e production logs htmltopdf # reads <project>-htmltopdf-1 and <project>-htmltopdf-2
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
An exact container name always takes precedence: if a container literally named `app` exists,
|
|
520
|
+
`logs app` reads that one instead of `<project>-app`. Names matching no container at all are
|
|
521
|
+
passed to docker as they are.
|
|
522
|
+
|
|
523
|
+
`logs` follows all matching containers at once, prefixing every line with a container name:
|
|
524
|
+
|
|
525
|
+
```
|
|
526
|
+
itfitness-htmltopdf-1 | POST /generate-pdf 200 3355.969 ms - 837045
|
|
527
|
+
itfitness-htmltopdf-2 | POST /generate-pdf 200 889.662 ms - 747746
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
Commands which can only operate on one container (`bash`, `sh`, `exec`, `exec-it`, `healthcheck`)
|
|
531
|
+
use the first match and warn about the rest.
|
|
532
|
+
|
|
501
533
|
### 5. Another useful commands
|
|
502
534
|
|
|
503
535
|
Sometimes, instead of calling whole deployment process, you just need to call compose commands directly:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = '22.4.0'
|
|
@@ -8,7 +8,7 @@ from mantis.app import command, state
|
|
|
8
8
|
|
|
9
9
|
@command(shortcut="l", panel="Containers")
|
|
10
10
|
def logs(
|
|
11
|
-
container: Optional[str] = typer.Argument(None, help="Container name"),
|
|
11
|
+
container: Optional[str] = typer.Argument(None, help="Container or service name"),
|
|
12
12
|
):
|
|
13
13
|
"""Prints logs of containers"""
|
|
14
14
|
state.logs(container)
|
|
@@ -22,7 +22,7 @@ def networks():
|
|
|
22
22
|
|
|
23
23
|
@command(shortcut="hc", panel="Containers")
|
|
24
24
|
def healthcheck(
|
|
25
|
-
container: Optional[str] = typer.Argument(None, help="Container name"),
|
|
25
|
+
container: Optional[str] = typer.Argument(None, help="Container or service name"),
|
|
26
26
|
):
|
|
27
27
|
"""Execute health-check of container"""
|
|
28
28
|
state.healthcheck(container)
|
|
@@ -72,7 +72,7 @@ def rename(
|
|
|
72
72
|
|
|
73
73
|
@command(panel="Containers")
|
|
74
74
|
def bash(
|
|
75
|
-
container: str = typer.Argument(..., help="Container name"),
|
|
75
|
+
container: str = typer.Argument(..., help="Container or service name"),
|
|
76
76
|
):
|
|
77
77
|
"""Runs bash in container"""
|
|
78
78
|
state.bash(container)
|
|
@@ -80,7 +80,7 @@ def bash(
|
|
|
80
80
|
|
|
81
81
|
@command(panel="Containers")
|
|
82
82
|
def sh(
|
|
83
|
-
container: str = typer.Argument(..., help="Container name"),
|
|
83
|
+
container: str = typer.Argument(..., help="Container or service name"),
|
|
84
84
|
):
|
|
85
85
|
"""Runs sh in container"""
|
|
86
86
|
state.sh(container)
|
|
@@ -88,7 +88,7 @@ def sh(
|
|
|
88
88
|
|
|
89
89
|
@command(name="exec", panel="Containers")
|
|
90
90
|
def exec_cmd(
|
|
91
|
-
container: str = typer.Argument(..., help="Container name"),
|
|
91
|
+
container: str = typer.Argument(..., help="Container or service name"),
|
|
92
92
|
cmd: List[str] = typer.Argument(..., help="Command to execute"),
|
|
93
93
|
):
|
|
94
94
|
"""Executes command in container"""
|
|
@@ -97,7 +97,7 @@ def exec_cmd(
|
|
|
97
97
|
|
|
98
98
|
@command(name="exec-it", panel="Containers")
|
|
99
99
|
def exec_it(
|
|
100
|
-
container: str = typer.Argument(..., help="Container name"),
|
|
100
|
+
container: str = typer.Argument(..., help="Container or service name"),
|
|
101
101
|
cmd: List[str] = typer.Argument(..., help="Command to execute"),
|
|
102
102
|
):
|
|
103
103
|
"""Executes command in container (interactive)"""
|
|
@@ -2,7 +2,9 @@ import asyncio
|
|
|
2
2
|
import atexit
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import re
|
|
5
6
|
import shutil
|
|
7
|
+
import signal
|
|
6
8
|
import subprocess
|
|
7
9
|
import sys
|
|
8
10
|
import tempfile
|
|
@@ -519,10 +521,23 @@ class AbstractManager(object):
|
|
|
519
521
|
except OSError as e:
|
|
520
522
|
CLI.error(f"{error_message}: {e}")
|
|
521
523
|
|
|
522
|
-
|
|
524
|
+
@property
|
|
525
|
+
def quiet_flag(self) -> str:
|
|
526
|
+
"""
|
|
527
|
+
Layer progress is meant to be repainted in place, which needs a terminal. Redirected
|
|
528
|
+
to a log file or a CI pipe, docker prints every progress event on its own line
|
|
529
|
+
instead -- hundreds per image -- so ask for no progress at all when there is nobody
|
|
530
|
+
to watch it move. Errors and the final result are not progress and still print.
|
|
531
|
+
"""
|
|
532
|
+
return '' if sys.stdout.isatty() else '--quiet'
|
|
533
|
+
|
|
534
|
+
def build_docker_command(self, command: str, use_connection: bool = True) -> str:
|
|
523
535
|
docker_connection = self.docker_connection if use_connection else ''
|
|
524
536
|
|
|
525
|
-
|
|
537
|
+
return f'{docker_connection} {command}'.strip()
|
|
538
|
+
|
|
539
|
+
def docker_command(self, command: str, return_output: bool = False, use_connection: bool = True) -> Optional[str]:
|
|
540
|
+
cmd = self.build_docker_command(command, use_connection)
|
|
526
541
|
|
|
527
542
|
if return_output:
|
|
528
543
|
if self.dry_run:
|
|
@@ -889,6 +904,97 @@ class BaseManager(AbstractManager):
|
|
|
889
904
|
prefix = self.get_project_by_service(service)
|
|
890
905
|
return f'{prefix}{suffix}'.replace('_', '-')
|
|
891
906
|
|
|
907
|
+
def resolve_containers(self, names: List[str], project_containers: Optional[List[str]] = None) -> List[str]:
|
|
908
|
+
"""
|
|
909
|
+
Resolves given names to project container names.
|
|
910
|
+
Exact container name has the highest priority, service name is used as a fallback,
|
|
911
|
+
i.e. "app" resolves to "<project>-app" (or its scaled containers "<project>-app-1", ...)
|
|
912
|
+
unless a container literally named "app" exists.
|
|
913
|
+
"""
|
|
914
|
+
if project_containers is None:
|
|
915
|
+
project_containers = self.get_containers()
|
|
916
|
+
|
|
917
|
+
services = self.services()
|
|
918
|
+
resolved = []
|
|
919
|
+
|
|
920
|
+
for name in names:
|
|
921
|
+
# exact container name has the highest priority
|
|
922
|
+
if name in project_containers:
|
|
923
|
+
containers = [name]
|
|
924
|
+
else:
|
|
925
|
+
# service name, or a common name of numbered containers
|
|
926
|
+
containers = (self.get_containers_by_service(name, project_containers) if name in services else []) \
|
|
927
|
+
or self.get_containers_by_name(name, project_containers)
|
|
928
|
+
|
|
929
|
+
if containers:
|
|
930
|
+
CLI.info(f'{name} resolved to: {", ".join(containers)}')
|
|
931
|
+
else:
|
|
932
|
+
# unknown name: keep it as given and let docker report it
|
|
933
|
+
containers = [name]
|
|
934
|
+
|
|
935
|
+
# keep the order and avoid duplicates
|
|
936
|
+
resolved.extend(container for container in containers if container not in resolved)
|
|
937
|
+
|
|
938
|
+
return resolved
|
|
939
|
+
|
|
940
|
+
def resolve_container(self, name: str, project_containers: Optional[List[str]] = None) -> str:
|
|
941
|
+
"""
|
|
942
|
+
Resolves given name to a single project container name (see resolve_containers).
|
|
943
|
+
Used by commands which can operate on one container only.
|
|
944
|
+
"""
|
|
945
|
+
containers = self.resolve_containers([name], project_containers)
|
|
946
|
+
|
|
947
|
+
if len(containers) > 1:
|
|
948
|
+
CLI.warning(f'{name} matches {len(containers)} containers, using {containers[0]}')
|
|
949
|
+
|
|
950
|
+
return containers[0]
|
|
951
|
+
|
|
952
|
+
def get_containers_by_service(self, service: str, project_containers: Optional[List[str]] = None) -> List[str]:
|
|
953
|
+
"""
|
|
954
|
+
Returns names of existing containers belonging to given service,
|
|
955
|
+
including scaled ones with numerical suffixes
|
|
956
|
+
"""
|
|
957
|
+
if project_containers is None:
|
|
958
|
+
project_containers = self.get_containers()
|
|
959
|
+
|
|
960
|
+
# service can define its container name explicitly
|
|
961
|
+
defined_container_name = self.compose_config.get('services', {}).get(service, {}).get('container_name', None)
|
|
962
|
+
|
|
963
|
+
if defined_container_name and defined_container_name in project_containers:
|
|
964
|
+
return [defined_container_name]
|
|
965
|
+
|
|
966
|
+
return self.match_containers(self.get_container_name(service), project_containers)
|
|
967
|
+
|
|
968
|
+
def get_containers_by_name(self, name: str, project_containers: Optional[List[str]] = None) -> List[str]:
|
|
969
|
+
"""
|
|
970
|
+
Returns names of existing project containers matching given name prefixed with a project name.
|
|
971
|
+
Covers names which are not declared services, i.e. "htmltopdf" for services "htmltopdf-1"
|
|
972
|
+
and "htmltopdf-2" running as containers "<project>-htmltopdf-1" and "<project>-htmltopdf-2"
|
|
973
|
+
"""
|
|
974
|
+
if project_containers is None:
|
|
975
|
+
project_containers = self.get_containers()
|
|
976
|
+
|
|
977
|
+
containers = []
|
|
978
|
+
|
|
979
|
+
for project in self.project_services().keys():
|
|
980
|
+
container_name = f'{project}{self.get_container_suffix(name)}'.replace('_', '-')
|
|
981
|
+
|
|
982
|
+
containers.extend(
|
|
983
|
+
container
|
|
984
|
+
for container in self.match_containers(container_name, project_containers)
|
|
985
|
+
if container not in containers
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
return containers
|
|
989
|
+
|
|
990
|
+
def match_containers(self, container_name: str, project_containers: List[str]) -> List[str]:
|
|
991
|
+
"""
|
|
992
|
+
Returns containers named exactly as given container name or its numbered variants
|
|
993
|
+
"""
|
|
994
|
+
pattern = re.compile(rf'^{re.escape(container_name)}(-\d+)?$')
|
|
995
|
+
|
|
996
|
+
return [container for container in project_containers if pattern.match(container)]
|
|
997
|
+
|
|
892
998
|
def get_service_containers(self, service: str) -> List[str]:
|
|
893
999
|
"""
|
|
894
1000
|
Prints container names of given service
|
|
@@ -954,7 +1060,10 @@ class BaseManager(AbstractManager):
|
|
|
954
1060
|
"""
|
|
955
1061
|
Execute health-check of given project container
|
|
956
1062
|
"""
|
|
957
|
-
|
|
1063
|
+
project_containers = self.get_containers()
|
|
1064
|
+
container = self.resolve_container(container, project_containers)
|
|
1065
|
+
|
|
1066
|
+
if container not in project_containers:
|
|
958
1067
|
CLI.error(f"Container {container} not found")
|
|
959
1068
|
|
|
960
1069
|
console = Console()
|
|
@@ -1130,7 +1239,7 @@ class BaseManager(AbstractManager):
|
|
|
1130
1239
|
CLI.info(f'Services = {params}')
|
|
1131
1240
|
|
|
1132
1241
|
# Push using docker compose
|
|
1133
|
-
self.docker_compose(f'push {params}', use_connection=False)
|
|
1242
|
+
self.docker_compose(f'push {self.quiet_flag} {params}', use_connection=False)
|
|
1134
1243
|
|
|
1135
1244
|
def pull(self, services: Optional[List[str]] = None) -> None:
|
|
1136
1245
|
"""
|
|
@@ -1141,7 +1250,7 @@ class BaseManager(AbstractManager):
|
|
|
1141
1250
|
CLI.info(f'Services = {params}')
|
|
1142
1251
|
|
|
1143
1252
|
# Pull using docker compose
|
|
1144
|
-
self.docker_compose(f'pull {params}')
|
|
1253
|
+
self.docker_compose(f'pull {self.quiet_flag} {params}')
|
|
1145
1254
|
|
|
1146
1255
|
def upload(self) -> None:
|
|
1147
1256
|
"""
|
|
@@ -1746,28 +1855,112 @@ class BaseManager(AbstractManager):
|
|
|
1746
1855
|
"""
|
|
1747
1856
|
CLI.info('Reading logs...')
|
|
1748
1857
|
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1858
|
+
if not params:
|
|
1859
|
+
containers = self.get_containers()
|
|
1860
|
+
steps = len(containers)
|
|
1752
1861
|
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1862
|
+
for index, container in enumerate(containers):
|
|
1863
|
+
CLI.step(index + 1, steps, f'{container} logs')
|
|
1864
|
+
self.docker(f'logs {container} --tail 10')
|
|
1865
|
+
|
|
1866
|
+
return
|
|
1867
|
+
|
|
1868
|
+
containers = self.resolve_containers(params.split())
|
|
1869
|
+
|
|
1870
|
+
# following a single container does not need any output multiplexing
|
|
1871
|
+
if len(containers) == 1:
|
|
1872
|
+
self.docker(f'logs {containers[0]} --tail 1000 -f')
|
|
1873
|
+
return
|
|
1874
|
+
|
|
1875
|
+
self.follow_logs(containers)
|
|
1876
|
+
|
|
1877
|
+
def follow_logs(self, containers: List[str], tail: int = 1000) -> None:
|
|
1878
|
+
"""
|
|
1879
|
+
Follows logs of multiple containers at once, prefixing every line with a container name.
|
|
1880
|
+
Following them one by one is not an option as the first one would never finish.
|
|
1881
|
+
"""
|
|
1882
|
+
commands = {
|
|
1883
|
+
container: self.build_docker_command(f'docker logs {container} --tail {tail} -f')
|
|
1884
|
+
for container in containers
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
if self.dry_run:
|
|
1888
|
+
for command in commands.values():
|
|
1889
|
+
CLI.warning(f'[DRY-RUN] {command}')
|
|
1890
|
+
return
|
|
1891
|
+
|
|
1892
|
+
processes = {}
|
|
1893
|
+
|
|
1894
|
+
for container, command in commands.items():
|
|
1895
|
+
print(command)
|
|
1896
|
+
processes[container] = subprocess.Popen(
|
|
1897
|
+
command,
|
|
1898
|
+
shell=True,
|
|
1899
|
+
stdout=subprocess.PIPE,
|
|
1900
|
+
stderr=subprocess.STDOUT,
|
|
1901
|
+
text=True,
|
|
1902
|
+
bufsize=1,
|
|
1903
|
+
# own process group, so Ctrl+C is handled here and kills the whole shell + docker tree
|
|
1904
|
+
start_new_session=True
|
|
1905
|
+
)
|
|
1906
|
+
|
|
1907
|
+
width = max(map(len, processes.keys()))
|
|
1908
|
+
|
|
1909
|
+
def stream(container: str, process: subprocess.Popen) -> None:
|
|
1910
|
+
for line in process.stdout:
|
|
1911
|
+
print(f'{container:<{width}} | {line}', end='', flush=True)
|
|
1912
|
+
|
|
1913
|
+
threads = [
|
|
1914
|
+
threading.Thread(target=stream, args=(container, process), daemon=True)
|
|
1915
|
+
for container, process in processes.items()
|
|
1916
|
+
]
|
|
1917
|
+
|
|
1918
|
+
for thread in threads:
|
|
1919
|
+
thread.start()
|
|
1920
|
+
|
|
1921
|
+
try:
|
|
1922
|
+
# wait for the processes rather than for the reader threads, so that Ctrl+C
|
|
1923
|
+
# interrupts a plain sleep in the main thread and cleanup always runs
|
|
1924
|
+
while any(process.poll() is None for process in processes.values()):
|
|
1925
|
+
sleep(0.2)
|
|
1926
|
+
except KeyboardInterrupt:
|
|
1927
|
+
pass
|
|
1928
|
+
finally:
|
|
1929
|
+
for process in processes.values():
|
|
1930
|
+
self.terminate_process_group(process)
|
|
1931
|
+
|
|
1932
|
+
for thread in threads:
|
|
1933
|
+
thread.join(timeout=1)
|
|
1934
|
+
|
|
1935
|
+
@staticmethod
|
|
1936
|
+
def terminate_process_group(process: subprocess.Popen) -> None:
|
|
1937
|
+
"""
|
|
1938
|
+
Terminates given process together with its children, i.e. the shell running a docker command
|
|
1939
|
+
"""
|
|
1940
|
+
if process.poll() is not None:
|
|
1941
|
+
return
|
|
1942
|
+
|
|
1943
|
+
try:
|
|
1944
|
+
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
|
|
1945
|
+
except (ProcessLookupError, PermissionError):
|
|
1946
|
+
process.terminate()
|
|
1756
1947
|
|
|
1757
1948
|
def bash(self, params: str) -> None:
|
|
1758
1949
|
"""
|
|
1759
1950
|
Runs bash in container
|
|
1760
1951
|
"""
|
|
1952
|
+
container = self.resolve_container(params)
|
|
1761
1953
|
CLI.info('Running bash...')
|
|
1762
|
-
self.docker(f'exec -it --user root {
|
|
1954
|
+
self.docker(f'exec -it --user root {container} /bin/bash')
|
|
1763
1955
|
# self.docker_compose(f'run --entrypoint /bin/bash {container}')
|
|
1764
1956
|
|
|
1765
1957
|
def sh(self, params: str) -> None:
|
|
1766
1958
|
"""
|
|
1767
1959
|
Runs sh in container
|
|
1768
1960
|
"""
|
|
1961
|
+
container = self.resolve_container(params)
|
|
1769
1962
|
CLI.info('Logging to container...')
|
|
1770
|
-
self.docker(f'exec -it --user root {
|
|
1963
|
+
self.docker(f'exec -it --user root {container} /bin/sh')
|
|
1771
1964
|
|
|
1772
1965
|
def ssh(self) -> None:
|
|
1773
1966
|
if not self.connection:
|
|
@@ -1783,6 +1976,7 @@ class BaseManager(AbstractManager):
|
|
|
1783
1976
|
"""
|
|
1784
1977
|
Executes command in container
|
|
1785
1978
|
"""
|
|
1979
|
+
container = self.resolve_container(container)
|
|
1786
1980
|
command = ' '.join(cmd)
|
|
1787
1981
|
CLI.info(f'Executing command "{command}" in container {container}...')
|
|
1788
1982
|
self.docker(f'exec {container} {command}')
|
|
@@ -1791,6 +1985,7 @@ class BaseManager(AbstractManager):
|
|
|
1791
1985
|
"""
|
|
1792
1986
|
Executes command in container using interactive pseudo-TTY
|
|
1793
1987
|
"""
|
|
1988
|
+
container = self.resolve_container(container)
|
|
1794
1989
|
command = ' '.join(cmd)
|
|
1795
1990
|
CLI.info(f'Executing command "{command}" in container {container}...')
|
|
1796
1991
|
self.docker(f'exec -it {container} {command}')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mantis-cli
|
|
3
|
-
Version: 22.
|
|
3
|
+
Version: 22.4.0
|
|
4
4
|
Summary: Management command to build and deploy webapps, especially based on Django
|
|
5
5
|
Home-page: https://github.com/PragmaticMates/mantis-cli
|
|
6
6
|
Author: Erik Telepovský
|
|
@@ -325,18 +325,18 @@ Run `mantis --help` to see all available commands with their descriptions.
|
|
|
325
325
|
|
|
326
326
|
| Command / Shortcut | Description |
|
|
327
327
|
|---------------------------------------|-----------------------------------------------------------|
|
|
328
|
-
| logs [CONTAINER] / l
|
|
328
|
+
| logs [CONTAINER\|SERVICE] / l | Prints logs of containers |
|
|
329
329
|
| networks / n | Prints docker networks |
|
|
330
|
-
| healthcheck [CONTAINER] / hc
|
|
330
|
+
| healthcheck [CONTAINER\|SERVICE] / hc | Execute health-check of container |
|
|
331
331
|
| stop [CONTAINERS...] | Stops containers |
|
|
332
332
|
| start [CONTAINERS...] | Starts containers |
|
|
333
333
|
| kill [CONTAINERS...] | Kills containers |
|
|
334
334
|
| remove [CONTAINERS...] [--force] | Removes containers |
|
|
335
335
|
| rename CONTAINER NEW_NAME | Rename container |
|
|
336
|
-
| bash CONTAINER
|
|
337
|
-
| sh CONTAINER
|
|
338
|
-
| exec CONTAINER COMMAND...
|
|
339
|
-
| exec-it CONTAINER COMMAND...
|
|
336
|
+
| bash CONTAINER\|SERVICE | Runs bash in container |
|
|
337
|
+
| sh CONTAINER\|SERVICE | Runs sh in container |
|
|
338
|
+
| exec CONTAINER\|SERVICE COMMAND... | Executes command in container |
|
|
339
|
+
| exec-it CONTAINER\|SERVICE COMMAND... | Executes command in container (interactive) |
|
|
340
340
|
| get-container-name SERVICE | Gets container name for service |
|
|
341
341
|
| remove-suffixes [PREFIX] | Removes numerical suffixes from container names |
|
|
342
342
|
|
|
@@ -520,6 +520,38 @@ If you need to follow logs of a specific container, you can do it by passing con
|
|
|
520
520
|
mantis -e <ENVIRONMENT> logs <container-name>
|
|
521
521
|
```
|
|
522
522
|
|
|
523
|
+
#### Container or service name
|
|
524
|
+
|
|
525
|
+
Commands taking a container (`logs`, `healthcheck`, `bash`, `sh`, `exec`, `exec-it`) also accept a
|
|
526
|
+
plain service name and mantis finds its container(s) for you, including scaled ones with numerical
|
|
527
|
+
suffixes and services declaring their own `container_name`:
|
|
528
|
+
|
|
529
|
+
```bash
|
|
530
|
+
mantis -e production logs app # reads logs of <project>-app container
|
|
531
|
+
mantis -e production bash app # same container, no need to type the project prefix
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
A name which is not a service is still matched against container names without their project
|
|
535
|
+
prefix, which covers services numbered in the compose file:
|
|
536
|
+
|
|
537
|
+
```bash
|
|
538
|
+
mantis -e production logs htmltopdf # reads <project>-htmltopdf-1 and <project>-htmltopdf-2
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
An exact container name always takes precedence: if a container literally named `app` exists,
|
|
542
|
+
`logs app` reads that one instead of `<project>-app`. Names matching no container at all are
|
|
543
|
+
passed to docker as they are.
|
|
544
|
+
|
|
545
|
+
`logs` follows all matching containers at once, prefixing every line with a container name:
|
|
546
|
+
|
|
547
|
+
```
|
|
548
|
+
itfitness-htmltopdf-1 | POST /generate-pdf 200 3355.969 ms - 837045
|
|
549
|
+
itfitness-htmltopdf-2 | POST /generate-pdf 200 889.662 ms - 747746
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
Commands which can only operate on one container (`bash`, `sh`, `exec`, `exec-it`, `healthcheck`)
|
|
553
|
+
use the first match and warn about the rest.
|
|
554
|
+
|
|
523
555
|
### 5. Another useful commands
|
|
524
556
|
|
|
525
557
|
Sometimes, instead of calling whole deployment process, you just need to call compose commands directly:
|
|
@@ -4,6 +4,7 @@ from unittest.mock import patch, MagicMock
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
6
|
from mantis.managers import (
|
|
7
|
+
BaseManager,
|
|
7
8
|
validate_environment_for_commands,
|
|
8
9
|
resolve_environment,
|
|
9
10
|
SECRETS_COMMANDS,
|
|
@@ -403,3 +404,146 @@ class TestSecretsCommandsConstant:
|
|
|
403
404
|
"""Test that managers SECRETS_COMMANDS matches config module."""
|
|
404
405
|
from mantis.config import SECRETS_COMMANDS as CONFIG_SECRETS_COMMANDS
|
|
405
406
|
assert SECRETS_COMMANDS == CONFIG_SECRETS_COMMANDS
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class TestResolveContainers:
|
|
410
|
+
"""Tests for resolving container names from container or service names."""
|
|
411
|
+
|
|
412
|
+
def _manager(self, containers, services, project='itfitness', compose_services=None):
|
|
413
|
+
manager = BaseManager.__new__(BaseManager)
|
|
414
|
+
manager.get_containers = lambda *args, **kwargs: list(containers)
|
|
415
|
+
manager.services = lambda *args, **kwargs: list(services)
|
|
416
|
+
manager.get_project_by_service = lambda service: project
|
|
417
|
+
manager.project_services = lambda: {project: list(services)}
|
|
418
|
+
manager.compose_config = {'services': compose_services or {}}
|
|
419
|
+
return manager
|
|
420
|
+
|
|
421
|
+
def test_exact_container_name_has_priority(self):
|
|
422
|
+
"""A container named exactly like the argument wins over the service lookup."""
|
|
423
|
+
manager = self._manager(['itfitness-app', 'app'], ['app'])
|
|
424
|
+
|
|
425
|
+
assert manager.resolve_containers(['app']) == ['app']
|
|
426
|
+
|
|
427
|
+
def test_service_name_resolves_to_container(self):
|
|
428
|
+
"""Service name is prefixed with the project name."""
|
|
429
|
+
manager = self._manager(['itfitness-app', 'itfitness-db'], ['app', 'db'])
|
|
430
|
+
|
|
431
|
+
assert manager.resolve_containers(['app']) == ['itfitness-app']
|
|
432
|
+
|
|
433
|
+
def test_service_name_resolves_to_scaled_containers(self):
|
|
434
|
+
"""All containers of a scaled service are resolved."""
|
|
435
|
+
manager = self._manager(['itfitness-app-1', 'itfitness-app-2'], ['app'])
|
|
436
|
+
|
|
437
|
+
assert manager.resolve_containers(['app']) == ['itfitness-app-1', 'itfitness-app-2']
|
|
438
|
+
|
|
439
|
+
def test_service_name_does_not_match_similar_services(self):
|
|
440
|
+
"""Service "app" must not pull in containers of service "app-worker"."""
|
|
441
|
+
manager = self._manager(['itfitness-app', 'itfitness-app-worker'], ['app', 'app-worker'])
|
|
442
|
+
|
|
443
|
+
assert manager.resolve_containers(['app']) == ['itfitness-app']
|
|
444
|
+
|
|
445
|
+
def test_explicit_container_name_of_service_is_used(self):
|
|
446
|
+
"""Service defining its own container_name is resolved to that name."""
|
|
447
|
+
manager = self._manager(
|
|
448
|
+
['legacy-app'],
|
|
449
|
+
['app'],
|
|
450
|
+
compose_services={'app': {'container_name': 'legacy-app'}},
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
assert manager.resolve_containers(['app']) == ['legacy-app']
|
|
454
|
+
|
|
455
|
+
def test_common_name_of_numbered_services_resolves(self):
|
|
456
|
+
""""htmltopdf" matches containers of services "htmltopdf-1" and "htmltopdf-2"."""
|
|
457
|
+
manager = self._manager(
|
|
458
|
+
['itfitness-htmltopdf-1', 'itfitness-htmltopdf-2', 'itfitness-app'],
|
|
459
|
+
['htmltopdf-1', 'htmltopdf-2', 'app'],
|
|
460
|
+
compose_services={
|
|
461
|
+
'htmltopdf-1': {'container_name': 'itfitness-htmltopdf-1'},
|
|
462
|
+
'htmltopdf-2': {'container_name': 'itfitness-htmltopdf-2'},
|
|
463
|
+
},
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
assert manager.resolve_containers(['htmltopdf']) == ['itfitness-htmltopdf-1', 'itfitness-htmltopdf-2']
|
|
467
|
+
|
|
468
|
+
def test_numbered_service_still_resolves_to_its_own_container(self):
|
|
469
|
+
"""The numbered service name keeps resolving to a single container."""
|
|
470
|
+
manager = self._manager(
|
|
471
|
+
['itfitness-htmltopdf-1', 'itfitness-htmltopdf-2'],
|
|
472
|
+
['htmltopdf-1', 'htmltopdf-2'],
|
|
473
|
+
compose_services={
|
|
474
|
+
'htmltopdf-1': {'container_name': 'itfitness-htmltopdf-1'},
|
|
475
|
+
'htmltopdf-2': {'container_name': 'itfitness-htmltopdf-2'},
|
|
476
|
+
},
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
assert manager.resolve_containers(['htmltopdf-2']) == ['itfitness-htmltopdf-2']
|
|
480
|
+
|
|
481
|
+
def test_unknown_name_is_kept_as_given(self):
|
|
482
|
+
"""Names which are neither a container nor a service are passed to docker untouched."""
|
|
483
|
+
manager = self._manager(['itfitness-app'], ['app'])
|
|
484
|
+
|
|
485
|
+
assert manager.resolve_containers(['whatever']) == ['whatever']
|
|
486
|
+
|
|
487
|
+
def test_multiple_names_are_deduplicated(self):
|
|
488
|
+
"""Service name and its container name resolve to a single container."""
|
|
489
|
+
manager = self._manager(['itfitness-app', 'itfitness-db'], ['app', 'db'])
|
|
490
|
+
|
|
491
|
+
assert manager.resolve_containers(['app', 'itfitness-app', 'db']) == ['itfitness-app', 'itfitness-db']
|
|
492
|
+
|
|
493
|
+
@patch('mantis.managers.CLI.warning')
|
|
494
|
+
def test_resolve_container_returns_single_name(self, mock_warning):
|
|
495
|
+
"""Commands operating on one container get one container name."""
|
|
496
|
+
manager = self._manager(['itfitness-app', 'itfitness-db'], ['app', 'db'])
|
|
497
|
+
|
|
498
|
+
assert manager.resolve_container('app') == 'itfitness-app'
|
|
499
|
+
mock_warning.assert_not_called()
|
|
500
|
+
|
|
501
|
+
@patch('mantis.managers.CLI.warning')
|
|
502
|
+
def test_resolve_container_warns_on_multiple_matches(self, mock_warning):
|
|
503
|
+
"""A scaled service picks the first container and warns about the rest."""
|
|
504
|
+
manager = self._manager(['itfitness-app-1', 'itfitness-app-2'], ['app'])
|
|
505
|
+
|
|
506
|
+
assert manager.resolve_container('app') == 'itfitness-app-1'
|
|
507
|
+
mock_warning.assert_called_once()
|
|
508
|
+
|
|
509
|
+
def test_resolve_container_accepts_prefetched_containers(self):
|
|
510
|
+
"""Callers which already listed containers do not trigger another docker call."""
|
|
511
|
+
manager = self._manager([], ['app'])
|
|
512
|
+
manager.get_containers = lambda *args, **kwargs: pytest.fail('containers listed again')
|
|
513
|
+
|
|
514
|
+
assert manager.resolve_container('app', ['itfitness-app']) == 'itfitness-app'
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
class TestQuietProgress:
|
|
518
|
+
"""Layer progress is repainted in place, which only works on a terminal."""
|
|
519
|
+
|
|
520
|
+
@staticmethod
|
|
521
|
+
def _manager():
|
|
522
|
+
manager = BaseManager.__new__(BaseManager)
|
|
523
|
+
manager.commands = []
|
|
524
|
+
manager.docker_compose = lambda command, **kwargs: manager.commands.append(
|
|
525
|
+
' '.join(command.split())
|
|
526
|
+
)
|
|
527
|
+
return manager
|
|
528
|
+
|
|
529
|
+
@patch('sys.stdout', new_callable=MagicMock)
|
|
530
|
+
def test_push_and_pull_are_quiet_off_a_terminal(self, mock_stdout):
|
|
531
|
+
"""A log file or a CI pipe would otherwise get every progress event as its own line."""
|
|
532
|
+
mock_stdout.isatty.return_value = False
|
|
533
|
+
manager = self._manager()
|
|
534
|
+
|
|
535
|
+
manager.push()
|
|
536
|
+
manager.pull()
|
|
537
|
+
|
|
538
|
+
assert manager.commands == ['push --quiet', 'pull --quiet']
|
|
539
|
+
|
|
540
|
+
@patch('sys.stdout', new_callable=MagicMock)
|
|
541
|
+
def test_push_and_pull_keep_progress_on_a_terminal(self, mock_stdout):
|
|
542
|
+
"""Interactively the progress is the point, so nothing changes."""
|
|
543
|
+
mock_stdout.isatty.return_value = True
|
|
544
|
+
manager = self._manager()
|
|
545
|
+
|
|
546
|
+
manager.push(['app'])
|
|
547
|
+
manager.pull(['app'])
|
|
548
|
+
|
|
549
|
+
assert manager.commands == ['push app', 'pull app']
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VERSION = '22.2.0'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|