vjer 30.0.0__py3-none-any.whl

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.
vjer/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ """Vjer CI/CD module.
2
+
3
+ This module provides utilities for making it easier to write CI/CD automations.
4
+
5
+ """
6
+
7
+ __all__ = ('__title__', '__summary__', '__uri__',
8
+ '__version__', '__build_name__', '__build_date__',
9
+ '__author__', '__email__',
10
+ '__license__', '__copyright__')
11
+
12
+ __title__ = 'Vjer'
13
+ __summary__ = 'CI/CD Toolkit'
14
+ __uri__ = 'https://github.com/tardis4500/vjer/'
15
+
16
+ __version__ = '30.0.0'
17
+ __build_name__ = '{var:build_name}'
18
+ __build_date__ = '{var:build_date}'
19
+
20
+ __author__ = 'Jeffery G. Smith'
21
+ __email__ = 'web@pobox.com'
22
+
23
+ __license__ = 'MIT'
24
+ __copyright__ = 'Copyright (c) 2024 Jeffery G. Smith'
25
+
26
+ # cSpell:ignore vjer
vjer/build.py ADDED
@@ -0,0 +1,117 @@
1
+ """This module provides build actions."""
2
+
3
+ # Import standard modules
4
+ from os import getenv
5
+ from pathlib import Path
6
+ from shutil import copyfile
7
+ from tempfile import mkdtemp
8
+ from typing import cast, Optional
9
+
10
+ # Import third-party modules
11
+ from batcave.fileutil import pack
12
+ from batcave.lang import str_to_pythonval
13
+ from batcave.sysutil import rmpath
14
+ from docker.errors import BuildError as DockerBuildError
15
+
16
+ # Import project modules
17
+ from .utils import VJER_ENV, VjerAction, VjerStep, helm
18
+
19
+
20
+ class BuildStep(VjerStep):
21
+ """This class provides build support.
22
+
23
+ Build processing flow:
24
+ pre:
25
+ remove an old artifact directory
26
+ create the artifact directory
27
+ update the version files
28
+ execute:
29
+ run the inheriting build() method for the project type
30
+ post:
31
+ revert the version files
32
+ """
33
+ def pre(self) -> None:
34
+ """This method is run at the start of the build."""
35
+ super().pre()
36
+ if self.step_info.is_first_step:
37
+ self.log_message('Preparing artifact directory', True)
38
+ if Path(self.project.artifacts_dir).exists():
39
+ self.log_message(f'Removing stale artifact directory: {self.project.artifacts_dir}')
40
+ rmpath(self.project.artifacts_dir)
41
+ if not Path(self.project.artifacts_dir).exists():
42
+ self.log_message(f'Creating clean artifact directory: {self.project.artifacts_dir}')
43
+ Path(self.project.artifacts_dir).mkdir(parents=True)
44
+ self.update_version_files()
45
+
46
+ def post(self) -> None:
47
+ """This method is run at the end of the build."""
48
+ super().post()
49
+ self.log_message('Build Completed Successfully', True)
50
+
51
+ def always_post(self) -> None:
52
+ """This method is always run at the end of the build."""
53
+ super().always_post()
54
+ self.update_version_files(reset=True)
55
+
56
+ def create_archive(self, name: str, what: list, /, *, location: Optional[str] = None, arc_type: Optional[str] = None, use_tmpdir: bool = False) -> None: # pylint: disable=too-many-arguments
57
+ """Helper function to create an archive. When complete, the archive is copied to the project build artifact directory.
58
+
59
+ Args:
60
+ name: The archive file name.
61
+ what: The files to include in the archive.
62
+ location (optional, default=None): The location of the files to archive. If None, the current directory is used.
63
+ arc_type (optional, default=None): The type of the archive. If None, is inferred from the file extension.
64
+ use_tmpdir (optional, default=False): If True, create the archive in a temporary directory.
65
+
66
+ Returns:
67
+ Nothing.
68
+ """
69
+ package_dir = Path(mkdtemp()) if use_tmpdir else self.project.artifacts_dir
70
+ package = package_dir / name
71
+ try:
72
+ self.log_message(f'Creating "{package}" archive from: {",".join(what)}', True)
73
+ pack(package, what, item_location=location, archive_type=str(arc_type), ignore_empty=False)
74
+ copyfile(package, self.project.artifacts_dir / name)
75
+ finally:
76
+ if use_tmpdir:
77
+ rmpath(package_dir)
78
+
79
+ def build_docker(self) -> None:
80
+ """Run a Docker build."""
81
+ push_image = str_to_pythonval(getenv('VJER_DOCKER_PUSH', str(not VJER_ENV == 'local')))
82
+ self._docker_init(push_image)
83
+ self.log_message(f'Building docker image: {self.image_tag}', True)
84
+ build_args = {'VERSION': self.project.version,
85
+ 'BUILD_VERSION': self.build.build_version} | self.step_info.build_args
86
+ platform_arg = {'platform': platform} if (platform := getenv('DOCKER_DEFAULT_PLATFORM', '')) else {}
87
+ try:
88
+ log = self.docker_client.client.images.build(rm=True, pull=True, tag=self.image_tag,
89
+ dockerfile=(self.dockerfile),
90
+ buildargs=build_args.toDict(),
91
+ path=str(self.project.project_root),
92
+ **platform_arg)[1]
93
+ error = None
94
+ except DockerBuildError as err:
95
+ error = err
96
+ log = err.build_log
97
+ for line in log:
98
+ if ('stream' in line) and (line['stream'] != '\n'):
99
+ self.log_message(line['stream'].strip())
100
+ if error:
101
+ raise error
102
+ if push_image:
103
+ self.log_message('Pushing image to registry', True)
104
+ self.registry_client.get_image(self.image_tag).push()
105
+
106
+ def build_helm(self) -> None:
107
+ """Build method for Helm charts."""
108
+ helm('dependency', 'build', self.helm_chart_root)
109
+ helm('package', self.helm_chart_root)
110
+ self.copy_artifact(self.helm_package.name)
111
+
112
+
113
+ def build() -> None:
114
+ """This is the main entry point."""
115
+ VjerAction('build', cast(VjerStep, BuildStep)).execute()
116
+
117
+ # cSpell:ignore batcave fileutil pythonval buildargs vjer
vjer/deploy.py ADDED
@@ -0,0 +1,34 @@
1
+ """This module provides deployment actions."""
2
+
3
+ # Import standard module
4
+ from typing import cast
5
+
6
+ # Import project modules
7
+ from .utils import helm, VjerAction, VjerStep
8
+
9
+
10
+ class DeployStep(VjerStep):
11
+ """This class provides deployment support."""
12
+
13
+ def deploy_helm(self) -> None:
14
+ """Deploy method for Helm charts."""
15
+ chart_name = self.step_info.chart_name if self.step_info.chart_name else self.project.product.lower()
16
+ release_name = self.step_info.release_name.lower() if self.step_info.release_name else chart_name
17
+ helm_args = self.helm_args
18
+ is_remote = self.step_info.remote is not False
19
+ if is_remote:
20
+ helm_chart = f'{self.helm_repo.name}/{chart_name}'
21
+ if 'version' not in helm_args:
22
+ helm_args['version'] = self.project.version
23
+ else:
24
+ helm_chart = self.helm_package
25
+ if is_remote:
26
+ helm('repo', 'update')
27
+ helm('upgrade', release_name, helm_chart, install=True, atomic=True, wait=True, **helm_args)
28
+
29
+
30
+ def deploy() -> None:
31
+ """This is the main entry point."""
32
+ VjerAction('deploy', cast(VjerStep, DeployStep)).execute()
33
+
34
+ # cSpell:ignore vjer
vjer/freeze.py ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env python3
2
+ """This module creates a Python freeze file for the project.
3
+
4
+ Attributes:
5
+ PROJECT_ROOT (Path): The root directory of the project.
6
+ FREEZE_FILE_EXT (str): The extension for the frozen requirements file.
7
+ REQUIREMENTS_FILE (Path): The full path of the frozen requirements file.
8
+ FREEZE_FILE_BASE (str): The base name of the frozen requirements file.
9
+ LINUX_MODULES (list): A list of modules that should be limited to the linux platform.
10
+ WINDOWS_MODULES (list): A list of modules that should be limited to the Windows platform.
11
+ """
12
+
13
+ # Import standard modules
14
+ import os
15
+ from pathlib import Path
16
+ from sys import argv
17
+ from tempfile import mkdtemp
18
+ from venv import EnvBuilder
19
+
20
+ # Import BatCave modules
21
+ from batcave.fileutil import slurp, spew
22
+ from batcave.lang import WIN32
23
+ from batcave.sysutil import rmpath, syscmd, SysCmdRunner
24
+
25
+ from .utils import DEFAULT_ENCODING
26
+
27
+ PROJECT_ROOT = Path.cwd()
28
+ FREEZE_FILE_EXT = '.txt'
29
+ REQUIREMENTS_FILE = (PROJECT_ROOT / 'requirements').with_suffix(FREEZE_FILE_EXT)
30
+ FREEZE_FILE_BASE = 'requirements-frozen'
31
+ LINUX_MODULES: list = []
32
+ WINDOWS_MODULES = ['WMI']
33
+
34
+ pip = SysCmdRunner('pip', show_cmd=False, show_stdout=False, syscmd_args={'ignore_stderr': True}).run
35
+
36
+
37
+ def freeze() -> None:
38
+ """Create the requirement-freeze.txt file leaving out the development tools and adding platform specifiers."""
39
+ freeze_file = (PROJECT_ROOT / (FREEZE_FILE_BASE + (f'-{argv[2]}' if (len(argv) > 2) else ''))).with_suffix(FREEZE_FILE_EXT)
40
+ print('Creating virtual environment in:', venv_dir := Path(mkdtemp()))
41
+ EnvBuilder(with_pip=True).create(venv_dir)
42
+ python = ((venv_bin := venv_dir / ('Scripts' if WIN32 else 'bin')) / 'python').with_suffix('.exe' if WIN32 else '')
43
+ os.environ['PATH'] = os.path.pathsep.join((str(venv_bin), os.environ['PATH']))
44
+ print('Upgrading pip')
45
+ syscmd(str(python), '-m', 'pip', 'install', '-qqq', '--upgrade', 'pip')
46
+ print('Updating pip install tools')
47
+ pip('install', '-qqq', 'setuptools', 'wheel', upgrade=True)
48
+ print('Installing modules from', REQUIREMENTS_FILE)
49
+ pip('install', '-qqq', upgrade=True, requirement=REQUIREMENTS_FILE)
50
+ print('Creating frozen requirements file:', freeze_file)
51
+ spew(freeze_file, pip('freeze', requirement=REQUIREMENTS_FILE))
52
+ freeze_file_contents = [line.strip() for line in slurp(freeze_file)]
53
+ with open(freeze_file, 'w', encoding=DEFAULT_ENCODING) as freeze_file_stream:
54
+ for line in freeze_file_contents:
55
+ module = line.split('==')[0]
56
+ if ('win32' in module) or (module in WINDOWS_MODULES):
57
+ line += "; sys_platform == 'win32'"
58
+ if ('ansible' in module) or (module in LINUX_MODULES):
59
+ line += "; sys_platform != 'win32'"
60
+ print(line, file=freeze_file_stream)
61
+ rmpath(venv_dir)
62
+
63
+ # cSpell:ignore batcave fileutil syscmd
vjer/pre_release.py ADDED
@@ -0,0 +1,42 @@
1
+ """This module provides pre-release actions."""
2
+
3
+ # Import standard modules
4
+ from typing import cast
5
+
6
+ # Import project modules
7
+ from .release import ReleaseStep
8
+ from .utils import VjerAction, VjerStep, helm
9
+
10
+
11
+ class PreReleaseStep(ReleaseStep):
12
+ """Provide pre_release support.
13
+
14
+ Attributes:
15
+ is_pre_release: Specifies to the ReleaseStep parent class that this is a pre-release action.
16
+ """
17
+ is_pre_release = True
18
+
19
+ def __init__(self):
20
+ """Sets the project version to a pre-release value."""
21
+ super().__init__()
22
+ self.project.version = f'{self.project.version}-{self.pre_release_num}'
23
+
24
+ def release_helm(self) -> None:
25
+ """Pre-release a Helm chart."""
26
+ if self.helm_repo.type == 'oci':
27
+ self.update_version_files()
28
+ try:
29
+ helm('package', self.helm_chart_root)
30
+ self.copy_artifact(self.helm_package.name)
31
+ finally:
32
+ self.update_version_files(reset=True)
33
+ else:
34
+ list(self.project.artifacts_dir.glob('*.tgz'))[0].rename(self.helm_package)
35
+ super().release_helm()
36
+
37
+
38
+ def pre_release() -> None:
39
+ """This is the main entry point."""
40
+ VjerAction('release', cast(VjerStep, PreReleaseStep)).execute()
41
+
42
+ # cSpell:ignore batcave vjer
vjer/py.typed ADDED
File without changes
vjer/release.py ADDED
@@ -0,0 +1,70 @@
1
+ """This module provides release actions."""
2
+
3
+ # Import standard modules
4
+ from typing import cast
5
+
6
+ # Import third-party modules
7
+ from batcave.cloudmgr import gcloud
8
+
9
+ # Import project modules
10
+ from .utils import helm, VjerAction, VjerStep
11
+
12
+
13
+ class ReleaseStep(VjerStep):
14
+ """Provide release support.
15
+
16
+ Attributes:
17
+ is_pre_release: Specifies that this is not a pre-release action.
18
+ """
19
+ is_pre_release = False
20
+
21
+ def release_docker(self) -> None:
22
+ """Perform a release of a Docker image by tagging."""
23
+ self._docker_init()
24
+ if (registry := self.container_registry).type not in ('gcp', 'jfrog'):
25
+ (image := self.registry_client.get_image(self.image_tag)).pull()
26
+ default_tags = [self.version_tag.lower()]
27
+ if not self.is_pre_release:
28
+ default_tags.append(f'{self.image_name}:latest'.lower())
29
+ for tag in self.step_info.tags if self.step_info.tags else default_tags:
30
+ self.log_message(f'Tagging image: {tag}')
31
+ match registry.type:
32
+ case 'gcp':
33
+ gcloud('container', 'images', 'add-tag', self.image_tag, tag, syscmd_args={'ignore_stderr': True})
34
+ case _:
35
+ image.tag(tag)
36
+ image.push()
37
+
38
+ def release_helm(self) -> None:
39
+ """Perform a release of a Helm chart."""
40
+ helm('push', self.helm_package, self.helm_repo.name, **self.helm_repo.push_args)
41
+
42
+ def release_increment_release(self) -> None:
43
+ """Increment the project release version."""
44
+ if self.is_pre_release:
45
+ self.log_message('Skipping on pre-release')
46
+ return
47
+ if hasattr(self.project, 'version_service'):
48
+ self.log_message('Incrementing version service not supported...skipping')
49
+ return
50
+ version_tuple = self.project.version.split('.')
51
+ version_tuple[len(version_tuple) - 1] = str(int(version_tuple[-1]) + 1)
52
+ new_version = '.'.join(version_tuple)
53
+ use_branch = self.step_info.increment_branch if self.step_info.increment_branch else self.git_client.CI_COMMIT_REF_NAME
54
+ self.project.version = new_version
55
+ self.log_message(f'Incrementing release to {new_version} on branch {use_branch}')
56
+ self.commit_files('Automated pipeline version update check-in [skip ci]', use_branch, self.config.filename, file_updater=self.config.write)
57
+
58
+ def release_tag_source(self) -> None:
59
+ """Tag the source in Git with a release tag."""
60
+ if self.is_pre_release:
61
+ self.log_message('Skipping on pre-release')
62
+ return
63
+ self.tag_source(self.release.release_tag, f'Release {self.release.release_tag}')
64
+
65
+
66
+ def release() -> None:
67
+ """This is the main entry point."""
68
+ VjerAction('release', cast(VjerStep, ReleaseStep)).execute()
69
+
70
+ # cSpell:ignore syscmd batcave cloudmgr vjer
vjer/rollback.py ADDED
@@ -0,0 +1,25 @@
1
+ """This module provides deployment rollback actions."""
2
+
3
+ # Import standard module
4
+ from typing import cast
5
+
6
+ # Import project modules
7
+ from .utils import VjerAction, VjerStep, helm
8
+
9
+
10
+ class RollbackStep(VjerStep):
11
+ """This class provides rollback support."""
12
+
13
+ def rollback_helm(self) -> None:
14
+ """Rollback method for Helm charts."""
15
+ chart_name = self.step_info.chart_name if self.step_info.chart_name else self.project.product.lower()
16
+ helm('rollback',
17
+ self.step_info.release_name.lower() if self.step_info.release_name else chart_name,
18
+ wait=True, **self.helm_args)
19
+
20
+
21
+ def rollback() -> None:
22
+ """This is the main entry point."""
23
+ VjerAction('rollback', cast(VjerStep, RollbackStep)).execute()
24
+
25
+ # cSpell:ignore vjer
vjer/test.py ADDED
@@ -0,0 +1,52 @@
1
+ """This module provides test actions."""
2
+
3
+ # Import standard modules
4
+ from pathlib import Path
5
+ from typing import cast
6
+
7
+ # Import third-party-modules
8
+ from batcave.fileutil import slurp
9
+ from batcave.sysutil import rmpath, syscmd
10
+ from yaml import full_load as yaml_load
11
+
12
+ # Import project modules
13
+ from .utils import DEFAULT_ENCODING, VjerAction, VjerStep, helm
14
+
15
+
16
+ class TestStep(VjerStep):
17
+ """This class provides test support."""
18
+
19
+ def pre(self) -> None:
20
+ """Prepare for testing on first run."""
21
+ super().pre()
22
+ if self.step_info.is_first_step:
23
+ self.log_message('Preparing test results directory', True)
24
+ if Path(self.project.test_results_dir).exists():
25
+ self.log_message(f'Removing test results directory: {self.project.test_results_dir}')
26
+ rmpath(self.project.test_results_dir)
27
+ if not Path(self.project.test_results_dir).exists():
28
+ self.log_message(f'Creating clean test results directory: {self.project.test_results_dir}')
29
+ Path(self.project.test_results_dir).mkdir(parents=True)
30
+
31
+ def test_docker(self) -> None:
32
+ """Lint method for Docker dockerfiles."""
33
+ dockerfile = slurp(self.dockerfile)
34
+ syscmd('docker', 'run', '--interactive', '--rm', 'hadolint/hadolint', input_lines=dockerfile, ignore_stderr=True)
35
+ if self.step_info.build_test_stage:
36
+ self.docker_build(target=self.step_info.build_test_stage)
37
+
38
+ def test_helm(self) -> None:
39
+ """Lint method for Helm charts."""
40
+ helm('dependency', 'build', self.helm_chart_root)
41
+ helm('lint', self.helm_chart_root, **self.helm_args)
42
+ with open(self.helm_chart_root / 'Chart.yaml', encoding=DEFAULT_ENCODING) as yaml_stream:
43
+ helm_info = yaml_load(yaml_stream)
44
+ if helm_info['type'] != 'library':
45
+ helm('template', self.helm_chart_root, **self.helm_args)
46
+
47
+
48
+ def test() -> None:
49
+ """This is the main entry point."""
50
+ VjerAction('test', cast(VjerStep, TestStep)).execute()
51
+
52
+ # cSpell:ignore batcave fileutil syscmd hadolint dockerfiles vjer
vjer/tool_reporter.py ADDED
@@ -0,0 +1,85 @@
1
+ """This program prints the tool information.
2
+
3
+ Attributes:
4
+ PRODUCTS (list): This list of products on which to report.
5
+ """
6
+
7
+ # Import standard modules
8
+ from re import compile as re_compile
9
+
10
+ # Import third-party modules
11
+ from batcave.lang import CommandResult
12
+ from batcave.sysutil import syscmd, CMDError
13
+ from dotmap import DotMap
14
+
15
+ PRODUCTS = [DotMap(name='Docker', regex=re_compile('Docker version (.+)')),
16
+ DotMap(name='Helm', args=['version'], regex=re_compile(r'Version:"v([\d\.]+)"'))]
17
+
18
+
19
+ def tool_reporter() -> dict:
20
+ """Construct the tool report.
21
+
22
+ Returns:
23
+ A dictionary representing the report. There are three members in the dictionary:
24
+ tool_versions: a dictionary of the tools with their versions.
25
+ helm_plugins: a list of the helm plugins.
26
+ helm_repos: a list of the helm repositories.
27
+ """
28
+ tool_info = DotMap(tool_versions={})
29
+ for product in PRODUCTS:
30
+ tool_info.tool_versions[product.name] = get_version(product)
31
+
32
+ tool_info.helm_plugins = get_helm_info('plugin')
33
+ tool_info.helm_repos = get_helm_info('repo')
34
+ return tool_info.toDict()
35
+
36
+
37
+ def get_version(product: DotMap) -> str | list:
38
+ """Determine the version for the specified product.
39
+
40
+ Args:
41
+ product: The product for which the version should be returned.
42
+
43
+ Returns:
44
+ The version of the specified product.
45
+ """
46
+ version_command = product.command if product.command else product.name.lower()
47
+ version_args = product.args if product.args else ['--version']
48
+ version_info: CommandResult = []
49
+ try:
50
+ version_info = syscmd(version_command, *version_args, ignore_stderr=True, append_stderr=True)
51
+ except FileNotFoundError:
52
+ pass
53
+ except CMDError as err:
54
+ if not (('not found' in str(err)) or ('not be found' in str(err)) or ('command could not be loaded' in str(err))):
55
+ raise
56
+ if product.raw:
57
+ return version_info
58
+ return version[1] if (version := product.regex.search(' '.join([line.strip() for line in version_info]))) else 'Not Found'
59
+
60
+
61
+ def get_helm_info(info_type: str) -> dict:
62
+ """Return the requested Helm info.
63
+
64
+ Args:
65
+ info_type: The type of helm info to return.
66
+
67
+ Returns:
68
+ A dictionary of the requested info.
69
+ """
70
+ helm_info = {}
71
+ try:
72
+ for line in syscmd('helm', info_type, 'list', ignore_stderr=True):
73
+ if line.startswith('NAME'):
74
+ continue
75
+ (name, url) = line.split()[0:2]
76
+ helm_info[name] = url
77
+ except FileNotFoundError:
78
+ helm_info['Helm'] = 'not installed'
79
+ except CMDError as err:
80
+ if 'no repositories to show' not in ''.join(err.vars['err_lines']):
81
+ raise
82
+ helm_info['found'] = 'none'
83
+ return helm_info if helm_info else {'found': 'none'}
84
+
85
+ # cSpell:ignore batcave syscmd dotmap
vjer/utils.py ADDED
@@ -0,0 +1,575 @@
1
+ """Module to hold common values and utility functions for CI/CD support scripts.
2
+
3
+ Attributes:
4
+ PROJECT_CFG_FILE (str): The name of the project config file.
5
+ TOOL_REPORT (Path): The path of the tool report.
6
+
7
+ RELEASE_PRE_STEPS (list): The list of release steps to perform before the ones specified for the project.
8
+ RELEASE_POST_STEPS (list): The list of release steps to perform before the ones specified for the project.
9
+
10
+ There are several tool runners defined for simplified usage: git, helm.
11
+ """
12
+ # Import standard modules
13
+ from copy import deepcopy as copy_object
14
+ from datetime import datetime
15
+ from os import getenv
16
+ from pathlib import Path
17
+ from random import randint
18
+ from shutil import copy, copyfile, copytree
19
+ from stat import S_IWUSR
20
+ from string import Template
21
+ from sys import exit as sys_exit, stderr
22
+ from typing import Callable, cast, Optional
23
+
24
+ # Import third-party modules
25
+ from batcave.automation import Action
26
+ from batcave.cloudmgr import Cloud, CloudType
27
+ from batcave.cms import Client, ClientType
28
+ from batcave.expander import Expander, file_expander
29
+ from batcave.lang import BatCaveError, BatCaveException, PathName, DEFAULT_ENCODING, WIN32, yaml_to_dotmap
30
+ from batcave.platarch import Platform
31
+ from batcave.sysutil import CMDError, SysCmdRunner, syscmd
32
+ from dotmap import DotMap
33
+ from yaml import safe_dump as yaml_dump, safe_load as yaml_load
34
+
35
+ # Import project modules
36
+ from . import __title__, __version__, __build_name__, __build_date__
37
+ from .tool_reporter import tool_reporter
38
+
39
+ _CONFIG_SECTIONS = ('project', 'test', 'build', 'deploy', 'rollback', 'release')
40
+ _PROJECT_DEFAULTS = DotMap(build_artifacts='artifacts',
41
+ build_num_var='VJER_BUILD_NUM',
42
+ chart_root='helm-chart',
43
+ container_registry=DotMap(type='local', name=''),
44
+ dockerfile='Dockerfile',
45
+ gcp_artifact_region='us',
46
+ test_results='test_results')
47
+ _VALID_SCHEMAS = [1]
48
+
49
+ PROJECT_CFG_FILE = getenv('VJER_CFG', 'vjer.yml')
50
+ TOOL_REPORT = Path(__file__).parent.absolute() / 'tool_report.yml'
51
+
52
+ HELM_CHART_FILE = 'Chart.yaml'
53
+ DEFAULT_VERSION_FILES = {'helm': [HELM_CHART_FILE, 'values.yaml']}
54
+
55
+ RELEASE_PRE_STEPS = ['tag_source']
56
+ RELEASE_POST_STEPS = ['increment_release']
57
+
58
+ VJER_ENV = getenv('VJER_ENV', 'local')
59
+ REMOTE_REF = 'vjer_origin'
60
+
61
+ apt = SysCmdRunner('apt-get', '-y').run
62
+ apt_install = SysCmdRunner('apt-get', '-y', 'install', no_install_recommends=True).run
63
+ git = SysCmdRunner('git').run
64
+ helm = SysCmdRunner('helm', syscmd_args={'ignore_stderr': True}).run
65
+ pip_install = SysCmdRunner('pip', 'install', quiet=True, no_cache_dir=True, upgrade=True).run
66
+
67
+
68
+ class ConfigurationError(BatCaveException):
69
+ """Configuration errors.
70
+
71
+ Attributes:
72
+ BAD_FORMAT: The format of the configuration file is bad.
73
+ CONFIG_FILE_NOT_FOUND: The config file was not found.
74
+ INVALID_SCHEMA: The schema of the configuration file is not supported.
75
+ """
76
+ BAD_FORMAT = BatCaveError(1, Template('Invalid format for configuration file: $file'))
77
+ CONFIG_FILE_NOT_FOUND = BatCaveError(2, Template('Configuration file not found: $file'))
78
+ INVALID_SCHEMA = BatCaveError(3, Template('Invalid configuration schema: found=$found, expected=$expected'))
79
+
80
+
81
+ class StepError(BatCaveException):
82
+ """Step errors.
83
+
84
+ Attributes:
85
+ UNKNOWN_OBJECT: The specified object is of an unknown type.
86
+ """
87
+ UNKNOWN_OBJECT = BatCaveError(1, Template('Unknown $type: $name'))
88
+
89
+
90
+ class Environment: # pylint: disable=too-few-public-methods
91
+ """Provides an interface to the environment variables."""
92
+
93
+ def __getattr__(self, attr: str):
94
+ value = getenv(attr)
95
+ if value is None:
96
+ raise AttributeError(f'Environment variable not found: {attr}')
97
+ return value
98
+
99
+
100
+ class GitClient(Environment):
101
+ """Provides an interface to the Git server environment and API."""
102
+
103
+ def __init__(self, project_id: Optional[str] = None, client_root: Optional[PathName] = None, branch: Optional[str] = None):
104
+ """
105
+ Args:
106
+ project_id (optional, default=None): The ID of the Git project.
107
+ client_root (optional, default=None): The root directory of the project.
108
+ branch (optional, default=None): The branch to use for the Git client.
109
+
110
+ Attributes:
111
+ CI_REMOTE_REF: The full git reference to the project.
112
+ branch: The value of the branch argument.
113
+ client: The git client for the project.
114
+ project_id: The value of the project_id argument.
115
+ """
116
+ self.project_id = project_id
117
+ self.client = Client(ClientType.git, 'vjer', connect_info=str(client_root), create=False) if (client_root and (Path(client_root) / '.git').exists()) else None
118
+ self.branch = branch if branch else getattr(self, 'CI_COMMIT_BRANCH', '')
119
+
120
+ def __enter__(self):
121
+ return self
122
+
123
+ def __exit__(self, *_unused_exc_info):
124
+ return False
125
+
126
+
127
+ class ConfigSection():
128
+ """Base class to manage configuration sections."""
129
+
130
+ def __init__(self, **defaults):
131
+ """
132
+ Args:
133
+ **defaults: The default items for the configuration.
134
+
135
+ Attributes:
136
+ _default_property_holders: A list of property holders for defaults.
137
+ _defaults: The configuration defaults.
138
+ _expander: The expander to use for variable replacement.
139
+ _values: The configuration values.
140
+ """
141
+ self._values = DotMap()
142
+ self._defaults = DotMap(**defaults)
143
+ self._default_property_holders = [Environment()]
144
+ self._expander = None
145
+ self.update_expander(property_holders=self._default_property_holders)
146
+
147
+ def __getattr__(self, attr: str):
148
+ for config in (self._values, self._defaults):
149
+ if attr in config:
150
+ value = getattr(config, attr)
151
+ if isinstance(value, list):
152
+ return [self._expander.expand(v) for v in value]
153
+ if isinstance(value, dict):
154
+ return DotMap({k: self._expander.expand(v) for (k, v) in value.items()})
155
+ if isinstance(value, str):
156
+ return self._expander.expand(value)
157
+ return value
158
+ raise AttributeError(f'No configuration value found: {attr}')
159
+
160
+ def __setattr__(self, attr: str, value: str):
161
+ if attr.startswith('_'):
162
+ super().__setattr__(attr, value)
163
+ return
164
+ setattr(self._values, attr, value)
165
+
166
+ values = property(lambda s: s._values.toDict(), doc='A read-only property which returns the configuration values.')
167
+
168
+ def update_expander(self, *, property_holders: Optional[list] = None, property_dict: Optional[dict] = None) -> None:
169
+ """Set the expander property holders.
170
+
171
+ Args:
172
+ property_holders (optional, default=None): A list of property holders from which to read values.
173
+ property_dict (optional, default=None): A dictionary from which to read values.
174
+
175
+ Returns:
176
+ Nothing.
177
+ """
178
+ if property_holders:
179
+ self._expander = Expander(var_props=property_holders + self._default_property_holders)
180
+ if property_dict:
181
+ self._expander.var_dict |= property_dict
182
+
183
+ def update(self, values: dict | DotMap, /) -> None:
184
+ """Update the configuration section values.
185
+
186
+ Args:
187
+ values: Update the configuration values from the provided dictionary.
188
+
189
+ Returns:
190
+ Nothing.
191
+ """
192
+ self._values |= values
193
+
194
+ def update_defaults(self, values: dict | DotMap, /) -> None:
195
+ """Updates the configuration section default values.
196
+
197
+ Args:
198
+ values: Update the defaults with the provided dictionary.
199
+
200
+ Returns:
201
+ Nothing.
202
+ """
203
+ self._defaults |= values
204
+
205
+
206
+ class ProjectConfig():
207
+ """Stores project related configuration items."""
208
+
209
+ def __init__(self):
210
+ """
211
+ Attributes:
212
+ _config_file: The config file for the project configuration.
213
+ _sections: A list of the sections in the project configuration.
214
+ schema: The schema version of the project configuration.
215
+ use_steps: A dictionary of steps by section.
216
+ """
217
+ project_root = Path.cwd()
218
+ self._sections = {'project': ConfigSection(**(DotMap(project_root=project_root) | _PROJECT_DEFAULTS)),
219
+ 'test': ConfigSection(),
220
+ 'deploy': ConfigSection(clean=True),
221
+ 'rollback': ConfigSection(clean=True),
222
+ 'release': ConfigSection()}
223
+ self._sections['build'] = ConfigSection(source_dir=self.project.project_root / 'src',
224
+ version_files=[],
225
+ artifacts={},
226
+ build_date=str(datetime.now()),
227
+ platform=Platform().bart)
228
+ self._config_file = self.project.project_root / PROJECT_CFG_FILE
229
+ self._load_config()
230
+ self._set_defaults()
231
+ self._set_version()
232
+
233
+ for section in _CONFIG_SECTIONS:
234
+ self._sections[section].update_expander(property_holders=list(self._sections.values()))
235
+
236
+ release_steps = [self._get_phase_step('release', s) for s in RELEASE_PRE_STEPS]
237
+ if hasattr(self.release, 'steps'):
238
+ release_steps += [copy_object(s) for s in self.release.steps if s.get('type') not in RELEASE_PRE_STEPS + RELEASE_POST_STEPS]
239
+ release_steps += [self._get_phase_step('release', s) for s in RELEASE_POST_STEPS]
240
+ self.use_steps = {'release': release_steps}
241
+
242
+ def __getattr__(self, attr: str):
243
+ if attr not in self._sections:
244
+ raise AttributeError(f'Configuration section not found: {attr}')
245
+ return self._sections[attr]
246
+
247
+ def _get_phase_step(self, phase: str, step_type: str) -> DotMap:
248
+ """Get the specified step type for the specified phase.
249
+
250
+ Args:
251
+ phase: The phase for which to return the step.
252
+ step_type: The step type to return from the phase.
253
+
254
+ Returns:
255
+ A DotMap for the step.
256
+ """
257
+ if hasattr(phase_ref := getattr(self, phase), 'steps'):
258
+ for step in phase_ref.steps:
259
+ if step.get('type') == step_type:
260
+ return copy_object(step)
261
+ return DotMap(type=step_type)
262
+
263
+ def _load_config(self) -> None:
264
+ if not self._config_file.exists():
265
+ raise ConfigurationError(ConfigurationError.CONFIG_FILE_NOT_FOUND, file=self._config_file)
266
+ yaml_as_dict = DotMap(schema=0)
267
+ with open(self._config_file, encoding=DEFAULT_ENCODING) as config_file:
268
+ yaml_as_dict |= DotMap(yaml_load(config_file))
269
+ if not yaml_as_dict:
270
+ raise ConfigurationError(ConfigurationError.BAD_FORMAT, file=self._config_file)
271
+ if yaml_as_dict.schema not in _VALID_SCHEMAS:
272
+ raise ConfigurationError(ConfigurationError.INVALID_SCHEMA, found=yaml_as_dict.schema, expected=_VALID_SCHEMAS)
273
+ self.schema = yaml_as_dict.schema
274
+ for section in _CONFIG_SECTIONS:
275
+ if section in yaml_as_dict:
276
+ self._sections[section].update(yaml_as_dict[section])
277
+
278
+ def _set_defaults(self) -> None:
279
+ self.project.update_defaults(DotMap(artifacts_dir=self.project.project_root / self.project.build_artifacts,
280
+ test_results_dir=self.project.project_root / self.project.test_results))
281
+ if hasattr(self.project, 'artifact_repo'):
282
+ if hasattr(self.project, 'docker_repo'):
283
+ self.project.update_defaults(DotMap(container_registry=DotMap(type='local',
284
+ name=f'{self.project.gcp_artifact_region}-docker.pkg.dev/{self.project.artifact_repo}/{self.project.docker_repo}')))
285
+ if hasattr(self.project, 'helm_repo'):
286
+ self.project.update_defaults(DotMap(chart_repo=DotMap(type='oci',
287
+ name=f'oci://{self.project.gcp_artifact_region}-docker.pkg.dev/{self.project.artifact_repo}/{self.project.helm_repo}')))
288
+
289
+ def _set_version(self) -> None:
290
+ if hasattr(self.project, 'version_service'):
291
+ self._set_version_from_service()
292
+ build_num = getenv(self.project.build_num_var, '0')
293
+ build_version = f'{self.project.version}-{build_num}'
294
+ self.build.update_defaults(DotMap(build_num=build_num,
295
+ build_version=build_version,
296
+ build_version_msbuild=f'{self.project.version}.{build_num}',
297
+ build_name=f'{self.project.product}_{build_version}'))
298
+ self.release.update_defaults(DotMap(release_tag=f'v{self.project.version}'))
299
+ for (piece, index) in {'major': 0, 'minor': 1, 'patch': 2}.items():
300
+ self.project.update_defaults({f'{piece}': self.project.version.split('.', 2)[index]})
301
+
302
+ def _set_version_from_service(self) -> None:
303
+ version_service = DotMap(self.project.version_service)
304
+ match version_service.type:
305
+ case 'environment':
306
+ self.project.version = getenv(version_service.variable, '').rstrip('.')
307
+ case 'semver':
308
+ self.project.version = version_service.value
309
+ case _:
310
+ print('Unknown version service:', version_service.type, file=stderr)
311
+ sys_exit(1)
312
+
313
+ filename = property(lambda s: s._config_file, doc='A read-only property which returns the configuration file name.')
314
+
315
+ def write(self) -> None:
316
+ """Writes out the project configuration.
317
+
318
+ Returns:
319
+ Nothing.
320
+ """
321
+ with open(self._config_file, 'w', encoding=DEFAULT_ENCODING) as config_file:
322
+ yaml_dump({'schema': self.schema} | {s: c.values for (s, c) in self._sections.items() if c.values},
323
+ config_file, indent=2)
324
+
325
+
326
+ class VjerStep(Action): # pylint: disable=too-many-instance-attributes
327
+ """ Class to represent a single Action step."""
328
+
329
+ def __init__(self):
330
+ """
331
+ Attributes:
332
+ build: The project build configuration.
333
+ config: The project configuration.
334
+ docker_client: The Docker client.
335
+ git_client: The Git repository for the project.
336
+ image_name: The Docker image name.
337
+ image_tag: The Docker image tag.
338
+ pre_release_num: The pre-release suffix for the project.
339
+ project: The project name.
340
+ registry_client: The Docker image registry.
341
+ release: The release configuration.
342
+ step_info: The step information.
343
+ version_tag: The Docker image version.
344
+ """
345
+ super().__init__()
346
+ self.config = ProjectConfig()
347
+ self.project = self.config.project
348
+ self.build = self.config.build
349
+ self.release = self.config.release
350
+ self.git_client = GitClient(client_root=self.project.project_root)
351
+ self.step_info = DotMap()
352
+ self.pre_release_num = self.build.build_num
353
+ # Used by Docker builds
354
+ self.registry_client = None
355
+ self.docker_client = None
356
+ self.image_name = ''
357
+ self.version_tag = ''
358
+ self.image_tag = ''
359
+
360
+ def __getattr__(self, attr: str):
361
+ if not hasattr(self.project, attr):
362
+ raise AttributeError(f'No such attribute: {attr}')
363
+ return getattr(self.step_info, attr) if getattr(self.step_info, attr) else getattr(self.project, attr)
364
+
365
+ def _docker_init(self, login: bool = True) -> None:
366
+ """Perform Docker initialization.
367
+
368
+ Args:
369
+ login (optional, default=True): If True, login to the Docker image registry.
370
+ mode (optional, default='pull'): The mode for which this initialization will be used.
371
+
372
+ Returns:
373
+ Nothing.
374
+ """
375
+ self.registry_client = Cloud(CloudType[self.container_registry.type], login=login)
376
+ self.docker_client = Cloud(CloudType.local)
377
+ registry_name_path = f'{self.container_registry.name}/' if login else ''
378
+ self.image_name = f'{registry_name_path}{self.step_info.image if self.step_info.image else self.project.product}'
379
+ self.version_tag = f'{self.image_name}:{self.project.version}'
380
+ self.image_tag = f'{self.version_tag}-{self.build.build_num}'.lower()
381
+
382
+ def _execute(self) -> None:
383
+ """This method is called by the Action super class when this class's execute method is called."""
384
+ class_name = type(self).__name__.lower().removeprefix('pre').removesuffix('step')
385
+ action_method = f'{class_name}_{self.step_info.type}'
386
+ if not hasattr(self, action_method):
387
+ print(f'No method defined for action type {self.step_info.type} on {class_name}', file=stderr)
388
+ sys_exit(1)
389
+ getattr(self, action_method)()
390
+
391
+ helm_chart_root = property(lambda s: Path(s.chart_root), doc='A read-only property which returns the helm chart root.')
392
+ helm_package = property(lambda s: s.project.artifacts_dir / (s.pkg_name.lower() + '.tgz'), doc='A read-only property which returns the helm chart package name.')
393
+ pkg_name = property(lambda s: f'{s.step_info.pkg_name if s.step_info.pkg_name else s.project.product}-{s.project.version}',
394
+ doc='A read-only property which returns the release package name.')
395
+
396
+ @property
397
+ def helm_args(self) -> dict:
398
+ """A read-only property which returns the Helm command arguments."""
399
+ helm_args = self.step_info.helm_args if self.step_info.helm_args else {}
400
+ if self.step_info.values_files:
401
+ helm_args['values'] = ','.join([str(self.project.artifacts_dir / v) for v in self.step_info.values_files])
402
+ if self.step_info.helm_variables:
403
+ helm_args['set'] = ','.join([f'{k}={v}' for (k, v) in self.step_info.helm_variables.items()])
404
+ return helm_args
405
+
406
+ @property
407
+ def helm_repo(self) -> DotMap:
408
+ """A read-only property which returns the Helm repo name with a randomized suffix."""
409
+ if (self.chart_repo.type != 'oci') and self.chart_repo.url and not self.chart_repo.name:
410
+ repo_name = f'vjer-{randint(0, 100)}'
411
+ helm('repo', 'add', repo_name, self.chart_repo.url)
412
+ helm('repo', 'update')
413
+ self.chart_repo.name = repo_name
414
+ return self.chart_repo
415
+
416
+ def commit_files(self, message: str, branch: str, *files, file_updater: Optional[Callable] = None) -> None:
417
+ """Checkin files during to the source repository."""
418
+ self.git_client.client.add_remote_ref(remote_ref := REMOTE_REF, self.git_client.CI_REMOTE_REF, exists_ok=True)
419
+ git('fetch', all=True, syscmd_args={'ignore_stderr': True})
420
+ git('remote', 'update', remote_ref, prune=True, syscmd_args={'ignore_stderr': True})
421
+ git('checkout', '-B', branch, '--track', f'{remote_ref}/{branch}', syscmd_args={'ignore_stderr': True})
422
+ git('pull', remote_ref, branch, syscmd_args={'ignore_stderr': True})
423
+ if file_updater:
424
+ file_updater()
425
+ self.git_client.client.add_files(*files)
426
+ self.git_client.client.checkin_files(message, remote=remote_ref, all_branches=False)
427
+
428
+ def copy_artifact(self, src: PathName, dest: Optional[str] = None, /) -> None:
429
+ """Helper method to copy artifacts to the location expected by the continuous integration tool.
430
+
431
+ Args:
432
+ src: The source of the artifact to copy.
433
+ dest (optional, default=None): The directory to which to copy the artifact.
434
+ If not absolute, this will be a subdirectory of the project archive directory.
435
+
436
+ Returns:
437
+ Nothing.
438
+ """
439
+ use_dest = self.project.artifacts_dir
440
+ if dest:
441
+ if Path(dest).is_absolute():
442
+ use_dest = Path(dest)
443
+ else:
444
+ use_dest /= dest
445
+ self.log_message(f'Copying "{src}" to "{use_dest}"')
446
+ if not Path(src).is_dir():
447
+ copy(src, use_dest)
448
+ return
449
+
450
+ if WIN32:
451
+ try:
452
+ syscmd('robocopy', str(src), str(use_dest), '/S', '/DCOPY:D', '/COPY:D', '/MT', '/R:3', '/W:30', '/TBD', '/NP', '/NFL', show_stdout=True, ignore_stderr=True, append_stderr=True)
453
+ except CMDError as err:
454
+ if err.vars['returncode'] != 1:
455
+ raise
456
+ return
457
+
458
+ copytree(str(src), str(use_dest), dirs_exist_ok=True)
459
+
460
+ def tag_source(self, tag: str, label: Optional[str] = None) -> None:
461
+ """Tag the source in Git.
462
+
463
+ Args:
464
+ tag: The label which which to tag the source.
465
+ label (optional, default=None): The annotation to apply to the tag.
466
+
467
+ Returns:
468
+ Nothing.
469
+ """
470
+ self.log_message('Removing local tags and adding remote')
471
+ for local_tag in [t.strip() for t in git('tag', list=True)]:
472
+ git('tag', local_tag, delete=True)
473
+ self.git_client.client.add_remote_ref(REMOTE_REF, self.git_client.CI_REMOTE_REF, exists_ok=True)
474
+ self.log_message(f'Tagging the source with {tag}')
475
+ self.git_client.client.add_label(tag, label, exists_ok=True)
476
+ self.git_client.client.checkin_files('Automated pipeline tag check-in [skip ci]', remote=REMOTE_REF, tags=True, all_branches=False)
477
+
478
+ def update_version_files(self, *, reset: bool = False) -> None: # pylint: disable=too-many-branches
479
+ """Updates the version files for the project.
480
+
481
+ Args:
482
+ reset (optional, default=False): If True, reset the files.
483
+
484
+ Returns:
485
+ Nothing.
486
+ """
487
+ verb = 'Resetting' if reset else 'Updating'
488
+
489
+ if not self.step_info.version_files:
490
+ self.step_info.version_files = []
491
+
492
+ match self.step_info.type:
493
+ case 'helm':
494
+ prefix = self.helm_chart_root
495
+ case 'python_module':
496
+ prefix = self.python_module_source
497
+ case _:
498
+ prefix = Path('.')
499
+
500
+ if self.step_info.type in DEFAULT_VERSION_FILES:
501
+ self.step_info.version_files += [prefix / v for v in DEFAULT_VERSION_FILES[self.step_info.type] if (prefix / v).exists()]
502
+
503
+ if not self.step_info.version_files:
504
+ return
505
+
506
+ self.log_message(f'{verb} version files', True)
507
+ for file_name in self.step_info.version_files:
508
+ file_path = Path(file_name)
509
+ msg = f'{verb}: {file_path}'
510
+ file_orig = Path(str(file_path) + '.orig')
511
+ if reset:
512
+ if file_orig.exists():
513
+ self.log_message(msg)
514
+ if file_path.exists():
515
+ file_path.unlink()
516
+ file_orig.rename(file_path)
517
+ else:
518
+ self.log_message(msg)
519
+ file_path.chmod(file_path.stat().st_mode | S_IWUSR)
520
+ copyfile(file_path, file_orig)
521
+ file_expander(file_orig, file_path, var_props=(self.project, self.build, self.step_info))
522
+ if file_path.name == HELM_CHART_FILE:
523
+ with open(file_path, encoding=DEFAULT_ENCODING) as yaml_stream:
524
+ helm_info = yaml_load(yaml_stream)
525
+ helm_info['version'] = self.project.version
526
+ if self.step_info.set_app_version:
527
+ helm_info['appVersion'] = self.project.version
528
+ with open(file_path, 'w', encoding=DEFAULT_ENCODING) as yaml_stream:
529
+ yaml_dump(helm_info, yaml_stream)
530
+
531
+
532
+ class VjerAction: # pylint: disable=too-few-public-methods
533
+ """This is a base class to build CI/CD support scripts using the BatCave automation module Action class."""
534
+ def __init__(self, action_type: str, action_step_class: VjerStep):
535
+ """
536
+ Args:
537
+ action_type: The action type.
538
+ action_step_class: The Class to use for the action.
539
+
540
+ Attributes:
541
+ action_step_class: The value of the action_step_class argument.
542
+ action_type: The value of the action_type argument.
543
+ config: The project configuration.
544
+ """
545
+ self.config = ProjectConfig()
546
+ self.action_type = action_type
547
+ self.action_step_class = action_step_class
548
+
549
+ def execute(self) -> None:
550
+ """Run the action."""
551
+ VjerStep().log_message(f'Starting {__title__} version {__version__} ({__build_name__}) [{__build_date__}]', True)
552
+ for (category, info) in (yaml_to_dotmap(TOOL_REPORT) if TOOL_REPORT.exists() else tool_reporter()).items():
553
+ VjerStep().log_message(category.replace('_', ' ').title(), True)
554
+ for (name, data) in info.items():
555
+ VjerStep().log_message(f' {name}: {data}')
556
+ steps = []
557
+ if self.action_type in self.config.use_steps:
558
+ steps = self.config.use_steps[self.action_type]
559
+ elif hasattr(action_def := getattr(self.config, self.action_type), 'steps'):
560
+ steps = action_def.steps
561
+ if not steps:
562
+ return
563
+
564
+ is_first_step = True
565
+ for step in [DotMap(s) for s in steps]:
566
+ step.is_first_step = is_first_step
567
+ verb = 'Skipping' if step.ignore else 'Executing'
568
+ VjerStep().log_message(f'{verb} {self.action_type} step: {step.type if (not step.name) else step.name}', True)
569
+ if step.ignore:
570
+ continue
571
+ (executor := cast(Callable, self.action_step_class)()).step_info = step
572
+ executor.execute()
573
+ is_first_step = False
574
+
575
+ # cSpell:ignore batcave cloudmgr dotmap platarch syscmd vjer checkin
vjer/vjer.py ADDED
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env python3
2
+ """This is the bootstrap program for running the Vjer actions.
3
+ Since this program can be called before the requirements are install, it can only import standard modules.
4
+ """
5
+
6
+ # Import standard modules
7
+ from importlib import import_module
8
+ import os
9
+ from os import getenv
10
+ from platform import uname
11
+ from sys import exit as sys_exit, stderr, version as python_version
12
+
13
+ # Import third-party modules
14
+ from batcave.commander import Argument, Commander
15
+ from batcave.fileutil import slurp
16
+ from batcave.sysutil import syscmd
17
+
18
+ # Import local modules
19
+ from .utils import apt, apt_install, VJER_ENV, pip_install, ProjectConfig, ConfigurationError, PROJECT_CFG_FILE
20
+
21
+ ACTIONS = ['test', 'build', 'deploy', 'rollback', 'pre_release', 'release', 'freeze']
22
+
23
+
24
+ def main() -> None:
25
+ """The main entrypoint."""
26
+ args = Commander('Vjer CI/CD Automation Tool', [Argument('actions', choices=ACTIONS, nargs='+')]).parse_args()
27
+ _setup_environment()
28
+
29
+ match uname().system:
30
+ case 'Darwin':
31
+ syscmd('sw_vers', show_stdout=True)
32
+ case 'Linux':
33
+ print(slurp('/etc/os-release'))
34
+ case _:
35
+ print('Unknown OS:', uname().system, file=stderr)
36
+ sys_exit(1)
37
+ print(f'Python version: {python_version}')
38
+
39
+ _sys_initialize()
40
+ for action in args.actions:
41
+ action_module = import_module(f'vjer.{action}')
42
+ action_module.__dict__[action]()
43
+
44
+
45
+ def _pip_setup() -> None:
46
+ pip_install('pip')
47
+ pip_install('setuptools', 'wheel')
48
+
49
+
50
+ def _setup_environment() -> None:
51
+ try:
52
+ config = ProjectConfig()
53
+ except ConfigurationError as err:
54
+ if err.code != ConfigurationError.CONFIG_FILE_NOT_FOUND.code:
55
+ raise
56
+ print('The Vjer configuration file was not found:', PROJECT_CFG_FILE, file=stderr)
57
+ sys_exit(1)
58
+ if (VJER_ENV == 'local') and not getenv('VIRTUAL_ENV', ''):
59
+ print('Vjer must be run from a virtual environment.', file=stderr)
60
+ sys_exit(1)
61
+ if hasattr((config := ProjectConfig()).project, 'environment'):
62
+ for (var, val) in config.project.environment.items():
63
+ print(f'setting {var}={val}')
64
+ os.environ[var] = val # putenv doesn't work because the values are needed for this process.
65
+
66
+
67
+ def _sys_initialize() -> None:
68
+ if pkg_installs := getenv('VJER_PKG_INSTALLS', ''):
69
+ apt('update')
70
+ apt_install(pkg_installs)
71
+
72
+ if (pip_installs := getenv('VJER_PIP_INSTALLS', '')) or (pip_file := getenv('VJER_PIP_INSTALL_FILE', '')):
73
+ _pip_setup()
74
+ if pip_installs:
75
+ pip_install(pip_installs)
76
+ if pip_file:
77
+ pip_install(requirement=pip_file)
78
+
79
+
80
+ if __name__ == '__main__':
81
+ main()
82
+
83
+ # cSpell:ignore batcave vjer fileutil syscmd putenv
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Jeffery G. Smith
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.1
2
+ Name: vjer
3
+ Version: 30.0.0
4
+ Summary: Vjer CI/CD module.
5
+ Keywords: python,programming,utilities
6
+ Author-email: "Jeffery G. Smith" <web@pobox.com>
7
+ Requires-Python: ~=3.11
8
+ Description-Content-Type: text/markdown
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Topic :: Software Development
15
+ Classifier: Natural Language :: English
16
+ Requires-Dist: BatCave~=43.0
17
+ Requires-Dist: flit ; extra == "dev"
18
+ Requires-Dist: twine ; extra == "dev"
19
+ Requires-Dist: bumpver ; extra == "dev"
20
+ Requires-Dist: flake8 ; extra == "test"
21
+ Requires-Dist: flake8-annotations ; extra == "test"
22
+ Requires-Dist: flake8-pyproject ; extra == "test"
23
+ Requires-Dist: mypy ; extra == "test"
24
+ Requires-Dist: pylint ; extra == "test"
25
+ Requires-Dist: types-PyYAML ; extra == "test"
26
+ Requires-Dist: types-requests ; extra == "test"
27
+ Project-URL: changelog, https://github.com/tardis4500/vjer/blob/main/CHANGELOG.md
28
+ Project-URL: documentation, https://vjer.readthedocs.io
29
+ Project-URL: homepage, https://github.com/tardis4500/vjer/
30
+ Project-URL: repository, https://github.com/tardis4500/vjer/
31
+ Provides-Extra: dev
32
+ Provides-Extra: test
33
+
34
+ # Vjer Python Module
35
+
36
+ A module for supporting CI/CD actions.
37
+
@@ -0,0 +1,17 @@
1
+ vjer/__init__.py,sha256=FrXoaBnL5D8B1mMZc8Jgjqvsp8ZfuZuDrOYuzGVVY8Q,654
2
+ vjer/build.py,sha256=854HRKHQ_mp0e_vYDyhM1qX0-4zdzX5ZIYunlXsKkdo,5108
3
+ vjer/deploy.py,sha256=LhIGI0thrPnLMw3cz2L_V55Msy6G7eb8PjL8UiPvlrI,1188
4
+ vjer/freeze.py,sha256=ZdkMi_jd94SW2fQVpBJh4vzZFOmrHsjMyOafFrQYWfk,2875
5
+ vjer/pre_release.py,sha256=3Gp_xLnUx2oa_p3C5bTBhhg_VlxeBFh0kQ9xaRVoPv0,1287
6
+ vjer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ vjer/release.py,sha256=INWn5abLc3MQJegV295PbDD8__XcOX9Y7Tvvc5SBU0o,2874
8
+ vjer/rollback.py,sha256=Q5cKzPw1N1e7dgHVcTWJeEyqrR3kbVwzpqjQJi84GYU,772
9
+ vjer/test.py,sha256=yGG20CV6rqyxyIAIPpApqRWwQqHa6hcW2yprbVTPK2M,2106
10
+ vjer/tool_reporter.py,sha256=zhHoIC9k9K6GWFDruxNIYJmeyNQTLqipeyFCrsjadGs,2946
11
+ vjer/utils.py,sha256=bIRH4AyLcW4_JgpzxOSzsyShfl7aqh9EcdJD3fSnXRI,25994
12
+ vjer/vjer.py,sha256=xs1fcuC2sJ6iHpa4k63CMlY4aAGkQr_Pa7P5qA1RSjA,2760
13
+ vjer-30.0.0.dist-info/entry_points.txt,sha256=3clcPT0_-UMmTL5ByOMNWsdREutds6SOWp2rIJoIrtk,39
14
+ vjer-30.0.0.dist-info/LICENSE,sha256=X5r54-6S0TyVYhMGqPxqC6FK-WVFXfXqbO_8iR51HRg,1072
15
+ vjer-30.0.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
16
+ vjer-30.0.0.dist-info/METADATA,sha256=Cr3l30DjxmCYFQhyN_EB5e1qAFHSyj_RB4-wR5v4ddU,1380
17
+ vjer-30.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ vjer=vjer.vjer:main
3
+