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.
- {mantis-cli-12.4.0/mantis_cli.egg-info → mantis-cli-13.0.0}/PKG-INFO +4 -5
- mantis-cli-13.0.0/mantis/__init__.py +1 -0
- {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis/crypto.py +16 -3
- {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis/environment.py +1 -2
- {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis/logic.py +3 -3
- {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis/managers.py +87 -66
- {mantis-cli-12.4.0 → mantis-cli-13.0.0/mantis_cli.egg-info}/PKG-INFO +4 -5
- {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis_cli.egg-info/entry_points.txt +1 -0
- mantis-cli-12.4.0/mantis/__init__.py +0 -1
- {mantis-cli-12.4.0 → mantis-cli-13.0.0}/.gitignore +0 -0
- {mantis-cli-12.4.0 → mantis-cli-13.0.0}/LICENSE +0 -0
- {mantis-cli-12.4.0 → mantis-cli-13.0.0}/MANIFEST.in +0 -0
- {mantis-cli-12.4.0 → mantis-cli-13.0.0}/README.md +0 -0
- {mantis-cli-12.4.0 → mantis-cli-13.0.0}/TODO +0 -0
- {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis/__main__.py +0 -0
- {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis/command_line.py +0 -0
- {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis/extensions/__init__.py +0 -0
- {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis/extensions/django.py +0 -0
- {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis/extensions/nginx.py +0 -0
- {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis/extensions/postgres.py +0 -0
- {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis/helpers.py +0 -0
- {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis_cli.egg-info/SOURCES.txt +0 -0
- {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis_cli.egg-info/dependency_links.txt +0 -0
- {mantis-cli-12.4.0 → mantis-cli-13.0.0}/mantis_cli.egg-info/top_level.txt +0 -0
- {mantis-cli-12.4.0 → mantis-cli-13.0.0}/setup.cfg +0 -0
- {mantis-cli-12.4.0 → mantis-cli-13.0.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2
|
|
1
|
+
Metadata-Version: 1.2
|
|
2
2
|
Name: mantis-cli
|
|
3
|
-
Version:
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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
|
|
92
|
+
version_info = f'Mantis v{VERSION}'
|
|
93
93
|
|
|
94
94
|
if params['commands'] == ['--version']:
|
|
95
|
-
|
|
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
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
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
|
-
|
|
667
|
-
|
|
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
|
+
Metadata-Version: 1.2
|
|
2
2
|
Name: mantis-cli
|
|
3
|
-
Version:
|
|
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 +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
|
|
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
|