mantis-cli 12.4.0__tar.gz → 13.0.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 (26) hide show
  1. {mantis-cli-12.4.0/mantis_cli.egg-info → mantis-cli-13.0.0}/PKG-INFO +4 -5
  2. mantis-cli-13.0.0/mantis/__init__.py +1 -0
  3. {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis/crypto.py +16 -3
  4. {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis/environment.py +1 -2
  5. {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis/logic.py +3 -3
  6. {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis/managers.py +87 -66
  7. {mantis-cli-12.4.0 → mantis-cli-13.0.0/mantis_cli.egg-info}/PKG-INFO +4 -5
  8. {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis_cli.egg-info/entry_points.txt +1 -0
  9. mantis-cli-12.4.0/mantis/__init__.py +0 -1
  10. {mantis-cli-12.4.0 → mantis-cli-13.0.0}/.gitignore +0 -0
  11. {mantis-cli-12.4.0 → mantis-cli-13.0.0}/LICENSE +0 -0
  12. {mantis-cli-12.4.0 → mantis-cli-13.0.0}/MANIFEST.in +0 -0
  13. {mantis-cli-12.4.0 → mantis-cli-13.0.0}/README.md +0 -0
  14. {mantis-cli-12.4.0 → mantis-cli-13.0.0}/TODO +0 -0
  15. {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis/__main__.py +0 -0
  16. {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis/command_line.py +0 -0
  17. {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis/extensions/__init__.py +0 -0
  18. {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis/extensions/django.py +0 -0
  19. {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis/extensions/nginx.py +0 -0
  20. {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis/extensions/postgres.py +0 -0
  21. {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis/helpers.py +0 -0
  22. {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis_cli.egg-info/SOURCES.txt +0 -0
  23. {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis_cli.egg-info/dependency_links.txt +0 -0
  24. {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis_cli.egg-info/top_level.txt +0 -0
  25. {mantis-cli-12.4.0 → mantis-cli-13.0.0}/setup.cfg +0 -0
  26. {mantis-cli-12.4.0 → mantis-cli-13.0.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 1.2
2
2
  Name: mantis-cli
3
- Version: 12.4.0
3
+ Version: 13.0.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ý
@@ -8,7 +8,9 @@ Author-email: info@pragmaticmates.com
8
8
  Maintainer: Pragmatic Mates
9
9
  Maintainer-email: info@pragmaticmates.com
10
10
  License: GNU General Public License (GPL)
11
+ Description: # mantis-cli
11
12
  Keywords: management deployment docker command
13
+ Platform: UNKNOWN
12
14
  Classifier: Programming Language :: Python
13
15
  Classifier: Operating System :: OS Independent
14
16
  Classifier: Environment :: Web Environment
@@ -16,6 +18,3 @@ Classifier: Intended Audience :: Developers
16
18
  Classifier: Framework :: Django
17
19
  Classifier: License :: OSI Approved :: GNU General Public License (GPL)
18
20
  Classifier: Development Status :: 5 - Production/Stable
19
- License-File: LICENSE
20
-
21
- # mantis-cli
@@ -0,0 +1 @@
1
+ VERSION = '13.0.0'
@@ -10,6 +10,8 @@ except ImportError:
10
10
  # TODO: only if using environment encryption!
11
11
  raise ImportError('Install pycryptodome and cryptography!')
12
12
 
13
+ from mantis.helpers import CLI
14
+
13
15
 
14
16
  class Crypto(object):
15
17
  @staticmethod
@@ -43,9 +45,20 @@ class Crypto(object):
43
45
  @staticmethod
44
46
  def decrypt_deterministically(secret, key):
45
47
  import ast
46
- data = ast.literal_eval(b64decode(secret).decode())
47
- cipher = AES.new(key.strip().encode(), AES.MODE_SIV)
48
- data = cipher.decrypt_and_verify(data['ciphertext'], data['tag'])
48
+
49
+ try:
50
+ data = ast.literal_eval(b64decode(secret).decode())
51
+ except UnicodeDecodeError:
52
+ CLI.error('Decryption failed. Check if data are not corrupted.')
53
+
54
+ try:
55
+ cipher = AES.new(key.strip().encode(), AES.MODE_SIV)
56
+ data = cipher.decrypt_and_verify(data['ciphertext'], data['tag'])
57
+ except ValueError as e:
58
+ if str(e) == 'MAC check failed':
59
+ CLI.error('MAC check failed. You are probably decrypting with incorrect key.')
60
+ CLI.error(str(e))
61
+
49
62
  return data.decode()
50
63
 
51
64
  @staticmethod
@@ -4,10 +4,9 @@ from mantis.helpers import CLI, Colors
4
4
 
5
5
 
6
6
  class Environment(object):
7
- def __init__(self, environment_id, folder, file_prefix):
7
+ def __init__(self, environment_id, folder):
8
8
  self.id = environment_id
9
9
  self.folder = folder
10
- self.file_prefix = file_prefix
11
10
 
12
11
  if self.id:
13
12
  self.setup()
@@ -89,10 +89,10 @@ def main():
89
89
  params = parse_args()
90
90
 
91
91
  # version info
92
- version_info = f'Mantis (v{VERSION})'
92
+ version_info = f'Mantis v{VERSION}'
93
93
 
94
94
  if params['commands'] == ['--version']:
95
- exit(version_info)
95
+ return print(version_info)
96
96
 
97
97
  if len(params['commands']) == 0:
98
98
  CLI.error('Missing commands')
@@ -135,7 +135,7 @@ def main():
135
135
  else:
136
136
  host_intro = ''
137
137
 
138
- heading = f'{version_info} '\
138
+ heading = f'{version_info}, '\
139
139
  f'{environment_intro}'\
140
140
  f'{host_intro}'\
141
141
  f'mode: {Colors.GREEN}{manager.mode}{Colors.ENDC}, '\
@@ -3,6 +3,7 @@ import os
3
3
  import datetime
4
4
  import requests
5
5
  import time
6
+ import yaml
6
7
  from distutils.util import strtobool
7
8
  from os import path
8
9
  from os.path import dirname, normpath
@@ -128,7 +129,6 @@ class BaseManager(object):
128
129
  self.env = Environment(
129
130
  environment_id=self.environment_id,
130
131
  folder=self.config.get('environment', {}).get('folder', 'environments'),
131
- file_prefix=self.config.get('environment', {}).get('file_prefix', '')
132
132
  )
133
133
 
134
134
  # connection
@@ -426,6 +426,15 @@ class BaseManager(object):
426
426
  healthcheck_config = self.get_healthcheck_config(container)
427
427
  return healthcheck_config and healthcheck_config.get('Test')
428
428
 
429
+ def get_healthcheck_start_period(self, container):
430
+ healthcheck_config = self.get_healthcheck_config(container)
431
+
432
+ try:
433
+ return healthcheck_config['StartPeriod'] / 1000000000
434
+ except (KeyError, TypeError):
435
+ # TODO: return default value as fallback?
436
+ return None
437
+
429
438
  def check_health(self, container):
430
439
  if self.has_healthcheck(container):
431
440
  command = f'inspect --format="{{{{json .State.Health.Status}}}}" {container}'
@@ -442,37 +451,48 @@ class BaseManager(object):
442
451
 
443
452
  CLI.info(f'Health-checking {Colors.YELLOW}{container}{Colors.ENDC}...')
444
453
 
445
- if not self.has_healthcheck(container):
446
- CLI.error(f"Container '{container}' doesn't have healthcheck")
447
-
448
- healthcheck_config = self.get_healthcheck_config(container)
449
- coeficient = 10
450
- healthcheck_interval = healthcheck_config['Interval'] / 1000000000
451
- healthcheck_retries = healthcheck_config['Retries']
452
- interval = healthcheck_interval / coeficient
453
- retries = healthcheck_retries * coeficient
454
-
455
- CLI.info(f'Interval: {Colors.FAINT}{healthcheck_interval}{Colors.ENDC} s -> {Colors.YELLOW}{interval} s')
456
- CLI.info(f'Retries: {Colors.FAINT}{healthcheck_retries}{Colors.ENDC} -> {Colors.YELLOW}{retries}')
457
-
458
- start = time.time()
459
-
460
- for retry in range(retries):
461
- is_healthy, status = self.check_health(container)
462
-
463
- if is_healthy:
464
- print(f"#{retry+1}/{retries}: Status of '{container}' is {Colors.GREEN}{status}{Colors.ENDC}.")
465
- end = time.time()
466
- loading_time = end - start
467
- print(f'Container {Colors.YELLOW}{container}{Colors.ENDC} took {Colors.BLUE}{Colors.UNDERLINE}{loading_time} s{Colors.ENDC} to become healthy')
468
- return True
469
- else:
470
- print(f"#{retry+1}/{retries}: Status of '{container}' is {Colors.RED}{status}{Colors.ENDC}.")
471
-
472
- if retries > 1:
473
- sleep(interval)
454
+ if self.has_healthcheck(container):
455
+ healthcheck_config = self.get_healthcheck_config(container)
456
+ coeficient = 10
457
+ healthcheck_interval = healthcheck_config.get('Interval', 1000000000) / 1000000000
458
+ healthcheck_retries = healthcheck_config.get('Retries', 10)
459
+ interval = healthcheck_interval / coeficient
460
+ retries = healthcheck_retries * coeficient
461
+
462
+ CLI.info(f'Interval: {Colors.FAINT}{healthcheck_interval}{Colors.ENDC} s -> {Colors.YELLOW}{interval} s')
463
+ CLI.info(f'Retries: {Colors.FAINT}{healthcheck_retries}{Colors.ENDC} -> {Colors.YELLOW}{retries}')
464
+
465
+ start = time.time()
466
+
467
+ for retry in range(retries):
468
+ is_healthy, status = self.check_health(container)
469
+
470
+ if is_healthy:
471
+ print(f"#{retry + 1}/{retries}: Status of '{container}' is {Colors.GREEN}{status}{Colors.ENDC}.")
472
+ end = time.time()
473
+ loading_time = end - start
474
+ print(f'Container {Colors.YELLOW}{container}{Colors.ENDC} took {Colors.BLUE}{Colors.UNDERLINE}{loading_time} s{Colors.ENDC} to become healthy')
475
+ return True
476
+ else:
477
+ print(f"#{retry + 1}/{retries}: Status of '{container}' is {Colors.RED}{status}{Colors.ENDC}.")
474
478
 
475
- return False
479
+ if retries > 1:
480
+ sleep(interval)
481
+ else:
482
+ CLI.warning(f"Container '{container}' doesn't have healthcheck command defined. Looking for start period value...")
483
+ start_period = self.get_healthcheck_start_period(container)
484
+
485
+ if start_period is None:
486
+ CLI.danger(f"Container '{container}' doesn't have neither healthcheck command or start period defined.")
487
+ CLI.warning(f'Stopping and removing container {container}')
488
+ self.docker(f'container stop {container}')
489
+ self.docker(f'container rm {container}')
490
+ exit()
491
+
492
+ # If container doesn't have healthcheck command, sleep for N seconds
493
+ CLI.info(f'Sleeping for {start_period} seconds...')
494
+ sleep(start_period)
495
+ return None
476
496
 
477
497
  def build(self, params=''):
478
498
  CLI.info(f'Building...')
@@ -490,6 +510,7 @@ class BaseManager(object):
490
510
  CLI.info(f'Args = {build_args}')
491
511
 
492
512
  build_tool = self.config.get('build', {}).get('tool', 'compose')
513
+ available_tools = ['compose', 'docker']
493
514
 
494
515
  if build_tool == 'compose':
495
516
  # Build all services using docker compose
@@ -502,11 +523,9 @@ class BaseManager(object):
502
523
  # Build service using docker
503
524
  self.docker(f"build {info['context']} {build_args} {platform} {image} -f {info['dockerfile']} {params}", use_connection=False)
504
525
  else:
505
- CLI.error(f'Unknown build tool: {build_tool}')
526
+ CLI.error(f'Unknown build tool: {build_tool}. Available tools: {", ".join(available_tools)}')
506
527
 
507
528
  def services_to_build(self):
508
- import yaml
509
-
510
529
  with open(self.compose_file, 'r') as file:
511
530
  compose_data = yaml.safe_load(file)
512
531
 
@@ -544,33 +563,6 @@ class BaseManager(object):
544
563
  if not self.connection:
545
564
  return CLI.warning('Connection not defined. Skipping uploading files')
546
565
 
547
- # TODO: refactor
548
- DATABASE = self.config.get('db', 'postgres')
549
- CACHE = self.config.get('cache', 'redis')
550
- WEBSERVER = self.config.get('webserver', 'nginx')
551
- DATABASE_CONFIG = f'{self.configs_path}/{DATABASE}/{self.env.file_prefix}{self.env.id}.conf'
552
- CACHE_CONFIG = f'{self.configs_path}/{CACHE}/{self.env.file_prefix}{self.env.id}.conf'
553
- WEBSERVER_HTML = f'{self.configs_path}/{WEBSERVER}/html/'
554
- WEBSERVER_CONFIG_PROXY = f'{self.configs_path}/{WEBSERVER}/proxy_directives.conf'
555
- WEBSERVER_CONFIG_DEFAULT = f'{self.configs_path}/{WEBSERVER}/default.conf'
556
- WEBSERVER_CONFIG_SITE = f'{self.configs_path}/{WEBSERVER}/sites/{self.env.file_prefix}{self.env.id}.conf'
557
- HTPASSWD = f'{self.configs_path}/{WEBSERVER}/secrets/.htpasswd'
558
-
559
- mapping = {
560
- # TODO: paths
561
- 'services': {
562
- DATABASE_CONFIG: f'{self.project_path}/configs/{DATABASE}/',
563
- CACHE_CONFIG: f'{self.project_path}/configs/{CACHE}/',
564
- WEBSERVER_HTML: f'{self.project_path}/configs/{WEBSERVER}/html/',
565
- WEBSERVER_CONFIG_DEFAULT: f'{self.project_path}/configs/{WEBSERVER}/',
566
- WEBSERVER_CONFIG_PROXY: f'{self.project_path}/configs/{WEBSERVER}/',
567
- WEBSERVER_CONFIG_SITE: f'{self.project_path}/configs/{WEBSERVER}/sites/',
568
- HTPASSWD: f'{self.project_path}/configs/{WEBSERVER}/secrets/'
569
- }
570
-
571
- # TODO: other contexts
572
- }
573
-
574
566
  if context == 'all':
575
567
  self.upload('services')
576
568
  self.upload('compose')
@@ -591,8 +583,35 @@ class BaseManager(object):
591
583
  else:
592
584
  CLI.info('Uploading...')
593
585
 
594
- # TODO: refactor
595
586
  if context == 'services':
587
+ CLI.danger('DEPRECATED. NOT uploading service files! If you are using custom configs, include them in your custom Docker image instead of copying them to server')
588
+ return
589
+
590
+ # TODO: refactor
591
+ DATABASE = self.config.get('db', 'postgres')
592
+ CACHE = self.config.get('cache', 'redis')
593
+ WEBSERVER = self.config.get('webserver', 'nginx')
594
+ DATABASE_CONFIG = f'{self.configs_path}/{DATABASE}/{self.env.id}.conf'
595
+ CACHE_CONFIG = f'{self.configs_path}/{CACHE}/{self.env.id}.conf'
596
+ WEBSERVER_HTML = f'{self.configs_path}/{WEBSERVER}/html/'
597
+ WEBSERVER_CONFIG_PROXY = f'{self.configs_path}/{WEBSERVER}/proxy_directives.conf'
598
+ WEBSERVER_CONFIG_DEFAULT = f'{self.configs_path}/{WEBSERVER}/default.conf'
599
+ WEBSERVER_CONFIG_SITE = f'{self.configs_path}/{WEBSERVER}/sites/{self.env.id}.conf'
600
+ HTPASSWD = f'{self.configs_path}/{WEBSERVER}/secrets/.htpasswd'
601
+
602
+ mapping = {
603
+ # TODO: paths
604
+ 'services': {
605
+ DATABASE_CONFIG: f'{self.project_path}/configs/{DATABASE}/',
606
+ CACHE_CONFIG: f'{self.project_path}/configs/{CACHE}/',
607
+ WEBSERVER_HTML: f'{self.project_path}/configs/{WEBSERVER}/html/',
608
+ WEBSERVER_CONFIG_DEFAULT: f'{self.project_path}/configs/{WEBSERVER}/',
609
+ WEBSERVER_CONFIG_PROXY: f'{self.project_path}/configs/{WEBSERVER}/',
610
+ WEBSERVER_CONFIG_SITE: f'{self.project_path}/configs/{WEBSERVER}/sites/',
611
+ HTPASSWD: f'{self.project_path}/configs/{WEBSERVER}/secrets/'
612
+ }
613
+ }
614
+
596
615
  for local_path, remote_path in mapping['services'].items():
597
616
  if os.path.exists(local_path):
598
617
  self.cmd(f'rsync -arvz -e \'ssh -p {self.port}\' -rvzh --progress {local_path} {self.user}@{self.host}:{remote_path}')
@@ -661,10 +680,12 @@ class BaseManager(object):
661
680
  return
662
681
 
663
682
  container_prefix = self.get_container_name(service)
664
- old_container = self.get_containers(prefix=container_prefix)[0]
665
683
 
666
- if not self.has_healthcheck(old_container):
667
- CLI.error(f"Container '{old_container}' doesn't have healthcheck")
684
+ try:
685
+ old_container = self.get_containers(prefix=container_prefix)[0]
686
+ except IndexError:
687
+ CLI.danger(f'Old container for service {service} not found. Skipping zero-downtime deployment...')
688
+ return
668
689
 
669
690
  # run new container
670
691
  self.up(f'--no-deps --no-recreate --scale {service}=2')
@@ -871,7 +892,7 @@ class BaseManager(object):
871
892
  try:
872
893
  container_details = json.loads(self.docker(f'container inspect {container}', return_output=True))
873
894
  return container_details[0]["Config"]["Labels"]["com.docker.compose.project"]
874
- except IndexError:
895
+ except (IndexError, KeyError):
875
896
  pass
876
897
 
877
898
  return None
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 1.2
2
2
  Name: mantis-cli
3
- Version: 12.4.0
3
+ Version: 13.0.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ý
@@ -8,7 +8,9 @@ Author-email: info@pragmaticmates.com
8
8
  Maintainer: Pragmatic Mates
9
9
  Maintainer-email: info@pragmaticmates.com
10
10
  License: GNU General Public License (GPL)
11
+ Description: # mantis-cli
11
12
  Keywords: management deployment docker command
13
+ Platform: UNKNOWN
12
14
  Classifier: Programming Language :: Python
13
15
  Classifier: Operating System :: OS Independent
14
16
  Classifier: Environment :: Web Environment
@@ -16,6 +18,3 @@ Classifier: Intended Audience :: Developers
16
18
  Classifier: Framework :: Django
17
19
  Classifier: License :: OSI Approved :: GNU General Public License (GPL)
18
20
  Classifier: Development Status :: 5 - Production/Stable
19
- License-File: LICENSE
20
-
21
- # mantis-cli
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
2
  mantis = mantis.command_line:run
3
+
@@ -1 +0,0 @@
1
- VERSION = '12.4.0'
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes