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.
Files changed (48) hide show
  1. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/PKG-INFO +61 -10
  2. mantis_cli-22.2.0/mantis_cli.egg-info/PKG-INFO → mantis_cli-22.3.0/README.md +39 -30
  3. mantis_cli-22.3.0/mantis/__init__.py +1 -0
  4. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/containers.py +6 -6
  5. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/managers.py +196 -11
  6. mantis_cli-22.2.0/README.md → mantis_cli-22.3.0/mantis_cli.egg-info/PKG-INFO +81 -7
  7. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis_cli.egg-info/entry_points.txt +0 -1
  8. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/tests/test_managers.py +109 -0
  9. mantis_cli-22.2.0/mantis/__init__.py +0 -1
  10. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/LICENSE +0 -0
  11. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/MANIFEST.in +0 -0
  12. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/__main__.py +0 -0
  13. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/app.py +0 -0
  14. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/command_line.py +0 -0
  15. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/__init__.py +0 -0
  16. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/compose.py +0 -0
  17. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/configuration.py +0 -0
  18. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/connection.py +0 -0
  19. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/core.py +0 -0
  20. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/django.py +0 -0
  21. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/images.py +0 -0
  22. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/nginx.py +0 -0
  23. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/postgres.py +0 -0
  24. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/secrets.py +0 -0
  25. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/services.py +0 -0
  26. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/commands/volumes.py +0 -0
  27. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/config.py +0 -0
  28. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/cryptography.py +0 -0
  29. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/environment.py +0 -0
  30. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/extensions/__init__.py +0 -0
  31. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/extensions/django.py +0 -0
  32. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/extensions/nginx.py +0 -0
  33. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/extensions/postgres.py +0 -0
  34. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/helpers.py +0 -0
  35. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/mantis.tpl +0 -0
  36. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis/schema.py +0 -0
  37. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis_cli.egg-info/SOURCES.txt +0 -0
  38. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis_cli.egg-info/dependency_links.txt +0 -0
  39. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis_cli.egg-info/requires.txt +0 -0
  40. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/mantis_cli.egg-info/top_level.txt +0 -0
  41. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/setup.cfg +0 -0
  42. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/setup.py +0 -0
  43. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/tests/__init__.py +0 -0
  44. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/tests/test_build_config.py +0 -0
  45. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/tests/test_command_line.py +0 -0
  46. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/tests/test_config.py +0 -0
  47. {mantis_cli-22.2.0 → mantis_cli-22.3.0}/tests/test_helpers.py +0 -0
  48. {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
1
+ Metadata-Version: 2.4
2
2
  Name: mantis_cli
3
- Version: 22.2.0
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 | Prints logs of containers |
348
+ | logs [CONTAINER\|SERVICE] / l | Prints logs of containers |
329
349
  | networks / n | Prints docker networks |
330
- | healthcheck [CONTAINER] / hc | Execute health-check of container |
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 | Runs bash in container |
337
- | sh CONTAINER | Runs sh in container |
338
- | exec CONTAINER COMMAND... | Executes command in container |
339
- | exec-it CONTAINER COMMAND... | Executes command in container (interactive) |
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 | Prints logs of containers |
306
+ | logs [CONTAINER\|SERVICE] / l | Prints logs of containers |
329
307
  | networks / n | Prints docker networks |
330
- | healthcheck [CONTAINER] / hc | Execute health-check of container |
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 | Runs bash in container |
337
- | sh CONTAINER | Runs sh in container |
338
- | exec CONTAINER COMMAND... | Executes command in container |
339
- | exec-it CONTAINER COMMAND... | Executes command in container (interactive) |
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 docker_command(self, command: str, return_output: bool = False, use_connection: bool = True) -> Optional[str]:
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
- cmd = f'{docker_connection} {command}'
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
- if container not in self.get_containers():
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
- containers = params.split(' ') if params else self.get_containers()
1750
- lines = '--tail 1000 -f' if params else '--tail 10'
1751
- steps = len(containers)
1848
+ if not params:
1849
+ containers = self.get_containers()
1850
+ steps = len(containers)
1752
1851
 
1753
- for index, container in enumerate(containers):
1754
- CLI.step(index + 1, steps, f'{container} logs')
1755
- self.docker(f'logs {container} {lines}')
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 {params} /bin/bash')
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 {params} /bin/sh')
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 | Prints logs of containers |
348
+ | logs [CONTAINER\|SERVICE] / l | Prints logs of containers |
307
349
  | networks / n | Prints docker networks |
308
- | healthcheck [CONTAINER] / hc | Execute health-check of container |
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 | Runs bash in container |
315
- | sh CONTAINER | Runs sh in container |
316
- | exec CONTAINER COMMAND... | Executes command in container |
317
- | exec-it CONTAINER COMMAND... | Executes command in container (interactive) |
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).
@@ -1,3 +1,2 @@
1
1
  [console_scripts]
2
2
  mantis = mantis.command_line:run
3
-
@@ -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