mantis-cli 22.2.0__tar.gz → 22.3.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.3.0}/PKG-INFO +61 -10
- mantis_cli-22.2.0/mantis_cli.egg-info/PKG-INFO → mantis_cli-22.3.0/README.md +39 -30
- mantis_cli-22.3.0/mantis/__init__.py +1 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/containers.py +6 -6
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/managers.py +196 -11
- mantis_cli-22.2.0/README.md → mantis_cli-22.3.0/mantis_cli.egg-info/PKG-INFO +81 -7
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis_cli.egg-info/entry_points.txt +0 -1
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/tests/test_managers.py +109 -0
- mantis_cli-22.2.0/mantis/__init__.py +0 -1
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/LICENSE +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/MANIFEST.in +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/__main__.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/app.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/command_line.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/__init__.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/compose.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/configuration.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/connection.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/core.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/django.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/images.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/nginx.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/postgres.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/secrets.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/services.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/volumes.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/config.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/cryptography.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/environment.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/extensions/__init__.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/extensions/django.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/extensions/nginx.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/extensions/postgres.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/helpers.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/mantis.tpl +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/schema.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis_cli.egg-info/SOURCES.txt +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis_cli.egg-info/dependency_links.txt +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis_cli.egg-info/requires.txt +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis_cli.egg-info/top_level.txt +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/setup.cfg +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/setup.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/tests/__init__.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/tests/test_build_config.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/tests/test_command_line.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/tests/test_config.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/tests/test_helpers.py +0 -0
- {mantis_cli-22.2.0 → mantis_cli-22.3.0}/tests/test_tunnel.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: mantis_cli
|
|
3
|
-
Version: 22.
|
|
3
|
+
Version: 22.3.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ý
|
|
@@ -9,7 +9,6 @@ Maintainer: Pragmatic Mates
|
|
|
9
9
|
Maintainer-email: info@pragmaticmates.com
|
|
10
10
|
License: GNU General Public License (GPL)
|
|
11
11
|
Keywords: management deployment docker command
|
|
12
|
-
Platform: UNKNOWN
|
|
13
12
|
Classifier: Programming Language :: Python
|
|
14
13
|
Classifier: Operating System :: OS Independent
|
|
15
14
|
Classifier: Environment :: Web Environment
|
|
@@ -19,6 +18,27 @@ Classifier: License :: OSI Approved :: GNU General Public License (GPL)
|
|
|
19
18
|
Classifier: Development Status :: 5 - Production/Stable
|
|
20
19
|
Description-Content-Type: text/markdown
|
|
21
20
|
License-File: LICENSE
|
|
21
|
+
Requires-Dist: cffi
|
|
22
|
+
Requires-Dist: click
|
|
23
|
+
Requires-Dist: cryptography
|
|
24
|
+
Requires-Dist: pycryptodome
|
|
25
|
+
Requires-Dist: pydantic
|
|
26
|
+
Requires-Dist: PyYAML
|
|
27
|
+
Requires-Dist: rich
|
|
28
|
+
Requires-Dist: typer
|
|
29
|
+
Dynamic: author
|
|
30
|
+
Dynamic: author-email
|
|
31
|
+
Dynamic: classifier
|
|
32
|
+
Dynamic: description
|
|
33
|
+
Dynamic: description-content-type
|
|
34
|
+
Dynamic: home-page
|
|
35
|
+
Dynamic: keywords
|
|
36
|
+
Dynamic: license
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
Dynamic: maintainer
|
|
39
|
+
Dynamic: maintainer-email
|
|
40
|
+
Dynamic: requires-dist
|
|
41
|
+
Dynamic: summary
|
|
22
42
|
|
|
23
43
|
# mantis-cli
|
|
24
44
|
|
|
@@ -325,18 +345,18 @@ Run `mantis --help` to see all available commands with their descriptions.
|
|
|
325
345
|
|
|
326
346
|
| Command / Shortcut | Description |
|
|
327
347
|
|---------------------------------------|-----------------------------------------------------------|
|
|
328
|
-
| logs [CONTAINER] / l
|
|
348
|
+
| logs [CONTAINER\|SERVICE] / l | Prints logs of containers |
|
|
329
349
|
| networks / n | Prints docker networks |
|
|
330
|
-
| healthcheck [CONTAINER] / hc
|
|
350
|
+
| healthcheck [CONTAINER\|SERVICE] / hc | Execute health-check of container |
|
|
331
351
|
| stop [CONTAINERS...] | Stops containers |
|
|
332
352
|
| start [CONTAINERS...] | Starts containers |
|
|
333
353
|
| kill [CONTAINERS...] | Kills containers |
|
|
334
354
|
| remove [CONTAINERS...] [--force] | Removes containers |
|
|
335
355
|
| rename CONTAINER NEW_NAME | Rename container |
|
|
336
|
-
| bash CONTAINER
|
|
337
|
-
| sh CONTAINER
|
|
338
|
-
| exec CONTAINER COMMAND...
|
|
339
|
-
| exec-it CONTAINER COMMAND...
|
|
356
|
+
| bash CONTAINER\|SERVICE | Runs bash in container |
|
|
357
|
+
| sh CONTAINER\|SERVICE | Runs sh in container |
|
|
358
|
+
| exec CONTAINER\|SERVICE COMMAND... | Executes command in container |
|
|
359
|
+
| exec-it CONTAINER\|SERVICE COMMAND... | Executes command in container (interactive) |
|
|
340
360
|
| get-container-name SERVICE | Gets container name for service |
|
|
341
361
|
| remove-suffixes [PREFIX] | Removes numerical suffixes from container names |
|
|
342
362
|
|
|
@@ -520,6 +540,38 @@ If you need to follow logs of a specific container, you can do it by passing con
|
|
|
520
540
|
mantis -e <ENVIRONMENT> logs <container-name>
|
|
521
541
|
```
|
|
522
542
|
|
|
543
|
+
#### Container or service name
|
|
544
|
+
|
|
545
|
+
Commands taking a container (`logs`, `healthcheck`, `bash`, `sh`, `exec`, `exec-it`) also accept a
|
|
546
|
+
plain service name and mantis finds its container(s) for you, including scaled ones with numerical
|
|
547
|
+
suffixes and services declaring their own `container_name`:
|
|
548
|
+
|
|
549
|
+
```bash
|
|
550
|
+
mantis -e production logs app # reads logs of <project>-app container
|
|
551
|
+
mantis -e production bash app # same container, no need to type the project prefix
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
A name which is not a service is still matched against container names without their project
|
|
555
|
+
prefix, which covers services numbered in the compose file:
|
|
556
|
+
|
|
557
|
+
```bash
|
|
558
|
+
mantis -e production logs htmltopdf # reads <project>-htmltopdf-1 and <project>-htmltopdf-2
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
An exact container name always takes precedence: if a container literally named `app` exists,
|
|
562
|
+
`logs app` reads that one instead of `<project>-app`. Names matching no container at all are
|
|
563
|
+
passed to docker as they are.
|
|
564
|
+
|
|
565
|
+
`logs` follows all matching containers at once, prefixing every line with a container name:
|
|
566
|
+
|
|
567
|
+
```
|
|
568
|
+
itfitness-htmltopdf-1 | POST /generate-pdf 200 3355.969 ms - 837045
|
|
569
|
+
itfitness-htmltopdf-2 | POST /generate-pdf 200 889.662 ms - 747746
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
Commands which can only operate on one container (`bash`, `sh`, `exec`, `exec-it`, `healthcheck`)
|
|
573
|
+
use the first match and warn about the rest.
|
|
574
|
+
|
|
523
575
|
### 5. Another useful commands
|
|
524
576
|
|
|
525
577
|
Sometimes, instead of calling whole deployment process, you just need to call compose commands directly:
|
|
@@ -559,4 +611,3 @@ Works as follows:
|
|
|
559
611
|
## Release notes
|
|
560
612
|
|
|
561
613
|
Mantis uses semantic versioning. See more in [changelog](https://github.com/PragmaticMates/mantis-cli/blob/master/CHANGES.md).
|
|
562
|
-
|
|
@@ -1,25 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: mantis-cli
|
|
3
|
-
Version: 22.2.0
|
|
4
|
-
Summary: Management command to build and deploy webapps, especially based on Django
|
|
5
|
-
Home-page: https://github.com/PragmaticMates/mantis-cli
|
|
6
|
-
Author: Erik Telepovský
|
|
7
|
-
Author-email: info@pragmaticmates.com
|
|
8
|
-
Maintainer: Pragmatic Mates
|
|
9
|
-
Maintainer-email: info@pragmaticmates.com
|
|
10
|
-
License: GNU General Public License (GPL)
|
|
11
|
-
Keywords: management deployment docker command
|
|
12
|
-
Platform: UNKNOWN
|
|
13
|
-
Classifier: Programming Language :: Python
|
|
14
|
-
Classifier: Operating System :: OS Independent
|
|
15
|
-
Classifier: Environment :: Web Environment
|
|
16
|
-
Classifier: Intended Audience :: Developers
|
|
17
|
-
Classifier: Framework :: Django
|
|
18
|
-
Classifier: License :: OSI Approved :: GNU General Public License (GPL)
|
|
19
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
20
|
-
Description-Content-Type: text/markdown
|
|
21
|
-
License-File: LICENSE
|
|
22
|
-
|
|
23
1
|
# mantis-cli
|
|
24
2
|
|
|
25
3
|
Mantis is a CLI (command line interface) tool designed as a wrapper upon docker and docker compose commands for your project.
|
|
@@ -325,18 +303,18 @@ Run `mantis --help` to see all available commands with their descriptions.
|
|
|
325
303
|
|
|
326
304
|
| Command / Shortcut | Description |
|
|
327
305
|
|---------------------------------------|-----------------------------------------------------------|
|
|
328
|
-
| logs [CONTAINER] / l
|
|
306
|
+
| logs [CONTAINER\|SERVICE] / l | Prints logs of containers |
|
|
329
307
|
| networks / n | Prints docker networks |
|
|
330
|
-
| healthcheck [CONTAINER] / hc
|
|
308
|
+
| healthcheck [CONTAINER\|SERVICE] / hc | Execute health-check of container |
|
|
331
309
|
| stop [CONTAINERS...] | Stops containers |
|
|
332
310
|
| start [CONTAINERS...] | Starts containers |
|
|
333
311
|
| kill [CONTAINERS...] | Kills containers |
|
|
334
312
|
| remove [CONTAINERS...] [--force] | Removes containers |
|
|
335
313
|
| rename CONTAINER NEW_NAME | Rename container |
|
|
336
|
-
| bash CONTAINER
|
|
337
|
-
| sh CONTAINER
|
|
338
|
-
| exec CONTAINER COMMAND...
|
|
339
|
-
| 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) |
|
|
340
318
|
| get-container-name SERVICE | Gets container name for service |
|
|
341
319
|
| remove-suffixes [PREFIX] | Removes numerical suffixes from container names |
|
|
342
320
|
|
|
@@ -520,6 +498,38 @@ If you need to follow logs of a specific container, you can do it by passing con
|
|
|
520
498
|
mantis -e <ENVIRONMENT> logs <container-name>
|
|
521
499
|
```
|
|
522
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
|
+
|
|
523
533
|
### 5. Another useful commands
|
|
524
534
|
|
|
525
535
|
Sometimes, instead of calling whole deployment process, you just need to call compose commands directly:
|
|
@@ -558,5 +568,4 @@ Works as follows:
|
|
|
558
568
|
|
|
559
569
|
## Release notes
|
|
560
570
|
|
|
561
|
-
Mantis uses semantic versioning. See more in [changelog](https://github.com/PragmaticMates/mantis-cli/blob/master/CHANGES.md).
|
|
562
|
-
|
|
571
|
+
Mantis uses semantic versioning. See more in [changelog](https://github.com/PragmaticMates/mantis-cli/blob/master/CHANGES.md).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = '22.3.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,13 @@ class AbstractManager(object):
|
|
|
519
521
|
except OSError as e:
|
|
520
522
|
CLI.error(f"{error_message}: {e}")
|
|
521
523
|
|
|
522
|
-
def
|
|
524
|
+
def build_docker_command(self, command: str, use_connection: bool = True) -> str:
|
|
523
525
|
docker_connection = self.docker_connection if use_connection else ''
|
|
524
526
|
|
|
525
|
-
|
|
527
|
+
return f'{docker_connection} {command}'.strip()
|
|
528
|
+
|
|
529
|
+
def docker_command(self, command: str, return_output: bool = False, use_connection: bool = True) -> Optional[str]:
|
|
530
|
+
cmd = self.build_docker_command(command, use_connection)
|
|
526
531
|
|
|
527
532
|
if return_output:
|
|
528
533
|
if self.dry_run:
|
|
@@ -889,6 +894,97 @@ class BaseManager(AbstractManager):
|
|
|
889
894
|
prefix = self.get_project_by_service(service)
|
|
890
895
|
return f'{prefix}{suffix}'.replace('_', '-')
|
|
891
896
|
|
|
897
|
+
def resolve_containers(self, names: List[str], project_containers: Optional[List[str]] = None) -> List[str]:
|
|
898
|
+
"""
|
|
899
|
+
Resolves given names to project container names.
|
|
900
|
+
Exact container name has the highest priority, service name is used as a fallback,
|
|
901
|
+
i.e. "app" resolves to "<project>-app" (or its scaled containers "<project>-app-1", ...)
|
|
902
|
+
unless a container literally named "app" exists.
|
|
903
|
+
"""
|
|
904
|
+
if project_containers is None:
|
|
905
|
+
project_containers = self.get_containers()
|
|
906
|
+
|
|
907
|
+
services = self.services()
|
|
908
|
+
resolved = []
|
|
909
|
+
|
|
910
|
+
for name in names:
|
|
911
|
+
# exact container name has the highest priority
|
|
912
|
+
if name in project_containers:
|
|
913
|
+
containers = [name]
|
|
914
|
+
else:
|
|
915
|
+
# service name, or a common name of numbered containers
|
|
916
|
+
containers = (self.get_containers_by_service(name, project_containers) if name in services else []) \
|
|
917
|
+
or self.get_containers_by_name(name, project_containers)
|
|
918
|
+
|
|
919
|
+
if containers:
|
|
920
|
+
CLI.info(f'{name} resolved to: {", ".join(containers)}')
|
|
921
|
+
else:
|
|
922
|
+
# unknown name: keep it as given and let docker report it
|
|
923
|
+
containers = [name]
|
|
924
|
+
|
|
925
|
+
# keep the order and avoid duplicates
|
|
926
|
+
resolved.extend(container for container in containers if container not in resolved)
|
|
927
|
+
|
|
928
|
+
return resolved
|
|
929
|
+
|
|
930
|
+
def resolve_container(self, name: str, project_containers: Optional[List[str]] = None) -> str:
|
|
931
|
+
"""
|
|
932
|
+
Resolves given name to a single project container name (see resolve_containers).
|
|
933
|
+
Used by commands which can operate on one container only.
|
|
934
|
+
"""
|
|
935
|
+
containers = self.resolve_containers([name], project_containers)
|
|
936
|
+
|
|
937
|
+
if len(containers) > 1:
|
|
938
|
+
CLI.warning(f'{name} matches {len(containers)} containers, using {containers[0]}')
|
|
939
|
+
|
|
940
|
+
return containers[0]
|
|
941
|
+
|
|
942
|
+
def get_containers_by_service(self, service: str, project_containers: Optional[List[str]] = None) -> List[str]:
|
|
943
|
+
"""
|
|
944
|
+
Returns names of existing containers belonging to given service,
|
|
945
|
+
including scaled ones with numerical suffixes
|
|
946
|
+
"""
|
|
947
|
+
if project_containers is None:
|
|
948
|
+
project_containers = self.get_containers()
|
|
949
|
+
|
|
950
|
+
# service can define its container name explicitly
|
|
951
|
+
defined_container_name = self.compose_config.get('services', {}).get(service, {}).get('container_name', None)
|
|
952
|
+
|
|
953
|
+
if defined_container_name and defined_container_name in project_containers:
|
|
954
|
+
return [defined_container_name]
|
|
955
|
+
|
|
956
|
+
return self.match_containers(self.get_container_name(service), project_containers)
|
|
957
|
+
|
|
958
|
+
def get_containers_by_name(self, name: str, project_containers: Optional[List[str]] = None) -> List[str]:
|
|
959
|
+
"""
|
|
960
|
+
Returns names of existing project containers matching given name prefixed with a project name.
|
|
961
|
+
Covers names which are not declared services, i.e. "htmltopdf" for services "htmltopdf-1"
|
|
962
|
+
and "htmltopdf-2" running as containers "<project>-htmltopdf-1" and "<project>-htmltopdf-2"
|
|
963
|
+
"""
|
|
964
|
+
if project_containers is None:
|
|
965
|
+
project_containers = self.get_containers()
|
|
966
|
+
|
|
967
|
+
containers = []
|
|
968
|
+
|
|
969
|
+
for project in self.project_services().keys():
|
|
970
|
+
container_name = f'{project}{self.get_container_suffix(name)}'.replace('_', '-')
|
|
971
|
+
|
|
972
|
+
containers.extend(
|
|
973
|
+
container
|
|
974
|
+
for container in self.match_containers(container_name, project_containers)
|
|
975
|
+
if container not in containers
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
return containers
|
|
979
|
+
|
|
980
|
+
def match_containers(self, container_name: str, project_containers: List[str]) -> List[str]:
|
|
981
|
+
"""
|
|
982
|
+
Returns containers named exactly as given container name or its numbered variants
|
|
983
|
+
"""
|
|
984
|
+
pattern = re.compile(rf'^{re.escape(container_name)}(-\d+)?$')
|
|
985
|
+
|
|
986
|
+
return [container for container in project_containers if pattern.match(container)]
|
|
987
|
+
|
|
892
988
|
def get_service_containers(self, service: str) -> List[str]:
|
|
893
989
|
"""
|
|
894
990
|
Prints container names of given service
|
|
@@ -954,7 +1050,10 @@ class BaseManager(AbstractManager):
|
|
|
954
1050
|
"""
|
|
955
1051
|
Execute health-check of given project container
|
|
956
1052
|
"""
|
|
957
|
-
|
|
1053
|
+
project_containers = self.get_containers()
|
|
1054
|
+
container = self.resolve_container(container, project_containers)
|
|
1055
|
+
|
|
1056
|
+
if container not in project_containers:
|
|
958
1057
|
CLI.error(f"Container {container} not found")
|
|
959
1058
|
|
|
960
1059
|
console = Console()
|
|
@@ -1746,28 +1845,112 @@ class BaseManager(AbstractManager):
|
|
|
1746
1845
|
"""
|
|
1747
1846
|
CLI.info('Reading logs...')
|
|
1748
1847
|
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1848
|
+
if not params:
|
|
1849
|
+
containers = self.get_containers()
|
|
1850
|
+
steps = len(containers)
|
|
1752
1851
|
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1852
|
+
for index, container in enumerate(containers):
|
|
1853
|
+
CLI.step(index + 1, steps, f'{container} logs')
|
|
1854
|
+
self.docker(f'logs {container} --tail 10')
|
|
1855
|
+
|
|
1856
|
+
return
|
|
1857
|
+
|
|
1858
|
+
containers = self.resolve_containers(params.split())
|
|
1859
|
+
|
|
1860
|
+
# following a single container does not need any output multiplexing
|
|
1861
|
+
if len(containers) == 1:
|
|
1862
|
+
self.docker(f'logs {containers[0]} --tail 1000 -f')
|
|
1863
|
+
return
|
|
1864
|
+
|
|
1865
|
+
self.follow_logs(containers)
|
|
1866
|
+
|
|
1867
|
+
def follow_logs(self, containers: List[str], tail: int = 1000) -> None:
|
|
1868
|
+
"""
|
|
1869
|
+
Follows logs of multiple containers at once, prefixing every line with a container name.
|
|
1870
|
+
Following them one by one is not an option as the first one would never finish.
|
|
1871
|
+
"""
|
|
1872
|
+
commands = {
|
|
1873
|
+
container: self.build_docker_command(f'docker logs {container} --tail {tail} -f')
|
|
1874
|
+
for container in containers
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
if self.dry_run:
|
|
1878
|
+
for command in commands.values():
|
|
1879
|
+
CLI.warning(f'[DRY-RUN] {command}')
|
|
1880
|
+
return
|
|
1881
|
+
|
|
1882
|
+
processes = {}
|
|
1883
|
+
|
|
1884
|
+
for container, command in commands.items():
|
|
1885
|
+
print(command)
|
|
1886
|
+
processes[container] = subprocess.Popen(
|
|
1887
|
+
command,
|
|
1888
|
+
shell=True,
|
|
1889
|
+
stdout=subprocess.PIPE,
|
|
1890
|
+
stderr=subprocess.STDOUT,
|
|
1891
|
+
text=True,
|
|
1892
|
+
bufsize=1,
|
|
1893
|
+
# own process group, so Ctrl+C is handled here and kills the whole shell + docker tree
|
|
1894
|
+
start_new_session=True
|
|
1895
|
+
)
|
|
1896
|
+
|
|
1897
|
+
width = max(map(len, processes.keys()))
|
|
1898
|
+
|
|
1899
|
+
def stream(container: str, process: subprocess.Popen) -> None:
|
|
1900
|
+
for line in process.stdout:
|
|
1901
|
+
print(f'{container:<{width}} | {line}', end='', flush=True)
|
|
1902
|
+
|
|
1903
|
+
threads = [
|
|
1904
|
+
threading.Thread(target=stream, args=(container, process), daemon=True)
|
|
1905
|
+
for container, process in processes.items()
|
|
1906
|
+
]
|
|
1907
|
+
|
|
1908
|
+
for thread in threads:
|
|
1909
|
+
thread.start()
|
|
1910
|
+
|
|
1911
|
+
try:
|
|
1912
|
+
# wait for the processes rather than for the reader threads, so that Ctrl+C
|
|
1913
|
+
# interrupts a plain sleep in the main thread and cleanup always runs
|
|
1914
|
+
while any(process.poll() is None for process in processes.values()):
|
|
1915
|
+
sleep(0.2)
|
|
1916
|
+
except KeyboardInterrupt:
|
|
1917
|
+
pass
|
|
1918
|
+
finally:
|
|
1919
|
+
for process in processes.values():
|
|
1920
|
+
self.terminate_process_group(process)
|
|
1921
|
+
|
|
1922
|
+
for thread in threads:
|
|
1923
|
+
thread.join(timeout=1)
|
|
1924
|
+
|
|
1925
|
+
@staticmethod
|
|
1926
|
+
def terminate_process_group(process: subprocess.Popen) -> None:
|
|
1927
|
+
"""
|
|
1928
|
+
Terminates given process together with its children, i.e. the shell running a docker command
|
|
1929
|
+
"""
|
|
1930
|
+
if process.poll() is not None:
|
|
1931
|
+
return
|
|
1932
|
+
|
|
1933
|
+
try:
|
|
1934
|
+
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
|
|
1935
|
+
except (ProcessLookupError, PermissionError):
|
|
1936
|
+
process.terminate()
|
|
1756
1937
|
|
|
1757
1938
|
def bash(self, params: str) -> None:
|
|
1758
1939
|
"""
|
|
1759
1940
|
Runs bash in container
|
|
1760
1941
|
"""
|
|
1942
|
+
container = self.resolve_container(params)
|
|
1761
1943
|
CLI.info('Running bash...')
|
|
1762
|
-
self.docker(f'exec -it --user root {
|
|
1944
|
+
self.docker(f'exec -it --user root {container} /bin/bash')
|
|
1763
1945
|
# self.docker_compose(f'run --entrypoint /bin/bash {container}')
|
|
1764
1946
|
|
|
1765
1947
|
def sh(self, params: str) -> None:
|
|
1766
1948
|
"""
|
|
1767
1949
|
Runs sh in container
|
|
1768
1950
|
"""
|
|
1951
|
+
container = self.resolve_container(params)
|
|
1769
1952
|
CLI.info('Logging to container...')
|
|
1770
|
-
self.docker(f'exec -it --user root {
|
|
1953
|
+
self.docker(f'exec -it --user root {container} /bin/sh')
|
|
1771
1954
|
|
|
1772
1955
|
def ssh(self) -> None:
|
|
1773
1956
|
if not self.connection:
|
|
@@ -1783,6 +1966,7 @@ class BaseManager(AbstractManager):
|
|
|
1783
1966
|
"""
|
|
1784
1967
|
Executes command in container
|
|
1785
1968
|
"""
|
|
1969
|
+
container = self.resolve_container(container)
|
|
1786
1970
|
command = ' '.join(cmd)
|
|
1787
1971
|
CLI.info(f'Executing command "{command}" in container {container}...')
|
|
1788
1972
|
self.docker(f'exec {container} {command}')
|
|
@@ -1791,6 +1975,7 @@ class BaseManager(AbstractManager):
|
|
|
1791
1975
|
"""
|
|
1792
1976
|
Executes command in container using interactive pseudo-TTY
|
|
1793
1977
|
"""
|
|
1978
|
+
container = self.resolve_container(container)
|
|
1794
1979
|
command = ' '.join(cmd)
|
|
1795
1980
|
CLI.info(f'Executing command "{command}" in container {container}...')
|
|
1796
1981
|
self.docker(f'exec -it {container} {command}')
|
|
@@ -1,3 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mantis_cli
|
|
3
|
+
Version: 22.3.0
|
|
4
|
+
Summary: Management command to build and deploy webapps, especially based on Django
|
|
5
|
+
Home-page: https://github.com/PragmaticMates/mantis-cli
|
|
6
|
+
Author: Erik Telepovský
|
|
7
|
+
Author-email: info@pragmaticmates.com
|
|
8
|
+
Maintainer: Pragmatic Mates
|
|
9
|
+
Maintainer-email: info@pragmaticmates.com
|
|
10
|
+
License: GNU General Public License (GPL)
|
|
11
|
+
Keywords: management deployment docker command
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Environment :: Web Environment
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Framework :: Django
|
|
17
|
+
Classifier: License :: OSI Approved :: GNU General Public License (GPL)
|
|
18
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: cffi
|
|
22
|
+
Requires-Dist: click
|
|
23
|
+
Requires-Dist: cryptography
|
|
24
|
+
Requires-Dist: pycryptodome
|
|
25
|
+
Requires-Dist: pydantic
|
|
26
|
+
Requires-Dist: PyYAML
|
|
27
|
+
Requires-Dist: rich
|
|
28
|
+
Requires-Dist: typer
|
|
29
|
+
Dynamic: author
|
|
30
|
+
Dynamic: author-email
|
|
31
|
+
Dynamic: classifier
|
|
32
|
+
Dynamic: description
|
|
33
|
+
Dynamic: description-content-type
|
|
34
|
+
Dynamic: home-page
|
|
35
|
+
Dynamic: keywords
|
|
36
|
+
Dynamic: license
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
Dynamic: maintainer
|
|
39
|
+
Dynamic: maintainer-email
|
|
40
|
+
Dynamic: requires-dist
|
|
41
|
+
Dynamic: summary
|
|
42
|
+
|
|
1
43
|
# mantis-cli
|
|
2
44
|
|
|
3
45
|
Mantis is a CLI (command line interface) tool designed as a wrapper upon docker and docker compose commands for your project.
|
|
@@ -303,18 +345,18 @@ Run `mantis --help` to see all available commands with their descriptions.
|
|
|
303
345
|
|
|
304
346
|
| Command / Shortcut | Description |
|
|
305
347
|
|---------------------------------------|-----------------------------------------------------------|
|
|
306
|
-
| logs [CONTAINER] / l
|
|
348
|
+
| logs [CONTAINER\|SERVICE] / l | Prints logs of containers |
|
|
307
349
|
| networks / n | Prints docker networks |
|
|
308
|
-
| healthcheck [CONTAINER] / hc
|
|
350
|
+
| healthcheck [CONTAINER\|SERVICE] / hc | Execute health-check of container |
|
|
309
351
|
| stop [CONTAINERS...] | Stops containers |
|
|
310
352
|
| start [CONTAINERS...] | Starts containers |
|
|
311
353
|
| kill [CONTAINERS...] | Kills containers |
|
|
312
354
|
| remove [CONTAINERS...] [--force] | Removes containers |
|
|
313
355
|
| rename CONTAINER NEW_NAME | Rename container |
|
|
314
|
-
| bash CONTAINER
|
|
315
|
-
| sh CONTAINER
|
|
316
|
-
| exec CONTAINER COMMAND...
|
|
317
|
-
| exec-it CONTAINER COMMAND...
|
|
356
|
+
| bash CONTAINER\|SERVICE | Runs bash in container |
|
|
357
|
+
| sh CONTAINER\|SERVICE | Runs sh in container |
|
|
358
|
+
| exec CONTAINER\|SERVICE COMMAND... | Executes command in container |
|
|
359
|
+
| exec-it CONTAINER\|SERVICE COMMAND... | Executes command in container (interactive) |
|
|
318
360
|
| get-container-name SERVICE | Gets container name for service |
|
|
319
361
|
| remove-suffixes [PREFIX] | Removes numerical suffixes from container names |
|
|
320
362
|
|
|
@@ -498,6 +540,38 @@ If you need to follow logs of a specific container, you can do it by passing con
|
|
|
498
540
|
mantis -e <ENVIRONMENT> logs <container-name>
|
|
499
541
|
```
|
|
500
542
|
|
|
543
|
+
#### Container or service name
|
|
544
|
+
|
|
545
|
+
Commands taking a container (`logs`, `healthcheck`, `bash`, `sh`, `exec`, `exec-it`) also accept a
|
|
546
|
+
plain service name and mantis finds its container(s) for you, including scaled ones with numerical
|
|
547
|
+
suffixes and services declaring their own `container_name`:
|
|
548
|
+
|
|
549
|
+
```bash
|
|
550
|
+
mantis -e production logs app # reads logs of <project>-app container
|
|
551
|
+
mantis -e production bash app # same container, no need to type the project prefix
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
A name which is not a service is still matched against container names without their project
|
|
555
|
+
prefix, which covers services numbered in the compose file:
|
|
556
|
+
|
|
557
|
+
```bash
|
|
558
|
+
mantis -e production logs htmltopdf # reads <project>-htmltopdf-1 and <project>-htmltopdf-2
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
An exact container name always takes precedence: if a container literally named `app` exists,
|
|
562
|
+
`logs app` reads that one instead of `<project>-app`. Names matching no container at all are
|
|
563
|
+
passed to docker as they are.
|
|
564
|
+
|
|
565
|
+
`logs` follows all matching containers at once, prefixing every line with a container name:
|
|
566
|
+
|
|
567
|
+
```
|
|
568
|
+
itfitness-htmltopdf-1 | POST /generate-pdf 200 3355.969 ms - 837045
|
|
569
|
+
itfitness-htmltopdf-2 | POST /generate-pdf 200 889.662 ms - 747746
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
Commands which can only operate on one container (`bash`, `sh`, `exec`, `exec-it`, `healthcheck`)
|
|
573
|
+
use the first match and warn about the rest.
|
|
574
|
+
|
|
501
575
|
### 5. Another useful commands
|
|
502
576
|
|
|
503
577
|
Sometimes, instead of calling whole deployment process, you just need to call compose commands directly:
|
|
@@ -536,4 +610,4 @@ Works as follows:
|
|
|
536
610
|
|
|
537
611
|
## Release notes
|
|
538
612
|
|
|
539
|
-
Mantis uses semantic versioning. See more in [changelog](https://github.com/PragmaticMates/mantis-cli/blob/master/CHANGES.md).
|
|
613
|
+
Mantis uses semantic versioning. See more in [changelog](https://github.com/PragmaticMates/mantis-cli/blob/master/CHANGES.md).
|
|
@@ -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,111 @@ 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'
|
|
@@ -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
|