diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5c1518f..18694c4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,6 +4,9 @@ before_script: - python --version - python -m pip install --upgrade pip - pip install -r requirements.txt + - export IMAGE_TAG=$CI_IMAGE_TAG + - export IMAGE_DOCKERHUB_USER=&DOCKERHUB_USER + - export IMAGE_DOCKERHUB_PASSWORD=$DOCKERHUB_PASSWORD stages: - lint&test diff --git a/build.py b/build.py index b767497..38c97cf 100644 --- a/build.py +++ b/build.py @@ -28,7 +28,7 @@ use_plugin("python.distutils") default_task = "publish" name = "ddadevops" -version = "4.0.0-dev16" +version = "4.0.0-dev41" summary = "tools to support builds combining gopass, terraform, dda-pallet, aws & hetzner-cloud" description = __doc__ authors = [Author("meissa GmbH", "buero@meissa-gmbh.de")] diff --git a/doc/architecture/Domain.md b/doc/architecture/Domain.md index dd5687f..9a59a4c 100644 --- a/doc/architecture/Domain.md +++ b/doc/architecture/Domain.md @@ -3,25 +3,26 @@ ```mermaid classDiagram class Devops { - stage + <> name - project_root_path module + stage build_dir_name + project_root_path } class Image { - dockerhub_user - dockerhub_password - build_dir_name - use_package_common_files - build_commons_path - docker_build_commons_dir_name - docker_publish_tag + image_dockerhub_user + image_dockerhub_password + image_publish_tag + image_build_dir_name + image_use_package_common_files + image_build_commons_path + image_build_commons_dir_name } class C4k { - executabel_name + c4k_executabel_name c4k_mixin_config c4k_mixin_auth } @@ -33,18 +34,49 @@ classDiagram } class Release { - main_branch - config_file - } - class ReleaseContext { release_type + release_main_branch + release_current_branch version - current_branch + } + class Credentials { + <> + } + class CredentialMapping { + name + gopass_path + gopass_field + gopass_type() + name_for_input() + name_for_environment () } + + class BuildFile { + <> + file_path [id] + content + build_file_type() + get_version() + set_version(version) + } + + class Version { + to_string() + create_major() + create_minor() + create_patch() + create_bump(snapshot_suffix) + } + + Devops *-- "0..1" Image: spcialized_builds + Devops *-- "0..1" C4k: spcialized_builds + Devops *-- "0..1" Release: mixins + Release o-- "0..1" BuildFile: primary_build_file + Release o-- "0..n" BuildFile: secondary_build_files + BuildFile *-- "1" Version C4k *-- DnsRecord - Image *-- Devops - Release *-- "0..1" ReleaseContext + Credentials *-- "0..n" CredentialMapping: mappings ``` diff --git a/infrastructure/clojure/build.py b/infrastructure/clojure/build.py index 515334b..c3d89d2 100644 --- a/infrastructure/clojure/build.py +++ b/infrastructure/clojure/build.py @@ -3,38 +3,25 @@ from pybuilder.core import task, init from ddadevops import * name = "clojure" -MODULE = "docker" +MODULE = "image" PROJECT_ROOT_PATH = "../.." @init def initialize(project): + + input = { + "name": name, + "module": MODULE, + "stage": "notused", + "project_root_path": PROJECT_ROOT_PATH, + "build_types": ["IMAGE"], + "mixin_types": [], + } + project.build_depends_on("ddadevops>=4.0.0-dev") - stage = "notused" - dockerhub_user = environ.get("DOCKERHUB_USER") - if not dockerhub_user: - dockerhub_user = gopass_field_from_path("meissa/web/docker.com", "login") - dockerhub_password = environ.get("DOCKERHUB_PASSWORD") - if not dockerhub_password: - dockerhub_password = gopass_password_from_path("meissa/web/docker.com") - tag = environ.get("CI_COMMIT_TAG") - if not tag: - tag = get_tag_from_latest_commit() - - devops = Devops( - stage=stage, - project_root_path=PROJECT_ROOT_PATH, - module=MODULE, - name=name, - ) - image = Image( - dockerhub_user=dockerhub_user, - dockerhub_password=dockerhub_password, - docker_publish_tag=tag, - devops=devops, - ) - - build = DevopsImageBuild(project, image=image) + + build = DevopsImageBuild(project, input) build.initialize_build_dir() diff --git a/infrastructure/devops-build/build.py b/infrastructure/devops-build/build.py index 488d160..4dd891e 100644 --- a/infrastructure/devops-build/build.py +++ b/infrastructure/devops-build/build.py @@ -3,38 +3,25 @@ from pybuilder.core import task, init from ddadevops import * name = "devops-build" -MODULE = "docker" +MODULE = "image" PROJECT_ROOT_PATH = "../.." @init def initialize(project): + + input = { + "name": name, + "module": MODULE, + "stage": "notused", + "project_root_path": PROJECT_ROOT_PATH, + "build_types": ["IMAGE"], + "mixin_types": [], + } + project.build_depends_on("ddadevops>=4.0.0-dev") - stage = "notused" - dockerhub_user = environ.get("DOCKERHUB_USER") - if not dockerhub_user: - dockerhub_user = gopass_field_from_path("meissa/web/docker.com", "login") - dockerhub_password = environ.get("DOCKERHUB_PASSWORD") - if not dockerhub_password: - dockerhub_password = gopass_password_from_path("meissa/web/docker.com") - tag = environ.get("CI_COMMIT_TAG") - if not tag: - tag = get_tag_from_latest_commit() - - devops = Devops( - stage=stage, - project_root_path=PROJECT_ROOT_PATH, - module=MODULE, - name=name, - ) - image = Image( - dockerhub_user=dockerhub_user, - dockerhub_password=dockerhub_password, - docker_publish_tag=tag, - devops=devops, - ) - - build = DevopsImageBuild(project, image=image) + + build = DevopsImageBuild(project, input) build.initialize_build_dir() diff --git a/requirements.txt b/requirements.txt index 7a391b0..c53d0df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ setuptools dda-python-terraform==2.0.1 packaging boto3 -pyyaml \ No newline at end of file +pyyaml +inflection \ No newline at end of file diff --git a/src/main/python/ddadevops/__init__.py b/src/main/python/ddadevops/__init__.py index 3cd3560..e73b38f 100644 --- a/src/main/python/ddadevops/__init__.py +++ b/src/main/python/ddadevops/__init__.py @@ -9,7 +9,7 @@ from .provs_k3s_mixin import ProvsK3sMixin, add_provs_k3s_mixin_config from .aws_rds_pg_mixin import AwsRdsPgMixin, add_aws_rds_pg_mixin_config from .aws_mfa_mixin import AwsMfaMixin, add_aws_mfa_mixin_config from .aws_backend_properties_mixin import AwsBackendPropertiesMixin, add_aws_backend_properties_mixin_config -from .c4k_mixin import C4kBuild, add_c4k_mixin_config +from .c4k_build import C4kBuild, add_c4k_mixin_config from .exoscale_mixin import ExoscaleMixin, add_exoscale_mixin_config from .digitalocean_backend_properties_mixin import DigitaloceanBackendPropertiesMixin, add_digitalocean_backend_properties_mixin_config from .digitalocean_terraform_build import DigitaloceanTerraformBuild, create_digitalocean_terraform_build_config @@ -20,6 +20,4 @@ from .devops_build import DevopsBuild, create_devops_build_config, get_devops_bu from .credential import gopass_password_from_path, gopass_field_from_path from .release_mixin import ReleaseMixin -from .domain import Validateable, DnsRecord, Devops, Image, Release, ReleaseContext - __version__ = "${version}" diff --git a/src/main/python/ddadevops/application/__init__.py b/src/main/python/ddadevops/application/__init__.py index 6159750..9f46f1a 100644 --- a/src/main/python/ddadevops/application/__init__.py +++ b/src/main/python/ddadevops/application/__init__.py @@ -1,2 +1,2 @@ from .image_build_service import ImageBuildService -from .release_mixin_services import TagAndPushReleaseService, PrepareReleaseService +from .release_mixin_services import ReleaseService diff --git a/src/main/python/ddadevops/application/image_build_service.py b/src/main/python/ddadevops/application/image_build_service.py index a1fd210..811131a 100644 --- a/src/main/python/ddadevops/application/image_build_service.py +++ b/src/main/python/ddadevops/application/image_build_service.py @@ -1,54 +1,66 @@ -from src.main.python.ddadevops.domain import Image -from src.main.python.ddadevops.infrastructure import FileApi, ResourceApi, ImageApi +from ..domain import Image, Devops, BuildType +from ..infrastructure import FileApi, ResourceApi, ImageApi class ImageBuildService: - def __init__(self): - self.file_api = FileApi() - self.resource_api = ResourceApi() - self.docker_api = ImageApi() + def __init__(self, file_api: FileApi, resource_api: ResourceApi, image_api: ImageApi): + self.file_api = file_api + self.resource_api = resource_api + self.image_api = image_api - def __copy_build_resource_file_from_package__(self, resource_name, docker: Image): + @classmethod + def prod(cls): + return cls( + FileApi(), + ResourceApi(), + ImageApi(), + ) + + def __copy_build_resource_file_from_package__(self, resource_name, devops: Devops): data = self.resource_api.read_resource(f"src/main/resources/docker/{resource_name}") self.file_api.write_data_to_file( - f"{docker.devops.build_path()}/{resource_name}", data + f"{devops.build_path()}/{resource_name}", data ) - def __copy_build_resources_from_package__(self, docker: Image): + def __copy_build_resources_from_package__(self, devops: Devops): self.__copy_build_resource_file_from_package__( - "image/resources/install_functions.sh", docker + "image/resources/install_functions.sh", devops ) - def __copy_build_resources_from_dir__(self, docker: Image): + def __copy_build_resources_from_dir__(self, devops: Devops): + image = devops.specialized_builds[BuildType.IMAGE] self.file_api.cp_force( - docker.docker_build_commons_path(), docker.devops.build_path() + image.build_commons_path(), devops.build_path() ) - def initialize_build_dir(self, docker: Image): - build_path = docker.devops.build_path() + def initialize_build_dir(self, devops: Devops): + image = devops.specialized_builds[BuildType.IMAGE] + build_path = devops.build_path() self.file_api.clean_dir(f"{build_path}/image/resources") - if docker.use_package_common_files: - self.__copy_build_resources_from_package__(docker) + if image.image_use_package_common_files: + self.__copy_build_resources_from_package__(devops) else: - self.__copy_build_resources_from_dir__(docker) + self.__copy_build_resources_from_dir__(devops) self.file_api.cp_recursive("image", build_path) self.file_api.cp_recursive("test", build_path) - def image(self, docker: Image): - self.docker_api.image(docker.devops.name, docker.devops.build_path()) + def image(self, devops: Devops): + self.image_api.image(devops.name, devops.build_path()) - def drun(self, docker: Image): - self.docker_api.drun(docker.devops.name) + def drun(self, devops: Devops): + self.image_api.drun(devops.name) - def dockerhub_login(self, docker: Image): - self.docker_api.dockerhub_login( - docker.dockerhub_user, docker.dockerhub_password + def dockerhub_login(self, devops: Devops): + image = devops.specialized_builds[BuildType.IMAGE] + self.image_api.dockerhub_login( + image.image_dockerhub_user, image.image_dockerhub_password ) - def dockerhub_publish(self, docker: Image): - self.docker_api.dockerhub_publish( - docker.devops.name, docker.dockerhub_user, docker.docker_publish_tag + def dockerhub_publish(self, devops: Devops): + image = devops.specialized_builds[BuildType.IMAGE] + self.image_api.dockerhub_publish( + devops.name, image.image_dockerhub_user, image.image_tag ) - def test(self, docker: Image): - self.docker_api.test(docker.devops.name, docker.devops.build_path()) + def test(self, devops: Devops): + self.image_api.test(devops.name, devops.build_path()) diff --git a/src/main/python/ddadevops/application/release_mixin_services.py b/src/main/python/ddadevops/application/release_mixin_services.py index 6b33e52..5a9565a 100644 --- a/src/main/python/ddadevops/application/release_mixin_services.py +++ b/src/main/python/ddadevops/application/release_mixin_services.py @@ -1,35 +1,61 @@ -from src.main.python.ddadevops.infrastructure.release_mixin import ReleaseContextRepository, VersionRepository, GitApi -from src.main.python.ddadevops.domain import Version, Release +from typing import Optional, List +from pathlib import Path +from ..infrastructure import GitApi, BuildFileRepository +from ..domain import Version, Release, ReleaseType -class PrepareReleaseService(): - - def __init__(self): - self.git_api = GitApi() - - def __write_and_commit_version(self, release: Release, version_repository: VersionRepository, version: Version, commit_message: str): - release.is_valid() - - version_repository.write_file(version.get_version_string()) - self.git_api.add_file(version_repository.file) - self.git_api.commit(commit_message) - - def write_and_commit_release(self, release: Release, version_repository: VersionRepository): - self.__write_and_commit_version(release, version_repository, release.release_version(), commit_message=f'Release v{release.release_version().get_version_string()}') - - def write_and_commit_bump(self, release: Release, version_repository: VersionRepository): - self.__write_and_commit_version(release, version_repository, release.bump_version(), commit_message='Version bump') - -class TagAndPushReleaseService(): - - def __init__(self, git_api: GitApi, main_branch: str): +class ReleaseService: + def __init__(self, git_api: GitApi, build_file_repository: BuildFileRepository): self.git_api = git_api - self.main_branch = main_branch + self.build_file_repository = build_file_repository - def tag_release(self, release_repo: ReleaseContextRepository): - annotation = 'v' + release_repo.get_release(self.main_branch).version.get_version_string() - message = 'Release ' + annotation - self.git_api.tag_annotated_second_last(annotation, message) + @classmethod + def prod(cls, base_dir: str): + return cls( + GitApi(), + BuildFileRepository(base_dir), + ) - def push_release(self): + def prepare_release(self, release: Release): + match release.release_type: + case ReleaseType.MAJOR: + version = release.version.create_major() + case ReleaseType.MINOR: + version = release.version.create_minor() + case ReleaseType.PATCH: + version = release.version.create_patch() + case ReleaseType.NONE: + return + message = f"release: {version.to_string()}" + self.__set_version_and_commit__(version, release.build_files(), message) + + def tag_bump_and_push_release(self, release: Release): + match release.release_type: + case ReleaseType.MAJOR: + release_version = release.version.create_major() + case ReleaseType.MINOR: + release_version = release.version.create_minor() + case ReleaseType.PATCH: + release_version = release.version.create_patch() + case ReleaseType.NONE: + return + bump_version = release_version.create_bump("SNAPSHOT") + release_message = f"release: {release_version.to_string()}" + bump_message = f"bump version to: {bump_version.to_string()}" + self.git_api.tag_annotated(release_version.to_string(), release_message, 0) + self.__set_version_and_commit__( + bump_version, + release.build_files(), + bump_message, + ) self.git_api.push() + + def __set_version_and_commit__( + self, version: Version, build_file_ids: List[str], message: str + ): + for id in build_file_ids: + build_file = self.build_file_repository.get(Path(id)) + build_file.set_version(version) + self.build_file_repository.write(build_file) + self.git_api.add_file(build_file.file_path) + self.git_api.commit(message) diff --git a/src/main/python/ddadevops/c4k_mixin.py b/src/main/python/ddadevops/c4k_build.py similarity index 54% rename from src/main/python/ddadevops/c4k_mixin.py rename to src/main/python/ddadevops/c4k_build.py index 8a5f3f2..7964917 100644 --- a/src/main/python/ddadevops/c4k_mixin.py +++ b/src/main/python/ddadevops/c4k_build.py @@ -1,17 +1,16 @@ import deprecation -from .domain import C4k, DnsRecord +from .domain import BuildType, DnsRecord from .devops_build import DevopsBuild from .credential import gopass_field_from_path, gopass_password_from_path from .infrastructure import ExecutionApi -@deprecation.deprecated(deprecated_in="3.2") -# create objects direct instead +@deprecation.deprecated(deprecated_in="3.2", details="use direct dict instead") def add_c4k_mixin_config( config, c4k_config_dict, c4k_auth_dict, - executabel_name=None, + executable_name=None, grafana_cloud_user=None, grafana_cloud_password=None, grafana_cloud_url="https://prometheus-prod-01-eu-west-0.grafana.net/api/prom/push", @@ -36,7 +35,7 @@ def add_c4k_mixin_config( config.update( { "C4kMixin": { - "executabel_name": executabel_name, + "executable_name": executable_name, "config": c4k_config_dict, "auth": c4k_auth_dict, } @@ -48,27 +47,31 @@ class C4kBuild(DevopsBuild): def __init__(self, project, config): super().__init__(project, config) self.execution_api = ExecutionApi() - c4k_build = C4k(config) - self.repo.set_c4k(self.project, c4k_build) + devops = self.devops_repo.get_devops(self.project) + if BuildType.C4K not in devops.specialized_builds: + raise ValueError(f"C4kBuild requires BuildType.C4K") def update_runtime_config(self, dns_record: DnsRecord): - c4k_build = self.repo.get_c4k(self.project) - c4k_build.update_runtime_config(dns_record) - self.repo.set_c4k(self.project, c4k_build) + devops = self.devops_repo.get_devops(self.project) + devops.specialized_builds[BuildType.C4K].update_runtime_config(dns_record) + self.devops_repo.set_devops(self.project, devops) def write_c4k_config(self): - build = self.repo.get_devops(self.project) - c4k_build = self.repo.get_c4k(self.project) - path = build.build_path() + "/out_c4k_config.yaml" - self.file_api.write_yaml_to_file(path, c4k_build.config()) + devops = self.devops_repo.get_devops(self.project) + path = devops.build_path() + "/out_c4k_config.yaml" + self.file_api.write_yaml_to_file( + path, devops.specialized_builds[BuildType.C4K].config() + ) def write_c4k_auth(self): - build = self.repo.get_devops(self.project) - c4k_build = self.repo.get_c4k(self.project) - path = build.build_path() + "/out_c4k_auth.yaml" - self.file_api.write_yaml_to_file(path, c4k_build.c4k_mixin_auth) + devops = self.devops_repo.get_devops(self.project) + path = devops.build_path() + "/out_c4k_auth.yaml" + self.file_api.write_yaml_to_file( + path, devops.specialized_builds[BuildType.C4K].auth() + ) def c4k_apply(self, dry_run=False): - build = self.repo.get_devops(self.project) - c4k_build = self.repo.get_c4k(self.project) - return self.execution_api.execute(c4k_build.command(build), dry_run) + devops = self.devops_repo.get_devops(self.project) + return self.execution_api.execute( + devops.specialized_builds[BuildType.C4K].command(devops), dry_run + ) diff --git a/src/main/python/ddadevops/credential.py b/src/main/python/ddadevops/credential.py index 467e5f2..12c6735 100644 --- a/src/main/python/ddadevops/credential.py +++ b/src/main/python/ddadevops/credential.py @@ -1,15 +1,24 @@ +import deprecation from .python_util import execute -def gopass_field_from_path (path, field): + +@deprecation.deprecated( + deprecated_in="3.2", details="use infrastructure.CredentialsApi instead" +) +def gopass_field_from_path(path, field): credential = None if path and field: - print('get field for: ' + path + ', ' + field) - credential = execute(['gopass', 'show', path, field]) + print("get field for: " + path + ", " + field) + credential = execute(["gopass", "show", path, field]) return credential -def gopass_password_from_path (path): + +@deprecation.deprecated( + deprecated_in="3.2", details="use infrastructure.CredentialsApi instead" +) +def gopass_password_from_path(path): credential = None if path: - print('get password for: ' + path) - credential = execute(['gopass', 'show', '--password', path]) + print("get password for: " + path) + credential = execute(["gopass", "show", "--password", path]) return credential diff --git a/src/main/python/ddadevops/devops_build.py b/src/main/python/ddadevops/devops_build.py index a239ae2..f2622a9 100644 --- a/src/main/python/ddadevops/devops_build.py +++ b/src/main/python/ddadevops/devops_build.py @@ -1,7 +1,7 @@ from typing import Optional import deprecation -from .domain import Devops -from .infrastructure import ProjectRepository, FileApi +from .domain import Devops, InitService +from .infrastructure import DevopsRepository, FileApi @deprecation.deprecated(deprecated_in="3.2", details="create objects direct instead") @@ -15,36 +15,29 @@ def create_devops_build_config( "build_dir_name": build_dir_name, } + def get_devops_build(project): - return project.get_property("devops_build") + return project.get_property("build") + class DevopsBuild: - def __init__(self, project, config: Optional[dict] = None, devops: Optional[Devops] = None): + def __init__(self, project, input: dict): self.project = project self.file_api = FileApi() - self.repo = ProjectRepository() - if not devops: - if not config: - raise ValueError("Build parameters could not be set!") - devops = Devops( - stage=config["stage"], - project_root_path=config["project_root_path"], - module=config["module"], - name=project.name, - build_dir_name=config["build_dir_name"], - ) - - self.repo.set_devops(self.project, devops) - self.repo.set_build(self.project, self) + self.init_service = InitService.prod(project.basedir) + self.devops_repo = DevopsRepository() + devops = self.init_service.initialize(input) + self.devops_repo.set_devops(self.project, devops) + self.project.set_property("build", self) def name(self): - devops = self.repo.get_devops(self.project) + devops = self.devops_repo.get_devops(self.project) return devops.name def build_path(self): - devops = self.repo.get_devops(self.project) + devops = self.devops_repo.get_devops(self.project) return devops.build_path() def initialize_build_dir(self): - devops = self.repo.get_devops(self.project) + devops = self.devops_repo.get_devops(self.project) self.file_api.clean_dir(devops.build_path()) diff --git a/src/main/python/ddadevops/devops_image_build.py b/src/main/python/ddadevops/devops_image_build.py index a5e6e65..c5684bb 100644 --- a/src/main/python/ddadevops/devops_image_build.py +++ b/src/main/python/ddadevops/devops_image_build.py @@ -1,11 +1,11 @@ from typing import Optional import deprecation -from .domain import Image +from .domain import BuildType from .application import ImageBuildService from .devops_build import DevopsBuild, create_devops_build_config -@deprecation.deprecated(deprecated_in="3.2", details="create objects direct instead") +@deprecation.deprecated(deprecated_in="3.2", details="use direct dict instead") def create_devops_docker_build_config( stage, project_root_path, @@ -33,46 +33,34 @@ def create_devops_docker_build_config( class DevopsImageBuild(DevopsBuild): - def __init__(self, project, config: Optional[dict] = None, image: Optional[Image] = None): - self.image_build_service = ImageBuildService() - if not image: - if not config: - raise ValueError("Image parameters could not be set.") - super().__init__(project, config=config) - image = Image( - dockerhub_user=config["dockerhub_user"], - dockerhub_password=config["dockerhub_password"], - devops=self.repo.get_devops(project), - use_package_common_files=config["use_package_common_files"], - build_commons_path=config["build_commons_path"], - docker_build_commons_dir_name=config["docker_build_commons_dir_name"], - docker_publish_tag=config["docker_publish_tag"], - ) - else: - super().__init__(project, devops=image.devops) - self.repo.set_docker(self.project, image) + def __init__(self, project, input: dict): + super().__init__(project, input) + self.image_build_service = ImageBuildService.prod() + devops = self.devops_repo.get_devops(self.project) + if BuildType.IMAGE not in devops.specialized_builds: + raise ValueError(f"ImageBuild requires BuildType.IMAGE") def initialize_build_dir(self): super().initialize_build_dir() - image = self.repo.get_docker(self.project) - self.image_build_service.initialize_build_dir(image) + devops = self.devops_repo.get_devops(self.project) + self.image_build_service.initialize_build_dir(devops) def image(self): - image = self.repo.get_docker(self.project) - self.image_build_service.image(image) + devops = self.devops_repo.get_devops(self.project) + self.image_build_service.image(devops) def drun(self): - image = self.repo.get_docker(self.project) - self.image_build_service.drun(image) + devops = self.devops_repo.get_devops(self.project) + self.image_build_service.drun(devops) def dockerhub_login(self): - image = self.repo.get_docker(self.project) - self.image_build_service.dockerhub_login(image) + devops = self.devops_repo.get_devops(self.project) + self.image_build_service.dockerhub_login(devops) def dockerhub_publish(self): - image = self.repo.get_docker(self.project) - self.image_build_service.dockerhub_publish(image) + devops = self.devops_repo.get_devops(self.project) + self.image_build_service.dockerhub_publish(devops) def test(self): - image = self.repo.get_docker(self.project) - self.image_build_service.test(image) + devops = self.devops_repo.get_devops(self.project) + self.image_build_service.test(devops) diff --git a/src/main/python/ddadevops/domain/__init__.py b/src/main/python/ddadevops/domain/__init__.py index 2e4f6b1..f55faa3 100644 --- a/src/main/python/ddadevops/domain/__init__.py +++ b/src/main/python/ddadevops/domain/__init__.py @@ -1,4 +1,9 @@ -from .common import Validateable, DnsRecord, Devops +from .common import Validateable, DnsRecord, Devops, BuildType, MixinType, ReleaseType +from .devops_factory import DevopsFactory from .image import Image from .c4k import C4k -from .release import Release, ReleaseContext, ReleaseType, Version, EnvironmentKeys +from .release import Release +from .credentials import Credentials, CredentialMapping, GopassType +from .version import Version +from .build_file import BuildFileType, BuildFile +from .init_service import InitService \ No newline at end of file diff --git a/src/main/python/ddadevops/domain/build_file.py b/src/main/python/ddadevops/domain/build_file.py new file mode 100644 index 0000000..c69a737 --- /dev/null +++ b/src/main/python/ddadevops/domain/build_file.py @@ -0,0 +1,128 @@ +from enum import Enum +from typing import Optional +from pathlib import Path +import re +import json +from .common import ( + Validateable, + Devops, + ReleaseType, +) +from .version import ( + Version, +) + + +class BuildFileType(Enum): + JS = ".json" + JAVA_GRADLE = ".gradle" + JAVA_CLOJURE = ".clj" + PYTHON = ".py" + + +class BuildFile(Validateable): + def __init__(self, file_path: Path, content: str): + self.file_path = file_path + self.content = content + + def validate(self): + result = [] + result += self.__validate_is_not_empty__("file_path") + result += self.__validate_is_not_empty__("content") + if not self.build_file_type(): + result += [f"Suffix {self.file_path} is unknown."] + return result + + def build_file_type(self): + if not self.file_path: + return None + config_file_type = self.file_path.suffix + match config_file_type: + case ".json": + result = BuildFileType.JS + case ".gradle": + result = BuildFileType.JAVA_GRADLE + case ".clj": + result = BuildFileType.JAVA_CLOJURE + case ".py": + result = BuildFileType.PYTHON + case _: + result = None + return result + + def get_version(self) -> Version: + try: + match self.build_file_type(): + case BuildFileType.JS: + version_str = json.loads(self.content)["version"] + case BuildFileType.JAVA_GRADLE: + # TODO: '\nversion = ' will not parse all ?! + version_line = re.search("\nversion = .*", self.content) + version_line_group = version_line.group() + version_string = re.search( + "[0-9]*\\.[0-9]*\\.[0-9]*(-SNAPSHOT)?", version_line_group + ) + version_str = version_string.group() + case BuildFileType.PYTHON: + # TODO: '\nversion = ' will not parse all ?! + version_line = re.search("\nversion = .*\n", self.content) + version_line_group = version_line.group() + version_string = re.search( + "[0-9]*\\.[0-9]*\\.[0-9]*(-dev)?[0-9]*", version_line_group + ) + version_str = version_string.group() + case BuildFileType.JAVA_CLOJURE: + # TODO: unsure about the trailing '\n' ! + version_line = re.search("\\(defproject .*\n", self.content) + version_line_group = version_line.group() + version_string = re.search( + "[0-9]*\\.[0-9]*\\.[0-9]*(-SNAPSHOT)?", version_line_group + ) + version_str = version_string.group() + except: + raise Exception(f"Version not found in file {self.file_path}") + + result = Version.from_str(version_str) + result.throw_if_invalid() + + return result + + def set_version(self, new_version: Version): + # TODO: How can we create regex-pattern constants to use them at both places? + try: + match self.build_file_type(): + case BuildFileType.JS: + json_data = json.loads(self.content) + json_data["version"] = new_version.to_string() + self.content = json.dumps(json_data, indent=4) + case BuildFileType.JAVA_GRADLE: + substitute = re.sub( + '\nversion = "[0-9]*\\.[0-9]*\\.[0-9]*(-SNAPSHOT)?"', + f'\nversion = "{new_version.to_string()}"', + self.content, + ) + self.content = substitute + case BuildFileType.PYTHON: + substitute = re.sub( + '\nversion = "[0-9]*\\.[0-9]*\\.[0-9]*(-dev)?[0-9]*"', + f'\nversion = "{new_version.to_string()}"', + self.content, + ) + self.content = substitute + case BuildFileType.JAVA_CLOJURE: + version_line = re.search("\\(defproject .*\n", self.content) + version_line_group = version_line.group() + # TODO: we should stick here on defproject instead of first line! + version_string = re.search( + "[0-9]*\\.[0-9]*\\.[0-9]*(-SNAPSHOT)?", version_line_group + ) + version_str = version_string.group() + substitute = re.sub( + '"[0-9]*\\.[0-9]*\\.[0-9]*(-SNAPSHOT)?"', + f'"{new_version.to_string()}"', + self.content, + 1 + ) + self.content = substitute + except: + raise Exception(f"Version not found in file {self.file_path}") diff --git a/src/main/python/ddadevops/domain/c4k.py b/src/main/python/ddadevops/domain/c4k.py index 5ae0cbf..ca086a6 100644 --- a/src/main/python/ddadevops/domain/c4k.py +++ b/src/main/python/ddadevops/domain/c4k.py @@ -5,17 +5,20 @@ from .common import ( Devops, ) + class C4k(Validateable): - def __init__(self, config: dict): - tmp_executabel_name = config["C4kMixin"]["executabel_name"] - if not tmp_executabel_name: - tmp_executabel_name = config["module"] - self.executabel_name = tmp_executabel_name - self.c4k_mixin_config = config["C4kMixin"]["config"] - self.c4k_mixin_auth = config["C4kMixin"]["auth"] - tmp = self.c4k_mixin_config["mon-cfg"] - tmp.update({"cluster-name": config["module"], "cluster-stage": config["stage"]}) - self.c4k_mixin_config.update({"mon-cfg": tmp}) + def __init__(self, input: dict): + self.module = input.get("module") + self.stage = input.get("stage") + self.c4k_executable_name = input.get("c4k_executable_name", input.get("module")) + self.c4k_config = input.get("c4k_config", {}) + self.c4k_grafana_cloud_url = input.get( + "c4k_grafana_cloud_url", + "https://prometheus-prod-01-eu-west-0.grafana.net/api/prom/push", + ) + self.c4k_auth = input.get("c4k_auth", {}) + self.c4k_grafana_cloud_user = input.get('c4k_grafana_cloud_user') + self.c4k_grafana_cloud_password = input.get('c4k_grafana_cloud_password') self.dns_record: Optional[DnsRecord] = None # TODO: these functions should be located at TerraformBuild later on. @@ -24,20 +27,39 @@ class C4k(Validateable): def validate(self) -> List[str]: result = [] - result += self.__validate_is_not_empty__("fqdn") + result += self.__validate_is_not_empty__("module") + result += self.__validate_is_not_empty__("stage") + result += self.__validate_is_not_empty__("c4k_executable_name") + result += self.__validate_is_not_empty__("c4k_grafana_cloud_user") + result += self.__validate_is_not_empty__("c4k_grafana_cloud_password") if self.dns_record: result += self.dns_record.validate() return result def config(self): - fqdn = self.dns_record.fqdn - self.c4k_mixin_config.update({"fqdn": fqdn}) - return self.c4k_mixin_config + if not self.dns_record: + raise ValueError("dns_reqord was not set.") + result = self.c4k_config.copy() + result["fqdn"] = self.dns_record.fqdn + result["mon-cfg"] = { + "cluster-name": self.module, + "cluster-stage": self.stage, + "grafana-cloud-url": self.c4k_grafana_cloud_url, + } + return result - def command(self, build: Devops): - module = build.module - build_path = build.build_path() + def auth(self): + result = self.c4k_auth.copy() + result["mon-auth"] = { + "grafana-cloud-user": self.c4k_grafana_cloud_user, + "grafana-cloud-password": self.c4k_grafana_cloud_password, + } + return result + + def command(self, devops: Devops): + module = devops.module + build_path = devops.build_path() config_path = f"{build_path}/out_c4k_config.yaml" auth_path = f"{build_path}/out_c4k_auth.yaml" output_path = f"{build_path}/out_{module}.yaml" - return f"c4k-{self.executabel_name}-standalone.jar {config_path} {auth_path} > {output_path}" + return f"c4k-{self.c4k_executable_name}-standalone.jar {config_path} {auth_path} > {output_path}" diff --git a/src/main/python/ddadevops/domain/common.py b/src/main/python/ddadevops/domain/common.py index 076bfd3..0ed8664 100644 --- a/src/main/python/ddadevops/domain/common.py +++ b/src/main/python/ddadevops/domain/common.py @@ -1,17 +1,46 @@ -from typing import List import deprecation +from enum import Enum +from typing import List, TypedDict +import deprecation + def filter_none(list_to_filter): return [x for x in list_to_filter if x is not None] -class Validateable: - def __validate_is_not_empty__(self, field_name: str) -> List[str]: - value = self.__dict__[field_name] - if value is None or value == "": - return [f"Field '{field_name}' must not be empty."] +class BuildType(Enum): + IMAGE = 0 + C4K = 1 + + +class MixinType(Enum): + RELEASE = 0 + + +class ReleaseType(Enum): + MAJOR = 3 + MINOR = 2 + PATCH = 1 + NONE = None + + +class Validateable: + def __validate_is_not_none__(self, field_name: str) -> List[str]: + value = self.__dict__[field_name] + if value is None: + return [f"Field '{field_name}' must not be None."] return [] + def __validate_is_not_empty__(self, field_name: str) -> List[str]: + result = self.__validate_is_not_none__(field_name) + if len(result) == 0: + value = self.__dict__[field_name] + if type(value) is str and value == "": + result += [f"Field '{field_name}' must not be empty."] + elif type(value) is list and len(value) == 0: + result += [f"Field '{field_name}' must not be empty."] + return result + def validate(self) -> List[str]: return [] @@ -20,9 +49,10 @@ class Validateable: def throw_if_invalid(self): if not self.is_valid(): - issues = '\n'.join(self.validate()) + issues = "\n".join(self.validate()) raise ValueError(f"Invalid Validateable: {issues}") + class DnsRecord(Validateable): def __init__(self, fqdn, ipv4=None, ipv6=None): self.fqdn = fqdn @@ -39,28 +69,37 @@ class DnsRecord(Validateable): class Devops(Validateable): def __init__( - self, stage: str, project_root_path: str, module: str, name: str | None =None, build_dir_name: str="target" + self, + input: dict, + specialized_builds: dict[BuildType, Validateable], + mixins: dict[MixinType, Validateable], ): - self.stage = stage - self.name = name - self.project_root_path = project_root_path - self.module = module - if not name: - self.name = module - self.build_dir_name = build_dir_name - # Deprecated - no longer use generic stack ... - self.stack : dict = {} - - @deprecation.deprecated(deprecated_in="3.2") - # use .name instead - #pylint: disable=method-hidden - def name(self): - return self.name + self.stage = input.get("stage") + self.project_root_path = input.get("project_root_path") + self.module = input.get("module") + self.name = input.get("name", self.module) + self.build_dir_name = input.get("build_dir_name", "target") + self.specialized_builds = specialized_builds + self.mixins = mixins def build_path(self): path = [self.project_root_path, self.build_dir_name, self.name, self.module] return "/".join(filter_none(path)) + def validate(self) -> List[str]: + result = [] + result += self.__validate_is_not_empty__("stage") + result += self.__validate_is_not_empty__("project_root_path") + result += self.__validate_is_not_empty__("module") + result += self.__validate_is_not_none__("specialized_builds") + if self.specialized_builds: + for build in self.specialized_builds: + result += self.specialized_builds[build].validate() + if self.mixins: + for mixin in self.mixins: + result += self.mixins[mixin].validate() + return result + def __put__(self, key, value): self.stack[key] = value diff --git a/src/main/python/ddadevops/domain/credentials.py b/src/main/python/ddadevops/domain/credentials.py new file mode 100644 index 0000000..f144394 --- /dev/null +++ b/src/main/python/ddadevops/domain/credentials.py @@ -0,0 +1,63 @@ +import deprecation +from enum import Enum +from typing import List, TypedDict +from inflection import underscore +import deprecation +from .common import ( + Validateable, +) + + +class GopassType(Enum): + FIELD = 0 + PASSWORD = 1 + + +class CredentialMapping(Validateable): + def __init__(self, mapping: dict): + self.name = mapping.get("name", None) + self.gopass_field = mapping.get("gopass_field", None) + self.gopass_path = mapping.get("gopass_path", None) + + def validate(self) -> List[str]: + result = [] + result += self.__validate_is_not_empty__("gopass_path") + if not self.name and not self.gopass_field: + result.append(f"Either name or gopass field has to be defined.") + return result + + def gopass_type(self): + if self.gopass_field: + return GopassType.FIELD + else: + return GopassType.PASSWORD + + def name_for_input(self): + if self.name: + result = self.name + elif self.gopass_field: + result = underscore(self.gopass_field) + else: + result = "" + return result + + def name_for_environment(self): + return self.name_for_input().upper() + + +class Credentials(Validateable): + def __init__(self, input: dict, default_mappings: list = []): + input_mappings = input.get("credentials_mapping", []) + self.mappings = {} + for input_mapping in default_mappings: + mapping = CredentialMapping(input_mapping) + self.mappings[mapping.name_for_input()] = mapping + for input_mapping in input_mappings: + mapping = CredentialMapping(input_mapping) + self.mappings[mapping.name_for_input()] = mapping + + def validate(self) -> List[str]: + result = [] + for mapping in self.mappings.values(): + result += mapping.validate() + return result diff --git a/src/main/python/ddadevops/domain/devops_factory.py b/src/main/python/ddadevops/domain/devops_factory.py new file mode 100644 index 0000000..f631c31 --- /dev/null +++ b/src/main/python/ddadevops/domain/devops_factory.py @@ -0,0 +1,48 @@ +import deprecation +from enum import Enum +from typing import List +from .common import Devops, BuildType, MixinType +from .image import Image +from .c4k import C4k +from .release import Release +from .version import Version + + +class DevopsFactory: + def __init__(self): + pass + + def build_devops(self, input: dict, version: Version = None) -> Devops: + build_types = self.__parse_build_types__(input["build_types"]) + mixin_types = self.__parse_mixin_types__(input["mixin_types"]) + + specialized_builds = {} + if BuildType.IMAGE in build_types: + specialized_builds[BuildType.IMAGE] = Image(input) + if BuildType.C4K in build_types: + specialized_builds[BuildType.C4K] = C4k(input) + + mixins = {} + if MixinType.RELEASE in mixin_types: + mixins[MixinType.RELEASE] = Release(input, version) + + devops = Devops(input, specialized_builds=specialized_builds, mixins=mixins) + + devops.throw_if_invalid() + + return devops + + def merge(self, input: dict, context: dict, authorization: dict) -> dict: + return {} | context | authorization | input + + def __parse_build_types__(self, build_types: List[str]) -> List[BuildType]: + result = [] + for build_type in build_types: + result += [BuildType[build_type]] + return result + + def __parse_mixin_types__(self, mixin_types: List[str]) -> List[MixinType]: + result = [] + for mixin_type in mixin_types: + result += [MixinType[mixin_type]] + return result diff --git a/src/main/python/ddadevops/domain/image.py b/src/main/python/ddadevops/domain/image.py index 62e7c63..b959c3c 100644 --- a/src/main/python/ddadevops/domain/image.py +++ b/src/main/python/ddadevops/domain/image.py @@ -1,28 +1,38 @@ +from typing import Optional, List from .common import ( filter_none, Validateable, - Devops, ) + class Image(Validateable): def __init__( self, - dockerhub_user, - dockerhub_password, - devops: Devops, - use_package_common_files=True, - build_commons_path=None, - docker_build_commons_dir_name="docker", - docker_publish_tag=None, + input: dict, ): - self.dockerhub_user = dockerhub_user - self.dockerhub_password = dockerhub_password - self.use_package_common_files = use_package_common_files - self.build_commons_path = build_commons_path - self.docker_build_commons_dir_name = docker_build_commons_dir_name - self.docker_publish_tag = docker_publish_tag - self.devops = devops + self.image_dockerhub_user = input.get("image_dockerhub_user") + self.image_dockerhub_password = input.get("image_dockerhub_password") + self.image_tag = input.get("image_tag") + self.image_build_commons_path = input.get("image_build_commons_path") + self.image_use_package_common_files = input.get( + "image_use_package_common_files", True + ) + self.image_build_commons_dir_name = input.get( + "image_build_commons_dir_name", "docker" + ) - def docker_build_commons_path(self): - commons_path = [self.build_commons_path, self.docker_build_commons_dir_name] + def validate(self) -> List[str]: + result = [] + result += self.__validate_is_not_empty__("image_dockerhub_user") + result += self.__validate_is_not_empty__("image_dockerhub_password") + if not self.image_use_package_common_files: + result += self.__validate_is_not_empty__("image_build_commons_path") + result += self.__validate_is_not_empty__("image_build_commons_dir_name") + return result + + def build_commons_path(self): + commons_path = [ + self.image_build_commons_path, + self.image_build_commons_dir_name, + ] return "/".join(filter_none(commons_path)) + "/" diff --git a/src/main/python/ddadevops/domain/init_service.py b/src/main/python/ddadevops/domain/init_service.py new file mode 100644 index 0000000..e05a6e3 --- /dev/null +++ b/src/main/python/ddadevops/domain/init_service.py @@ -0,0 +1,129 @@ +from pathlib import Path +from typing import List +from .common import Devops, MixinType, BuildType +from .credentials import Credentials, GopassType +from .devops_factory import DevopsFactory +from .version import Version +from .release import ReleaseType +from ..infrastructure import BuildFileRepository, CredentialsApi, EnvironmentApi, GitApi + + +class InitService: + def __init__( + self, + devops_factory, + build_file_repository, + credentials_api, + environment_api, + git_api, + ): + self.devops_factory = devops_factory + self.build_file_repository = build_file_repository + self.credentials_api = credentials_api + self.environment_api = environment_api + self.git_api = git_api + + @classmethod + def prod(cls, base_dir: str): + return cls( + DevopsFactory(), + BuildFileRepository(base_dir), + CredentialsApi(), + EnvironmentApi(), + GitApi(), + ) + + def initialize(self, input: dict) -> Devops: + build_types = self.devops_factory.__parse_build_types__(input["build_types"]) + mixin_types = self.devops_factory.__parse_mixin_types__(input["mixin_types"]) + + version = None + default_mappings = [] + + if BuildType.C4K in build_types: + default_mappings += [ + { + "gopass_path": "server/meissa/grafana-cloud", + "gopass_field": "grafana-cloud-user", + "name": "c4k_grafana_cloud_user", + }, + { + "gopass_path": "server/meissa/grafana-cloud", + "name": "c4k_grafana_cloud_password", + }, + ] + if BuildType.IMAGE in build_types: + default_mappings += [ + { + "gopass_path": "meissa/web/docker.com", + "gopass_field": "login", + "name": "image_dockerhub_user", + }, + { + "gopass_path": "meissa/web/docker.com", + "name": "image_dockerhub_password", + }, + ] + + if MixinType.RELEASE in mixin_types: + primary_build_file_id = input.get( + "release_primary_build_file", "./project.clj" + ) + primary_build_file = self.build_file_repository.get( + Path(primary_build_file_id) + ) + version = primary_build_file.get_version() + + credentials = Credentials(input, default_mappings) + authorization = self.authorization(credentials) + + context = self.context(mixin_types, version) + + merged = self.devops_factory.merge(input, context, authorization) + + return self.devops_factory.build_devops(merged, version=version) + + def context(self, mixin_types, version) -> dict: + result = {} + + tag = self.environment_api.get("IMAGE_TAG") + + if MixinType.RELEASE in mixin_types: + release_type = self.environment_api.get("RELEASE_TYPE") + if not release_type: + latest_commit = self.git_api.get_latest_commit() + if latest_commit in [ + ReleaseType.MAJOR.name, + ReleaseType.MINOR.name, + ReleaseType.PATCH.name, + ReleaseType.NONE.name, + ]: + release_type = latest_commit + result["release_type"] = release_type + result["release_current_branch"] = self.git_api.get_current_branch() + + if not tag: + tag = version.to_string() + + if tag: + result["image_tag"] = tag + + return result + + def authorization(self, credentials: Credentials) -> List[str]: + result = {} + for name in credentials.mappings.keys(): + mapping = credentials.mappings[name] + env_value = self.environment_api.get(mapping.name_for_environment()) + if env_value: + result[name] = env_value + else: + if mapping.gopass_type() == GopassType.FIELD: + result[name] = self.credentials_api.gopass_field_from_path( + mapping.gopass_path, mapping.gopass_field + ) + if mapping.gopass_type() == GopassType.PASSWORD: + result[name] = self.credentials_api.gopass_password_from_path( + mapping.gopass_path + ) + return result diff --git a/src/main/python/ddadevops/domain/release.py b/src/main/python/ddadevops/domain/release.py index 2c27b4a..78f69ad 100644 --- a/src/main/python/ddadevops/domain/release.py +++ b/src/main/python/ddadevops/domain/release.py @@ -1,121 +1,60 @@ from enum import Enum -from typing import Optional +from typing import Optional, List from pathlib import Path from .common import ( Validateable, Devops, + ReleaseType, +) +from .version import ( + Version, ) -class ReleaseType(Enum): - MAJOR = 0 - MINOR = 1 - PATCH = 2 - SNAPSHOT = 3 - BUMP = None -class EnvironmentKeys(Enum): - DDADEVOPS_RELEASE_TYPE = 0 - -class Version(): - - def __init__(self, path: Path, version_list: list): - self.path = path - self.version_list = version_list - self.version_string: Optional[str | None] = None - self.is_snapshot: Optional[bool | None] = None - - def increment(self, release_type: ReleaseType | None): - self.is_snapshot = False - match release_type: - case ReleaseType.BUMP: - self.is_snapshot = True - self.version_list[ReleaseType.PATCH.value] += 1 - case ReleaseType.SNAPSHOT: - self.is_snapshot = True - case ReleaseType.PATCH: - self.version_list[ReleaseType.PATCH.value] += 1 - case ReleaseType.MINOR: - self.version_list[ReleaseType.PATCH.value] = 0 - self.version_list[ReleaseType.MINOR.value] += 1 - case ReleaseType.MAJOR: - self.version_list[ReleaseType.PATCH.value] = 0 - self.version_list[ReleaseType.MINOR.value] = 0 - self.version_list[ReleaseType.MAJOR.value] += 1 - case None: - raise RuntimeError("Release Type was not set!") - - def get_version_string(self) -> str: - self.version_string = ".".join([str(x) for x in self.version_list]) - if self.is_snapshot: - self.version_string += "-SNAPSHOT" - return self.version_string - - def create_release_version(self, release_type: ReleaseType | None): - release_version = Version(self.path, self.version_list.copy()) - release_version.is_snapshot = self.is_snapshot - release_version.increment(release_type) - return release_version - - def create_bump_version(self): - bump_version = Version(self.path, self.version_list.copy()) - bump_version.is_snapshot = self.is_snapshot - bump_version.increment(ReleaseType.BUMP) - return bump_version - -class ReleaseContext(Validateable): - def __init__(self, release_type: ReleaseType | None, version: Version, current_branch: str): - self.release_type = release_type +class Release(Validateable): + def __init__(self, input: dict, version: Version): + self.release_type = ReleaseType[input.get("release_type", "NONE")] + self.release_main_branch = input.get("release_main_branch", "main") + self.release_current_branch = input.get("release_current_branch") + self.release_primary_build_file = input.get( + "release_primary_build_file", "./project.clj" + ) + self.release_secondary_build_files = input.get( + "release_secondary_build_files", [] + ) self.version = version - self.current_branch = current_branch - - def release_version(self) -> Version: - return self.version.create_release_version(self.release_type) - - def bump_version(self) -> Version: - return self.release_version().create_bump_version() def validate(self): result = [] result += self.__validate_is_not_empty__("release_type") + result += self.__validate_is_not_empty__("release_main_branch") + result += self.__validate_is_not_empty__("release_current_branch") + result += self.__validate_is_not_empty__("release_primary_build_file") result += self.__validate_is_not_empty__("version") - result += self.__validate_is_not_empty__("current_branch") + try: + Path(self.release_primary_build_file) + except Exception as e: + result.append( + f"release_primary_build_file must be a valid path but was {e}" + ) + for path in self.release_secondary_build_files: + try: + Path(path) + except Exception as e: + result.append( + f"release_secondary_build_file must be contain valid paths but was {e}" + ) + if self.version: + result += self.version.validate() + if ( + self.release_type is not None + and self.release_type != ReleaseType.NONE + and self.release_main_branch != self.release_current_branch + ): + result.append(f"Releases are allowed only on {self.release_main_branch}") return result - def validate_branch(self, main_branch: str): - result = [] - if self.release_type is not None and main_branch != self.current_branch: - result.append(f"Releases are allowed only on {main_branch}") + def build_files(self) -> List[str]: + result = [self.release_primary_build_file] + result += self.release_secondary_build_files return result - -class Release(Validateable): - def __init__( - self, - devops: Devops, - main_branch: str, - config_file: str, - ): - self.devops = devops - self.main_branch = main_branch - self.config_file = config_file - self.release_context: ReleaseContext | None = None - - def set_release_context(self, set_release_context: ReleaseContext): - self.release_context = set_release_context - - def release_version(self): - return self.release_context.release_version() - - def bump_version(self): - return self.release_context.bump_version() - - - def validate(self): - result = [] - result += self.__validate_is_not_empty__("main_branch") - result += self.__validate_is_not_empty__("config_file") - result += self.__validate_is_not_empty__("release_context") - if self.release_context is not None: - result += self.release_context.validate() - result += self.release_context.validate_branch(self.main_branch) - return result - \ No newline at end of file diff --git a/src/main/python/ddadevops/domain/version.py b/src/main/python/ddadevops/domain/version.py new file mode 100644 index 0000000..230c1ae --- /dev/null +++ b/src/main/python/ddadevops/domain/version.py @@ -0,0 +1,100 @@ +from enum import Enum +from typing import Optional +from .common import ( + Validateable, +) + + +class Version(Validateable): + @classmethod + def from_str(cls, input_str: str): + snapshot_parsed = input_str.split("-") + version_str = snapshot_parsed[0] + suffix_str = None + if len(snapshot_parsed) > 1: + suffix_str = snapshot_parsed[1] + version_no_parsed = [int(x) for x in version_str.split(".")] + return cls( + version_no_parsed, + suffix_str, + input_str, + ) + + def __init__( + self, + version_list: list, + snapshot_suffix: Optional[str] = None, + version_str: Optional[str] = None, + ): + self.version_list = version_list + self.snapshot_suffix = snapshot_suffix + self.version_string = version_str + + def __eq__(self, other): + return other and self.to_string() == other.to_string() + + def __hash__(self) -> int: + return self.to_string().__hash__() + + def is_snapshot(self): + return not self.snapshot_suffix == None + + def to_string(self) -> str: + version_no = ".".join([str(x) for x in self.version_list]) + if self.is_snapshot(): + return f"{version_no}-{self.snapshot_suffix}" + return version_no + + def validate(self): + result = [] + result += self.__validate_is_not_empty__("version_list") + if self.version_list and len(self.version_list) < 3: + result += [f"version_list must have at least 3 levels."] + if ( + self.version_list + and self.version_string + and self.to_string() != self.version_string + ): + result += [ + f"version_string not parsed correct. Input was {self.version_string} parsed was {self.to_string()}" + ] + return result + + def create_bump(self, snapshot_suffix: str = None): + new_version_list = self.version_list.copy() + if self.is_snapshot(): + return Version( + new_version_list, snapshot_suffix=self.snapshot_suffix, version_str=None + ) + else: + new_version_list[2] += 1 + return Version( + new_version_list, snapshot_suffix=snapshot_suffix, version_str=None + ) + + def create_patch(self): + new_version_list = self.version_list.copy() + if self.is_snapshot(): + return Version(new_version_list, snapshot_suffix=None, version_str=None) + else: + new_version_list[2] += 1 + return Version(new_version_list, snapshot_suffix=None, version_str=None) + + def create_minor(self): + new_version_list = self.version_list.copy() + if self.is_snapshot() and new_version_list[2] == 0: + return Version(new_version_list, snapshot_suffix=None, version_str=None) + else: + new_version_list[2] = 0 + new_version_list[1] += 1 + return Version(new_version_list, snapshot_suffix=None, version_str=None) + + def create_major(self): + new_version_list = self.version_list.copy() + if self.is_snapshot() and new_version_list[2] == 0 and new_version_list[1] == 0: + return Version(new_version_list, snapshot_suffix=None, version_str=None) + else: + new_version_list[2] = 0 + new_version_list[1] = 0 + new_version_list[0] += 1 + return Version(new_version_list, snapshot_suffix=None, version_str=None) diff --git a/src/main/python/ddadevops/infrastructure/__init__.py b/src/main/python/ddadevops/infrastructure/__init__.py index 10deeea..a0e975e 100644 --- a/src/main/python/ddadevops/infrastructure/__init__.py +++ b/src/main/python/ddadevops/infrastructure/__init__.py @@ -1 +1,10 @@ -from .infrastructure import FileApi, ImageApi, ResourceApi, ExecutionApi, ProjectRepository +from .infrastructure import ( + FileApi, + ImageApi, + ResourceApi, + ExecutionApi, + EnvironmentApi, + CredentialsApi, + GitApi, +) +from .repository import DevopsRepository, BuildFileRepository diff --git a/src/main/python/ddadevops/infrastructure/infrastructure.py b/src/main/python/ddadevops/infrastructure/infrastructure.py index 15e1e32..7a44048 100644 --- a/src/main/python/ddadevops/infrastructure/infrastructure.py +++ b/src/main/python/ddadevops/infrastructure/infrastructure.py @@ -1,58 +1,30 @@ from pathlib import Path from sys import stdout -from os import chmod -from subprocess import run +from os import chmod, environ from pkg_resources import resource_string import yaml -from ..domain import Devops, Image, C4k, Release -from ..python_util import execute - - -class ProjectRepository: - def set_build(self, project, build): - project.set_property("devops_build", build) - - def get_devops(self, project) -> Devops: - return project.get_property("build") - - def set_devops(self, project, build: Devops): - project.set_property("build", build) - - def get_docker(self, project) -> Image: - return project.get_property("docker_build") - - def set_docker(self, project, build: Image): - project.set_property("docker_build", build) - - def get_c4k(self, project) -> C4k: - return project.get_property("c4k_build") - - def set_c4k(self, project, build: C4k): - project.set_property("c4k_build", build) - - def get_release(self, project) -> Release: - return project.get_property("release_build") - - def set_release(self, project, build: Release): - project.set_property("release_build", build) - +from subprocess import check_output, Popen, PIPE, run +from ..domain import Devops, Image, C4k, Release, BuildFile class ResourceApi: def read_resource(self, path: str) -> bytes: - return resource_string(__name__, path) + return resource_string('ddadevops', path) class FileApi: + def __init__(self): + self.execution_api = ExecutionApi() + def clean_dir(self, directory: str): - execute("rm -rf " + directory, shell=True) - execute("mkdir -p " + directory, shell=True) + self.execution_api.execute("rm -rf " + directory) + self.execution_api.execute("mkdir -p " + directory) def cp_force(self, src: str, target_dir: str): - execute("cp -f " + src + "* " + target_dir, shell=True) + self.execution_api.execute("cp -f " + src + "* " + target_dir) def cp_recursive(self, src: str, target_dir: str): - execute("cp -r " + src + " " + target_dir, shell=True) + self.execution_api.execute("cp -r " + src + " " + target_dir) def write_data_to_file(self, path: Path, data: bytes): with open(path, "w", encoding="utf-8") as output_file: @@ -74,7 +46,7 @@ class ImageApi: def drun(self, name: str): run( - f"docker run -it --entrypoint=\"\" {name} /bin/bash", + f'docker run -it --entrypoint="" {name} /bin/bash', shell=True, check=True, ) @@ -118,11 +90,93 @@ class ImageApi: class ExecutionApi: - def execute(self, command: str, dry_run=False): + def execute(self, command: str, dry_run=False, shell=True): output = "" if dry_run: print(command) else: - output = execute(command, True) - print(output) + output = check_output(command, encoding="UTF-8", shell=shell) + output = output.rstrip() return output + + def execute_live(command): + process = Popen(command, stdout=PIPE) + for line in iter(process.stdout.readline, b""): + print(line.decode("utf-8"), end="") + process.stdout.close() + process.wait() + + +class EnvironmentApi: + def get(self, key): + return environ.get(key) + + +class CredentialsApi: + def __init__(self): + self.execution_api = ExecutionApi() + + def gopass_field_from_path(self, path, field): + credential = None + if path and field: + print("get field for: " + path + ", " + field) + credential = self.execution_api.execute(["gopass", "show", path, field], shell=False) + return credential + + def gopass_password_from_path(self, path): + credential = None + if path: + print("get password for: " + path) + credential = self.execution_api.execute( + ["gopass", "show", "--password", path], shell=False + ) + return credential + + +class GitApi: + def __init__(self): + self.execution_api = ExecutionApi() + + # pylint: disable=invalid-name + def get_latest_n_commits(self, n: int): + return self.execution_api.execute(f'git log --oneline --format="%s %b" -n {n}') + + def get_latest_commit(self): + return self.get_latest_n_commits(1) + + def tag_annotated(self, annotation: str, message: str, count: int): + return self.execution_api.execute( + f"git tag -a {annotation} -m '{message}' HEAD~{count}" + ) + + def tag_annotated_second_last(self, annotation: str, message: str): + return self.tag_annotated(annotation, message, 1) + + def get_latest_tag(self): + return self.execution_api.execute("git describe --tags --abbrev=0") + + def get_current_branch(self): + return "".join(self.execution_api.execute("git branch --show-current")).rstrip() + + def init(self, default_branch: str = "main"): + self.execution_api.execute("git init") + self.execution_api.execute(f"git checkout -b {default_branch}") + + def set_user_config(self, email: str, name: str): + self.execution_api.execute(f"git config user.email {email}") + self.execution_api.execute(f"git config user.name {name}") + + def add_file(self, file_path: Path): + return self.execution_api.execute(f"git add {file_path}") + + def add_remote(self, origin: str, url: str): + return self.execution_api.execute(f"git remote add {origin} {url}") + + def commit(self, commit_message: str): + return self.execution_api.execute(f'git commit -m "{commit_message}"') + + def push(self): + return self.execution_api.execute("git push") + + def checkout(self, branch: str): + return self.execution_api.execute(f"git checkout {branch}") diff --git a/src/main/python/ddadevops/infrastructure/release_mixin/__init__.py b/src/main/python/ddadevops/infrastructure/release_mixin/__init__.py deleted file mode 100644 index 7c50c26..0000000 --- a/src/main/python/ddadevops/infrastructure/release_mixin/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .infrastructure_api import FileHandler, EnvironmentApi, GitApi, JsonFileHandler, GradleFileHandler, PythonFileHandler, ClojureFileHandler -from .repo import VersionRepository, ReleaseContextRepository, ReleaseTypeRepository diff --git a/src/main/python/ddadevops/infrastructure/release_mixin/infrastructure_api.py b/src/main/python/ddadevops/infrastructure/release_mixin/infrastructure_api.py deleted file mode 100644 index f65140e..0000000 --- a/src/main/python/ddadevops/infrastructure/release_mixin/infrastructure_api.py +++ /dev/null @@ -1,242 +0,0 @@ -import json -import re -from abc import ABC, abstractmethod -from typing import Optional -from pathlib import Path -from os import environ -from ..infrastructure import ExecutionApi - -# TODO: jem, zam - 2023_04_18: Discuss if we can move more functionality to domain? -class FileHandler(ABC): - def __init__(self) -> None: - self.config_file_path: Optional[Path | None] = None - self.config_file_type: Optional[Path | None] = None - - @classmethod - def from_file_path(cls, file_path): - config_file_type = file_path.suffix - match config_file_type: - case '.json': - file_handler = JsonFileHandler() - case '.gradle': - file_handler = GradleFileHandler() - case '.clj': - file_handler = ClojureFileHandler() - case '.py': - file_handler = PythonFileHandler() - case _: - raise RuntimeError( - f'The file type "{config_file_type}" is not implemented') - # TODO: Attribute is only set in classmethod. Should this be initialized outside of this class? - file_handler.config_file_path = file_path - file_handler.config_file_type = config_file_type - return file_handler - - @abstractmethod - def parse(self) -> tuple[list[int], bool]: - pass - - @abstractmethod - def write(self, version_string): - pass - - -class JsonFileHandler(FileHandler): - - def parse(self) -> tuple[list[int], bool]: - if self.config_file_path is None: - raise ValueError("No file name given.") - with open(self.config_file_path, 'r', encoding='utf-8') as json_file: - json_version = json.load(json_file)['version'] - is_snapshot = False - if '-SNAPSHOT' in json_version: - is_snapshot = True - json_version = json_version.replace('-SNAPSHOT', '') - version = [int(x) for x in json_version.split('.')] - return version, is_snapshot - - def write(self, version_string): - with open(self.config_file_path, 'r+', encoding='utf-8') as json_file: - json_data = json.load(json_file) - json_data['version'] = version_string - json_file.seek(0) - json.dump(json_data, json_file, indent=4) - json_file.truncate() - - -class GradleFileHandler(FileHandler): - - def parse(self) -> tuple[list[int], bool]: - if self.config_file_path is None: - raise ValueError("No file name given.") - with open(self.config_file_path, 'r', encoding='utf-8') as gradle_file: - contents = gradle_file.read() - version_line = re.search("\nversion = .*", contents) - exception = Exception("Version not found in gradle file") - if version_line is None: - raise exception - - version_line_group = version_line.group() - version_string = re.search( - '[0-9]*\\.[0-9]*\\.[0-9]*(-SNAPSHOT)?', version_line_group) - if version_string is None: - raise exception - - version_string_group = version_string.group() - is_snapshot = False - if '-SNAPSHOT' in version_string_group: - is_snapshot = True - version_string_group = version_string_group.replace('-SNAPSHOT', '') - - version = [int(x) for x in version_string_group.split('.')] - - return version, is_snapshot - - def write(self, version_string): - with open(self.config_file_path, 'r+', encoding='utf-8') as gradle_file: - contents = gradle_file.read() - version_substitute = re.sub( - '\nversion = "[0-9]*\\.[0-9]*\\.[0-9]*(-SNAPSHOT)?"', f'\nversion = "{version_string}"', contents) - gradle_file.seek(0) - gradle_file.write(version_substitute) - gradle_file.truncate() - - -class PythonFileHandler(FileHandler): - - def parse(self) -> tuple[list[int], bool]: - if self.config_file_path is None: - raise ValueError("No file name given.") - with open(self.config_file_path, 'r', encoding='utf-8') as python_file: - contents = python_file.read() - version_line = re.search("\nversion = .*\n", contents) - exception = Exception("Version not found in gradle file") - if version_line is None: - raise exception - - version_line_group = version_line.group() - version_string = re.search( - '[0-9]*\\.[0-9]*\\.[0-9]*(-SNAPSHOT)?', version_line_group) - if version_string is None: - raise exception - - version_string_group = version_string.group() - is_snapshot = False - if '-SNAPSHOT' in version_string_group: - is_snapshot = True - version_string_group = version_string_group.replace('-SNAPSHOT', '') - - version = [int(x) for x in version_string_group.split('.')] - - return version, is_snapshot - - def write(self, version_string): - with open(self.config_file_path, 'r+', encoding='utf-8') as python_file: - contents = python_file.read() - version_substitute = re.sub( - '\nversion = "[0-9]*\\.[0-9]*\\.[0-9]*(-SNAPSHOT)?"', f'\nversion = "{version_string}"', contents) - python_file.seek(0) - python_file.write(version_substitute) - python_file.truncate() - - -class ClojureFileHandler(FileHandler): - - def parse(self) -> tuple[list[int], bool]: - if self.config_file_path is None: - raise ValueError("No file name given.") - with open(self.config_file_path, 'r', encoding='utf-8') as clj_file: - contents = clj_file.read() - version_line = re.search("^\\(defproject .*\n", contents) - exception = Exception("Version not found in clj file") - if version_line is None: - raise exception - - version_line_group = version_line.group() - version_string = re.search( - '[0-9]*\\.[0-9]*\\.[0-9]*(-SNAPSHOT)?', version_line_group) - if version_string is None: - raise exception - - version_string_group = version_string.group() - is_snapshot = False - if '-SNAPSHOT' in version_string_group: - is_snapshot = True - version_string_group = version_string_group.replace('-SNAPSHOT', '') - - version = [int(x) for x in version_string_group.split('.')] - - return version, is_snapshot - - def write(self, version_string): - with open(self.config_file_path, 'r+', encoding='utf-8') as clj_file: - clj_first = clj_file.readline() - clj_rest = clj_file.read() - version_substitute = re.sub( - '[0-9]*\\.[0-9]*\\.[0-9]*(-SNAPSHOT)?', f'"{version_string}"\n', clj_first) - clj_file.seek(0) - clj_file.write(version_substitute) - clj_file.write(clj_rest) - clj_file.truncate() - - -class GitApi(): - - def __init__(self): - self.execution_api = ExecutionApi() - - # pylint: disable=invalid-name - def get_latest_n_commits(self, n: int): - return self.execution_api.execute( - f'git log --oneline --format="%s %b" -n {n}') - - def get_latest_commit(self): - return self.get_latest_n_commits(1) - - def tag_annotated(self, annotation: str, message: str, count: int): - return self.execution_api.execute( - f'git tag -a {annotation} -m {message} HEAD~{count}') - - def tag_annotated_second_last(self, annotation: str, message:str): - return self.tag_annotated(annotation, message, 1) - - def get_latest_tag(self): - return self.execution_api.execute('git describe --tags --abbrev=0') - - def get_current_branch(self): - return ''.join(self.execution_api.execute('git branch --show-current')).rstrip() - - def init(self, default_branch: str = "main"): - self.execution_api.execute('git init') - self.execution_api.execute(f'git checkout -b {default_branch}') - - def set_user_config(self, email: str, name: str): - self.execution_api.execute(f'git config user.email {email}') - self.execution_api.execute(f'git config user.name {name}') - - def add_file(self, file_path: Path): - return self.execution_api.execute(f'git add {file_path}') - - def add_remote(self, origin: str, url: str): - return self.execution_api.execute(f'git remote add {origin} {url}') - - def commit(self, commit_message: str): - return self.execution_api.execute( - f'git commit -m "{commit_message}"') - - def push(self): - return self.execution_api.execute('git push') - - def checkout(self, branch: str): - return self.execution_api.execute(f'git checkout {branch}') - -class EnvironmentApi(): - - def __init__(self): - self.environ = environ - - def get(self, key): - return self.environ.get(key) - - def set(self, key, value): - self.environ[key] = value diff --git a/src/main/python/ddadevops/infrastructure/release_mixin/repo.py b/src/main/python/ddadevops/infrastructure/release_mixin/repo.py deleted file mode 100644 index dc27457..0000000 --- a/src/main/python/ddadevops/infrastructure/release_mixin/repo.py +++ /dev/null @@ -1,121 +0,0 @@ - -from src.main.python.ddadevops.domain import ( - ReleaseContext, - Version, - ReleaseType, - EnvironmentKeys, -) -from src.main.python.ddadevops.infrastructure.release_mixin.infrastructure_api import ( - FileHandler, - GitApi, - EnvironmentApi, -) - - -class VersionRepository: - def __init__(self, file): - self.file = file - self.file_handler = None - - def load_file(self): - self.file_handler = FileHandler.from_file_path(self.file) - return self.file_handler - - def write_file(self, version_string): - if self.file_handler is None: - raise RuntimeError("Version was not created by load_file method.") - self.file_handler.write(version_string) - - def parse_file(self): - version_list, is_snapshot = self.file_handler.parse() - return version_list, is_snapshot - - def get_version(self) -> Version: - self.file_handler = self.load_file() - version_list, is_snapshot = self.parse_file() - version = Version(self.file, version_list) - version.is_snapshot = is_snapshot - - return version - - -class ReleaseTypeRepository: - def __init__( - self, - git_api: GitApi = GitApi(), - environment_api: EnvironmentApi = EnvironmentApi(), - ): - self.git_api: GitApi = git_api - self.environment_api: EnvironmentApi = environment_api - self.get_from_git: bool = False - self.get_from_env: bool = False - - @classmethod - def from_git(cls, git_api: GitApi): - release_type_repo = cls(git_api=git_api) - release_type_repo.get_from_git = True - return release_type_repo - - @classmethod - def from_environment(cls, environment_api: EnvironmentApi): - release_type_repo = cls(environment_api=environment_api) - release_type_repo.get_from_env = True - return release_type_repo - - def __get_release_type_git(self) -> ReleaseType | None: - latest_commit = self.git_api.get_latest_commit().upper() - if ReleaseType.MAJOR.name in latest_commit: - return ReleaseType.MAJOR - if ReleaseType.MINOR.name in latest_commit: - return ReleaseType.MINOR - if ReleaseType.PATCH.name in latest_commit: - return ReleaseType.PATCH - if ReleaseType.SNAPSHOT.name in latest_commit: - return ReleaseType.SNAPSHOT - return None - - def __get_release_type_environment(self) -> ReleaseType | None: - release_name = self.environment_api.get( - EnvironmentKeys.DDADEVOPS_RELEASE_TYPE.name - ).upper() - - if release_name is None: - raise ValueError( - "Release Name not found. Is the Environment correctly configured?" - ) - - if ReleaseType.MAJOR.name in release_name: - return ReleaseType.MAJOR - if ReleaseType.MINOR.name in release_name: - return ReleaseType.MINOR - if ReleaseType.PATCH.name in release_name: - return ReleaseType.PATCH - if ReleaseType.SNAPSHOT.name in release_name: - return ReleaseType.SNAPSHOT - return None - - def get_release_type(self) -> ReleaseType | None: - if self.get_from_git: - return self.__get_release_type_git() - if self.get_from_env: - return self.__get_release_type_environment() - raise ValueError("No valid api passed to ReleaseTypeRepository") - - -class ReleaseContextRepository: - def __init__( - self, - version_repository: VersionRepository, - release_type_repository: ReleaseTypeRepository, - ): - self.version_repository = version_repository - self.release_type_repository = release_type_repository - - def get_release(self, main_branch: str) -> ReleaseContext: - result = ReleaseContext( - self.release_type_repository.get_release_type(), - self.version_repository.get_version(), - main_branch, - ) - result.throw_if_invalid() - return result diff --git a/src/main/python/ddadevops/infrastructure/repository.py b/src/main/python/ddadevops/infrastructure/repository.py new file mode 100644 index 0000000..057f083 --- /dev/null +++ b/src/main/python/ddadevops/infrastructure/repository.py @@ -0,0 +1,43 @@ +from pathlib import Path +from sys import stdout +from os import chmod +from subprocess import run +from pkg_resources import resource_string +import yaml +import deprecation +from ..domain import Devops, Image, C4k, Release, BuildFile +from ..python_util import execute + + +class DevopsRepository: + def get_devops(self, project) -> Devops: + devops = project.get_property("devops") + devops.throw_if_invalid() + return devops + + def set_devops(self, project, devops: Devops): + devops.throw_if_invalid() + project.set_property("devops", devops) + + +class BuildFileRepository: + def __init__(self, base_dir: str): + self.base_dir = Path(base_dir) + + def get(self, path: Path) -> BuildFile: + with open(self.base_dir.joinpath(path), "r", encoding="utf-8") as file: + content = file.read() + result = BuildFile(path, content) + result.throw_if_invalid() + return result + + def write(self, build_file: BuildFile): + build_file.throw_if_invalid() + with open( + self.base_dir.joinpath(build_file.file_path), + "r+", + encoding="utf-8", + ) as file: + file.seek(0) + file.write(build_file.content) + file.truncate() diff --git a/src/main/python/ddadevops/python_util.py b/src/main/python/ddadevops/python_util.py index 204ee4d..e872ad6 100644 --- a/src/main/python/ddadevops/python_util.py +++ b/src/main/python/ddadevops/python_util.py @@ -1,15 +1,22 @@ from subprocess import check_output, Popen, PIPE +import deprecation + +@deprecation.deprecated(deprecated_in="3.2", details="use ExecutionApi instead") def execute(cmd, shell=False): - output = check_output(cmd, encoding='UTF-8', shell=shell) + output = check_output(cmd, encoding="UTF-8", shell=shell) return output.rstrip() + +@deprecation.deprecated(deprecated_in="3.2", details="use ExecutionApi instead") def execute_live(cmd): process = Popen(cmd, stdout=PIPE) - for line in iter(process.stdout.readline, b''): - print(line.decode('utf-8'), end='') + for line in iter(process.stdout.readline, b""): + print(line.decode("utf-8"), end="") process.stdout.close() process.wait() + +@deprecation.deprecated(deprecated_in="3.2", details="use domain.filter_none instead") def filter_none(list_to_filter): return [x for x in list_to_filter if x is not None] diff --git a/src/main/python/ddadevops/release_mixin.py b/src/main/python/ddadevops/release_mixin.py index d30e6d5..79e0a25 100644 --- a/src/main/python/ddadevops/release_mixin.py +++ b/src/main/python/ddadevops/release_mixin.py @@ -1,36 +1,23 @@ from pybuilder.core import Project -from src.main.python.ddadevops.devops_build import DevopsBuild -from src.main.python.ddadevops.infrastructure.release_mixin import ReleaseContextRepository, ReleaseTypeRepository, VersionRepository, GitApi, EnvironmentApi -from src.main.python.ddadevops.application import PrepareReleaseService, TagAndPushReleaseService -from src.main.python.ddadevops.domain import Release, EnvironmentKeys +from .devops_build import DevopsBuild +from .application import ReleaseService +from .domain import MixinType + class ReleaseMixin(DevopsBuild): - def __init__(self, project: Project, release: Release): - super().__init__(project, devops=release.devops) - self.repo.set_release(self.project, release) - self.main_branch = release.main_branch - - git_api = GitApi() - environment_api = EnvironmentApi() - env_key = EnvironmentKeys.DDADEVOPS_RELEASE_TYPE.name - environment_val_set = environment_api.get(env_key) != "" and environment_api.get(env_key) is not None - - if environment_val_set: - release_type_repo = ReleaseTypeRepository.from_environment(environment_api) - else: - release_type_repo = ReleaseTypeRepository.from_git(git_api) - - version_repo = VersionRepository(release.config_file) - self.release_repo = ReleaseContextRepository(version_repo, release_type_repo) - - self.prepare_release_service = PrepareReleaseService() - self.tag_and_push_release_service = TagAndPushReleaseService(git_api, self.main_branch) + def __init__(self, project: Project, input: dict): + super().__init__(project, input) + self.release_service = ReleaseService.prod(project.basedir) + devops = self.devops_repo.get_devops(self.project) + if MixinType.RELEASE not in devops.mixins: + raise ValueError(f"ReleaseMixin requires MixinType.RELEASE") def prepare_release(self): - release = self.release_repo.get_release(self.main_branch) - self.prepare_release_service.write_and_commit_release(release, self.release_repo.version_repository) - self.prepare_release_service.write_and_commit_bump(release, self.release_repo.version_repository) + devops = self.devops_repo.get_devops(self.project) + release = devops.mixins[MixinType.RELEASE] + self.release_service.prepare_release(release) - def tag_and_push_release(self): - self.tag_and_push_release_service.tag_release(self.release_repo) - self.tag_and_push_release_service.push_release() + def tag_bump_and_push_release(self): + devops = self.devops_repo.get_devops(self.project) + release = devops.mixins[MixinType.RELEASE] + self.release_service.tag_bump_and_push_release(release) diff --git a/src/test/python/domain/helper.py b/src/test/python/domain/helper.py new file mode 100644 index 0000000..3c6fdd0 --- /dev/null +++ b/src/test/python/domain/helper.py @@ -0,0 +1,117 @@ +from pathlib import Path +from src.main.python.ddadevops.domain import DevopsFactory, Devops, Version, BuildFile + + +def devops_config(overrides: dict) -> dict: + default = { + "name": "name", + "module": "module", + "stage": "test", + "project_root_path": "root_path", + "build_dir_name": "target", + "build_types": ["IMAGE", "C4K"], + "mixin_types": ["RELEASE"], + "image_dockerhub_user": "dockerhub_user", + "image_dockerhub_password": "dockerhub_password", + "image_tag": "image_tag", + "c4k_config": {}, + "c4k_grafana_cloud_user": "user", + "c4k_grafana_cloud_password": "password", + "c4k_grafana_cloud_url": "https://prometheus-prod-01-eu-west-0.grafana.net/api/prom/push", + "c4k_auth": {}, + "release_type": "NONE", + "release_main_branch": "main", + "release_current_branch": "my_feature", + "release_primary_build_file": "./package.json", + "release_secondary_build_file": [], + "credentials_mappings": [ + { + "gopass_path": "a/path", + "gopass_field": "a-field", + }, + ], + } + input = default.copy() + input.update(overrides) + return input + + +def build_devops( + overrides: dict, version: Version = Version.from_str("1.0.0-SNAPSHOT") +) -> Devops: + return DevopsFactory().build_devops(devops_config(overrides), version=version) + + +class BuildFileRepositoryMock: + def get(self, path: Path) -> BuildFile: + return BuildFile( + Path("./package.json"), + """ +{ + "version": "1.1.5-SNAPSHOT" +} +""", + ) + + def write(self, build_file: BuildFile): + pass + + +class EnvironmentApiMock: + def __init__(self, mappings): + self.mappings = mappings + + def get(self, key): + return self.mappings.get(key, None) + + +class CredentialsApiMock: + def __init__(self, mappings): + self.mappings = mappings + + def gopass_field_from_path(self, path, field): + return self.mappings.get(f"{path}:{field}", None) + + def gopass_password_from_path(self, path): + return self.mappings.get(path, None) + + +class GitApiMock: + def get_latest_n_commits(self, n: int): + pass + + def get_latest_commit(self): + pass + + def tag_annotated(self, annotation: str, message: str, count: int): + pass + + def tag_annotated_second_last(self, annotation: str, message: str): + pass + + def get_latest_tag(self): + pass + + def get_current_branch(self): + pass + + def init(self, default_branch: str = "main"): + pass + + def set_user_config(self, email: str, name: str): + pass + + def add_file(self, file_path: Path): + pass + + def add_remote(self, origin: str, url: str): + pass + + def commit(self, commit_message: str): + pass + + def push(self): + pass + + def checkout(self, branch: str): + pass diff --git a/src/test/python/domain/test_build_file.py b/src/test/python/domain/test_build_file.py new file mode 100644 index 0000000..061427b --- /dev/null +++ b/src/test/python/domain/test_build_file.py @@ -0,0 +1,151 @@ +import pytest +from pathlib import Path +from src.main.python.ddadevops.domain import ( + BuildFileType, + BuildFile, + Version, +) + + +def test_sould_validate_build_file(): + sut = BuildFile(Path("./project.clj"), "content") + assert sut.is_valid() + + sut = BuildFile(None, "") + assert not sut.is_valid() + + sut = BuildFile(Path("./unknown.extension"), "content") + assert not sut.is_valid() + + +def test_sould_calculate_build_type(): + sut = BuildFile(Path("./project.clj"), "content") + assert sut.build_file_type() == BuildFileType.JAVA_CLOJURE + + sut = BuildFile(Path("./build.gradle"), "content") + assert sut.build_file_type() == BuildFileType.JAVA_GRADLE + + sut = BuildFile(Path("./package.json"), "content") + assert sut.build_file_type() == BuildFileType.JS + + +def test_sould_parse_and_set_js(): + sut = BuildFile( + Path("./package.json"), + """ +{ + "name":"c4k-jira", + "description": "Generate c4k yaml for a jira deployment.", + "author": "meissa GmbH", + "version": "1.1.5-SNAPSHOT", + "homepage": "https://gitlab.com/domaindrivenarchitecture/c4k-jira#readme", + "bin":{ + "c4k-jira": "./c4k-jira.js" + } +} +""", + ) + assert sut.get_version() == Version.from_str("1.1.5-SNAPSHOT") + + sut = BuildFile( + Path("./package.json"), + """ +{ + "name":"c4k-jira", +} +""", + ) + with pytest.raises(Exception): + sut.get_version() + + sut = BuildFile( + Path("./package.json"), + """ +{ + "name":"c4k-jira", + "version": "1.1.5-SNAPSHOT" +} +""", + ) + sut.set_version(Version.from_str("1.1.5-SNAPSHOT").create_major()) + assert """{ + "name": "c4k-jira", + "version": "2.0.0" +}""" == sut.content + + +def test_sould_parse_and_set_version_for_gradle(): + sut = BuildFile( + Path("./build.gradle"), + """ +version = "1.1.5-SNAPSHOT" + +""", + ) + assert sut.get_version() == Version.from_str("1.1.5-SNAPSHOT") + + sut = BuildFile( + Path("./build.gradle"), + """ +version = "1.1.5-SNAPSHOT" +""", + ) + sut.set_version(Version.from_str("1.1.5-SNAPSHOT").create_major()) + assert '\nversion = "2.0.0"\n' == sut.content + +def test_sould_parse_and_set_version_for_py(): + sut = BuildFile( + Path("./build.py"), + """ +from pybuilder.core import init, use_plugin, Author +use_plugin("python.core") + +name = "ddadevops" +version = "1.1.5-dev" +""", + ) + assert sut.get_version() == Version.from_str("1.1.5-dev") + + sut = BuildFile( + Path("./build.py"), + """ +version = "1.1.5-dev1" +""", + ) + sut.set_version(Version.from_str("1.1.5-dev1").create_major()) + assert '\nversion = "2.0.0"\n' == sut.content + + +def test_sould_parse_and_set_version_for_clj(): + sut = BuildFile( + Path("./project.clj"), + """ +(defproject org.domaindrivenarchitecture/c4k-jira "1.1.5-SNAPSHOT" + :description "jira c4k-installation package" + :url "https://domaindrivenarchitecture.org" +) +""", + ) + assert sut.get_version() == Version.from_str("1.1.5-SNAPSHOT") + + sut = BuildFile( + Path("./project.clj"), + """ +(defproject org.domaindrivenarchitecture/c4k-jira "1.1.5-SNAPSHOT" + :description "jira c4k-installation package" +) +""", + ) + sut.set_version(Version.from_str("1.1.5-SNAPSHOT").create_major()) + assert '\n(defproject org.domaindrivenarchitecture/c4k-jira "2.0.0"\n :description "jira c4k-installation package"\n)\n' == sut.content + + sut = BuildFile( + Path("./project.clj"), + """ +(defproject org.domaindrivenarchitecture/c4k-jira "1.1.5-SNAPSHOT" +:dependencies [[org.clojure/clojure "1.11.0"]] +) + """, + ) + sut.set_version(Version.from_str("1.1.5-SNAPSHOT").create_major()) + assert '\n(defproject org.domaindrivenarchitecture/c4k-jira "2.0.0"\n:dependencies [[org.clojure/clojure "1.11.0"]]\n)\n ' == sut.content diff --git a/src/test/python/domain/test_c4k.py b/src/test/python/domain/test_c4k.py new file mode 100644 index 0000000..bc69f1c --- /dev/null +++ b/src/test/python/domain/test_c4k.py @@ -0,0 +1,99 @@ +import pytest +from pathlib import Path +from src.main.python.ddadevops.domain import ( + DnsRecord, + BuildType, + C4k +) +from .helper import build_devops + + +def test_creation(): + sut = build_devops({}) + assert BuildType.C4K in sut.specialized_builds + + +def test_c4k_should_calculate_config(): + sut = build_devops({}) + with pytest.raises(Exception): + sut.specialized_builds[BuildType.C4K].config() + + sut = build_devops({}) + c4k = sut.specialized_builds[BuildType.C4K] + c4k.update_runtime_config(DnsRecord("fqdn")) + assert { + "fqdn": "fqdn", + "mon-cfg": { + "cluster-name": "module", + "cluster-stage": "test", + "grafana-cloud-url": "https://prometheus-prod-01-eu-west-0.grafana.net/api/prom/push", + }, + } == c4k.config() + + sut = build_devops( + { + "c4k_config": {"test": "test"}, + } + ) + c4k = sut.specialized_builds[BuildType.C4K] + c4k.update_runtime_config(DnsRecord("fqdn")) + assert { + "test": "test", + "fqdn": "fqdn", + "mon-cfg": { + "cluster-name": "module", + "cluster-stage": "test", + "grafana-cloud-url": "https://prometheus-prod-01-eu-west-0.grafana.net/api/prom/push", + }, + } == c4k.config() + + +def test_c4k_should_calculate_auth(): + sut = build_devops({}) + c4k = sut.specialized_builds[BuildType.C4K] + assert { + "mon-auth": {"grafana-cloud-password": "password", "grafana-cloud-user": "user"} + } == c4k.auth() + + sut = build_devops( + { + "c4k_auth": {"test": "test"}, + } + ) + c4k = sut.specialized_builds[BuildType.C4K] + assert { + "test": "test", + "mon-auth": { + "grafana-cloud-password": "password", + "grafana-cloud-user": "user", + }, + } == c4k.auth() + + +def test_c4k_build_should_calculate_command(): + sut = build_devops( + { + "project_root_path": ".", + } + ) + assert ( + "c4k-module-standalone.jar " + + "./target/name/module/out_c4k_config.yaml " + + "./target/name/module/out_c4k_auth.yaml > " + + "./target/name/module/out_module.yaml" + == sut.specialized_builds[BuildType.C4K].command(sut) + ) + + sut = build_devops( + { + "project_root_path": ".", + "c4k_executable_name": "executable_name", + } + ) + assert ( + "c4k-executable_name-standalone.jar " + + "./target/name/module/out_c4k_config.yaml " + + "./target/name/module/out_c4k_auth.yaml > " + + "./target/name/module/out_module.yaml" + == sut.specialized_builds[BuildType.C4K].command(sut) + ) diff --git a/src/test/python/domain/test_common.py b/src/test/python/domain/test_common.py new file mode 100644 index 0000000..ff606f1 --- /dev/null +++ b/src/test/python/domain/test_common.py @@ -0,0 +1,65 @@ +from pybuilder.core import Project +from pathlib import Path +from src.main.python.ddadevops.domain import ( + Validateable, + DnsRecord, + Devops, + BuildType, + Version, + ReleaseType, + Release, +) +from src.main.python.ddadevops.domain.image import Image +from .helper import build_devops + + +class MockValidateable(Validateable): + def __init__(self, value): + self.field = value + + def validate(self): + return self.__validate_is_not_empty__("field") + + +def test_should_validate_non_empty_strings(): + sut = MockValidateable("content") + assert sut.is_valid() + + sut = MockValidateable(None) + assert not sut.is_valid() + + sut = MockValidateable("") + assert not sut.is_valid() + + +def test_should_validate_non_empty_others(): + sut = MockValidateable(1) + assert sut.is_valid() + + sut = MockValidateable(1.0) + assert sut.is_valid() + + sut = MockValidateable(True) + assert sut.is_valid() + + sut = MockValidateable(None) + assert not sut.is_valid() + + +def test_validate_with_reason(): + sut = MockValidateable(None) + assert sut.validate()[0] == "Field 'field' must not be None." + + +def test_should_validate_DnsRecord(): + sut = DnsRecord(None) + assert not sut.is_valid() + + sut = DnsRecord("name") + assert not sut.is_valid() + + sut = DnsRecord("name", ipv4="1.2.3.4") + assert sut.is_valid() + + sut = DnsRecord("name", ipv6="1::") + assert sut.is_valid() diff --git a/src/test/python/domain/test_crededntials.py b/src/test/python/domain/test_crededntials.py new file mode 100644 index 0000000..d97d3b9 --- /dev/null +++ b/src/test/python/domain/test_crededntials.py @@ -0,0 +1,161 @@ +import pytest +from pathlib import Path +from src.main.python.ddadevops.domain import ( + CredentialMapping, + Credentials, + GopassType, + MixinType, +) +from .helper import build_devops + + +def test_should_create_mapping(): + sut = CredentialMapping( + { + "gopass_path": "server/meissa/grafana-cloud", + "gopass_field": "grafana-cloud-user", + } + ) + assert "grafana_cloud_user" == sut.name_for_input() + assert "GRAFANA_CLOUD_USER" == sut.name_for_environment() + assert GopassType.FIELD == sut.gopass_type() + + sut = CredentialMapping( + { + "gopass_path": "server/meissa/grafana-cloud", + "name": "grafana_cloud_password", + } + ) + assert "grafana_cloud_password" == sut.name_for_input() + assert "GRAFANA_CLOUD_PASSWORD" == sut.name_for_environment() + assert GopassType.PASSWORD == sut.gopass_type() + + sut = CredentialMapping( + { + "gopass_path": "server/meissa/grafana-cloud", + "gopass_field": "grafana-cloud-user", + "name": "gfc_user", + } + ) + assert "gfc_user" == sut.name_for_input() + assert "GFC_USER" == sut.name_for_environment() + assert GopassType.FIELD == sut.gopass_type() + + +def test_should_validate_CredentialMapping(): + sut = CredentialMapping( + { + "gopass_path": "server/meissa/grafana-cloud", + "gopass_field": "grafana-cloud-user", + } + ) + assert sut.is_valid() + + sut = CredentialMapping( + { + "gopass_path": "server/meissa/grafana-cloud", + "name": "grafana_cloud_user", + } + ) + assert sut.is_valid() + + sut = CredentialMapping( + { + "gopass_path": "server/meissa/grafana-cloud", + } + ) + assert not sut.is_valid() + + +def test_should_create_credentials(): + sut = Credentials( + { + "credentials_mapping": [ + { + "gopass_path": "server/meissa/grafana-cloud", + "gopass_field": "grafana-cloud-user", + }, + { + "gopass_path": "server/meissa/grafana-cloud", + "name": "grafana_cloud_password", + }, + ], + } + ) + assert sut + assert 2 == len(sut.mappings) + + sut = Credentials( + {}, + default_mappings=[ + { + "gopass_path": "server/meissa/grafana-cloud", + "gopass_field": "grafana-cloud-user", + }, + { + "gopass_path": "server/meissa/grafana-cloud", + "name": "grafana_cloud_password", + }, + ], + ) + assert sut + assert 2 == len(sut.mappings) + + sut = Credentials( + { + "credentials_mapping": [ + { + "gopass_path": "dome/path", + "gopass_field": "some-field", + }, + { + "gopass_path": "another_path", + "name": "grafana_cloud_password", + }, + ], + }, + default_mappings=[ + { + "gopass_path": "server/meissa/grafana-cloud", + "gopass_field": "grafana-cloud-user", + }, + { + "gopass_path": "server/meissa/grafana-cloud", + "name": "grafana_cloud_password", + }, + ], + ) + assert sut + assert 3 == len(sut.mappings) + assert sut.mappings["grafana_cloud_password"].gopass_path == "another_path" + + +def test_should_validate_credentials(): + sut = Credentials( + { + "credentials_mapping": [ + { + "gopass_path": "server/meissa/grafana-cloud", + "gopass_field": "grafana-cloud-user", + }, + { + "gopass_path": "server/meissa/grafana-cloud", + "name": "grafana_cloud_password", + }, + ], + } + ) + assert sut.is_valid() + + sut = Credentials( + { + "credentials_mapping": [ + { + "gopass_path": "server/meissa/grafana-cloud", + "gopass_field": "grafana-cloud-user", + }, + {"gopass_path": "server/meissa/grafana-cloud"}, + ], + } + ) + assert not sut.is_valid() diff --git a/src/test/python/domain/test_devops.py b/src/test/python/domain/test_devops.py new file mode 100644 index 0000000..a4c0de0 --- /dev/null +++ b/src/test/python/domain/test_devops.py @@ -0,0 +1,9 @@ +import pytest +from src.main.python.ddadevops.domain import ( + Devops, +) +from .helper import build_devops + +def test_devops_buildpath(): + sut = build_devops({'module': "cloud", 'name': "meissa"}) + assert "root_path/target/meissa/cloud" == sut.build_path() diff --git a/src/test/python/domain/test_devops_factory.py b/src/test/python/domain/test_devops_factory.py new file mode 100644 index 0000000..7287869 --- /dev/null +++ b/src/test/python/domain/test_devops_factory.py @@ -0,0 +1,63 @@ +import pytest +from src.main.python.ddadevops.domain import ( + DevopsFactory, Version +) + + +def test_devops_factory(): + with pytest.raises(Exception): + DevopsFactory().build_devops({"build_types": ["NOTEXISTING"]}) + + with pytest.raises(Exception): + DevopsFactory().build_devops( + { + "build_types": ["IMAGE"], + } + ) + + sut = DevopsFactory().build_devops( + { + "stage": "test", + "name": "mybuild", + "module": "test_image", + "project_root_path": "../../..", + "build_types": ["IMAGE"], + "mixin_types": [], + "image_dockerhub_user": "dockerhub_user", + "image_dockerhub_password": "dockerhub_password", + "image_tag": "docker_image_tag", + } + ) + assert sut is not None + + sut = DevopsFactory().build_devops( + { + "stage": "test", + "name": "mybuild", + "module": "test_image", + "project_root_path": "../../..", + "build_types": ["C4K"], + "mixin_types": [], + "c4k_grafana_cloud_user": "user", + "c4k_grafana_cloud_password": "password", + }, + Version.from_str("1.0.0") + ) + assert sut is not None + + sut = DevopsFactory().build_devops( + { + "stage": "test", + "name": "mybuild", + "module": "test_image", + "project_root_path": "../../..", + "build_types": [], + "mixin_types": ["RELEASE"], + "release_type": "NONE", + "release_main_branch": "main", + "release_current_branch": "my_feature", + "release_config_file": "project.clj", + }, + Version.from_str("1.0.0") + ) + assert sut is not None diff --git a/src/test/python/domain/test_domain.py b/src/test/python/domain/test_domain.py deleted file mode 100644 index b94b492..0000000 --- a/src/test/python/domain/test_domain.py +++ /dev/null @@ -1,226 +0,0 @@ -from pybuilder.core import Project -from pathlib import Path -from src.main.python.ddadevops.domain.common import ( - Validateable, - DnsRecord, - Devops, -) -from src.main.python.ddadevops.domain import Version, ReleaseType, Release, ReleaseContext -from src.main.python.ddadevops.domain.image import Image -from src.main.python.ddadevops.domain.c4k import C4k -from src.main.python.ddadevops.c4k_mixin import add_c4k_mixin_config - - -class MockValidateable(Validateable): - def __init__(self, value): - self.field = value - - def validate(self): - return self.__validate_is_not_empty__("field") - - -def test_should_validate_non_empty_strings(): - sut = MockValidateable("content") - assert sut.is_valid() - - sut = MockValidateable(None) - assert not sut.is_valid() - - sut = MockValidateable("") - assert not sut.is_valid() - - -def test_should_validate_non_empty_others(): - sut = MockValidateable(1) - assert sut.is_valid() - - sut = MockValidateable(1.0) - assert sut.is_valid() - - sut = MockValidateable(True) - assert sut.is_valid() - - sut = MockValidateable(None) - assert not sut.is_valid() - - -def test_validate_with_reason(): - sut = MockValidateable(None) - assert sut.validate()[0] == "Field 'field' must not be empty." - - -def test_should_validate_DnsRecord(): - sut = DnsRecord(None) - assert not sut.is_valid() - - sut = DnsRecord("name") - assert not sut.is_valid() - - sut = DnsRecord("name", ipv4="1.2.3.4") - assert sut.is_valid() - - sut = DnsRecord("name", ipv6="1::") - assert sut.is_valid() - - -def test_devops_buildpath(): - sut = Devops( - stage="test", project_root_path="../../..", module="cloud", name="meissa" - ) - assert "../../../target/meissa/cloud" == sut.build_path() - - -def test_devops_build_commons_path(): - devops = Devops( - stage="test", project_root_path="../../..", module="cloud", name="meissa" - ) - sut = Image( - dockerhub_user="user", - dockerhub_password="password", - devops = devops, - ) - assert "docker/" == sut.docker_build_commons_path() - - -def test_c4k_build_should_update_fqdn(): - project_config = { - "stage": "test", - "name": "name", - "project_root_path": "mypath", - "module": "module", - "build_dir_name": "target", - } - config = {"issuer": "staging"} - auth = { - "jvb-auth-password": "pw1", - "jicofo-auth-password": "pw2", - "jicofo-component-secret": "pw3", - } - add_c4k_mixin_config( - project_config, - config, - auth, - grafana_cloud_user="user", - grafana_cloud_password="password", - ) - - sut = C4k(project_config) - sut.update_runtime_config(DnsRecord("test.de", ipv6="1::")) - - assert { - "issuer": "staging", - "fqdn": "test.de", - "mon-cfg": { - "cluster-name": "module", - "cluster-stage": "test", - "grafana-cloud-url": "https://prometheus-prod-01-eu-west-0.grafana.net/api/prom/push", - }, - } == sut.config() - assert { - "jicofo-auth-password": "pw2", - "jicofo-component-secret": "pw3", - "jvb-auth-password": "pw1", - "mon-auth": { - "grafana-cloud-password": "password", - "grafana-cloud-user": "user", - }, - } == sut.c4k_mixin_auth - - -def test_c4k_build_should_calculate_command(): - devops = Devops(stage="test", project_root_path="", module="module", name="name") - project_config = { - "stage": "test", - "name": "name", - "project_root_path": "", - "module": "module", - "build_dir_name": "target", - } - add_c4k_mixin_config( - project_config, - {}, - {}, - grafana_cloud_user="user", - grafana_cloud_password="password", - ) - sut = C4k(project_config) - assert ( - "c4k-module-standalone.jar " - + "/target/name/module/out_c4k_config.yaml " - + "/target/name/module/out_c4k_auth.yaml > " - + "/target/name/module/out_module.yaml" - == sut.command(devops) - ) - - project_config = { - "stage": "test", - "name": "name", - "project_root_path": "", - "module": "module", - "build_dir_name": "target", - } - add_c4k_mixin_config( - project_config, - {}, - {}, - executabel_name="executabel_name", - grafana_cloud_user="user", - grafana_cloud_password="password", - ) - sut = C4k(project_config) - assert ( - "c4k-executabel_name-standalone.jar " - + "/target/name/module/out_c4k_config.yaml " - + "/target/name/module/out_c4k_auth.yaml > " - + "/target/name/module/out_module.yaml" - == sut.command(devops) - ) - -def test_version(tmp_path: Path): - version = Version(tmp_path, [1, 2, 3]) - version.increment(ReleaseType.SNAPSHOT) - assert version.get_version_string() == "1.2.3-SNAPSHOT" - assert version.version_list == [1, 2, 3] - assert version.is_snapshot - - version = Version(tmp_path, [1, 2, 3]) - version.increment(ReleaseType.BUMP) - assert version.get_version_string() == "1.2.4-SNAPSHOT" - assert version.version_list == [1, 2, 4] - assert version.is_snapshot - - version = Version(tmp_path, [1, 2, 3]) - version.increment(ReleaseType.PATCH) - assert version.get_version_string() == "1.2.4" - assert version.version_list == [1, 2, 4] - assert not version.is_snapshot - - version = Version(tmp_path, [1, 2, 3]) - version.increment(ReleaseType.MINOR) - assert version.get_version_string() == "1.3.0" - assert version.version_list == [1, 3, 0] - assert not version.is_snapshot - - version = Version(tmp_path, [1, 2, 3]) - version.increment(ReleaseType.MAJOR) - assert version.get_version_string() == "2.0.0" - assert version.version_list == [2, 0, 0] - assert not version.is_snapshot - -def test_release_context(tmp_path): - version = Version(tmp_path, [1, 2, 3]) - release = ReleaseContext(ReleaseType.MINOR, version, "main") - - release_version = release.release_version() - assert release_version.get_version_string() in '1.3.0' - - bump_version = release.bump_version() - assert bump_version.get_version_string() in "1.3.1-SNAPSHOT" - -def test_release(tmp_path): - devops = Devops(stage="test", project_root_path="", module="module", name="name") - sut = Release(devops, "main", "config_file.json") - assert not sut.is_valid() - - sut.set_release_context(ReleaseContext(ReleaseType.MINOR, Version("id", [1,2,3]), "main")) - assert sut.is_valid() diff --git a/src/test/python/domain/test_image.py b/src/test/python/domain/test_image.py new file mode 100644 index 0000000..e26c98d --- /dev/null +++ b/src/test/python/domain/test_image.py @@ -0,0 +1,14 @@ +from pybuilder.core import Project +from pathlib import Path +from src.main.python.ddadevops.domain import ( + BuildType, +) +from .helper import build_devops + + +def test_devops_build_commons_path(): + sut = build_devops({}) + image = sut.specialized_builds[BuildType.IMAGE] + assert image is not None + assert image.is_valid() + assert "docker/" == image.build_commons_path() diff --git a/src/test/python/domain/test_init_service.py b/src/test/python/domain/test_init_service.py new file mode 100644 index 0000000..686ec98 --- /dev/null +++ b/src/test/python/domain/test_init_service.py @@ -0,0 +1,58 @@ +import pytest +from src.main.python.ddadevops.domain import ( + InitService, + DevopsFactory, + Version, + MixinType, + BuildType, +) +from .helper import ( + BuildFileRepositoryMock, + EnvironmentApiMock, + CredentialsApiMock, + GitApiMock, + devops_config, +) + + +def test_should_load_build_file(): + sut = InitService( + DevopsFactory(), + BuildFileRepositoryMock(), + CredentialsApiMock({ + "server/meissa/grafana-cloud:grafana-cloud-user": "gopass-gfc-user", + "server/meissa/grafana-cloud": "gopass-gfc-password", + }), + EnvironmentApiMock({}), + GitApiMock(), + ) + assert ( + Version.from_str("1.1.5-SNAPSHOT") + == sut.initialize(devops_config({})).mixins[MixinType.RELEASE].version + ) + + +def test_should_resolve_passwords(): + sut = InitService( + DevopsFactory(), + BuildFileRepositoryMock(), + CredentialsApiMock( + { + "server/meissa/grafana-cloud:grafana-cloud-user": "gopass-gfc-user", + "server/meissa/grafana-cloud": "gopass-gfc-password", + } + ), + EnvironmentApiMock({"C4K_GRAFANA_CLOUD_USER": "env-gfc-user"}), + GitApiMock(), + ) + config = devops_config({}) + del config["c4k_grafana_cloud_user"] + del config["c4k_grafana_cloud_password"] + devops = sut.initialize(config) + c4k = devops.specialized_builds[BuildType.C4K] + assert { + "mon-auth": { + "grafana-cloud-password": "gopass-gfc-password", + "grafana-cloud-user": "env-gfc-user", + } + } == c4k.auth() diff --git a/src/test/python/domain/test_release.py b/src/test/python/domain/test_release.py new file mode 100644 index 0000000..8690fd1 --- /dev/null +++ b/src/test/python/domain/test_release.py @@ -0,0 +1,63 @@ +from pybuilder.core import Project +from pathlib import Path +from src.main.python.ddadevops.domain import ( + Validateable, + DnsRecord, + Devops, + BuildType, + MixinType, + Version, + ReleaseType, + Release, + Image, +) +from .helper import build_devops, devops_config + + +def test_sould_validate_release(): + sut = Release( + devops_config( + { + "release_type": "MINOR", + "release_current_branch": "main", + } + ), + Version.from_str("1.3.1-SNAPSHOT"), + ) + assert sut.is_valid() + + sut = Release( + devops_config( + { + "release_type": "MINOR", + "release_current_branch": "some-feature-branch", + } + ), + Version.from_str("1.3.1-SNAPSHOT"), + ) + assert not sut.is_valid() + + sut = Release( + devops_config( + { + "release_primary_build_file": 1, + } + ), + Version.from_str("1.3.1-SNAPSHOT"), + ) + assert not sut.is_valid() + + +def test_sould_validate_release(): + sut = Release( + devops_config( + { + "release_type": "MINOR", + "release_current_branch": "main", + "release_primary_build_file": "project.clj", + "release_secondary_build_files": ["package.json"], + } + ), + Version.from_str("1.3.1-SNAPSHOT"), + ) + assert ["project.clj", "package.json"] == sut.build_files() diff --git a/src/test/python/domain/test_version.py b/src/test/python/domain/test_version.py new file mode 100644 index 0000000..d601e67 --- /dev/null +++ b/src/test/python/domain/test_version.py @@ -0,0 +1,114 @@ +from pybuilder.core import Project +from pathlib import Path +from src.main.python.ddadevops.domain import ( + Version, + ReleaseType, + Image, +) +from .helper import build_devops, devops_config + + +def test_version_creation(): + sut = Version.from_str("1.2.3") + assert sut.to_string() == "1.2.3" + assert sut.version_list == [1, 2, 3] + assert sut.is_snapshot() == False + + sut = Version.from_str("1.2.3-SNAPSHOT") + assert sut.to_string() == "1.2.3-SNAPSHOT" + assert sut.version_list == [1, 2, 3] + assert sut.is_snapshot() == True + + +def test_should_validate_version_list(): + sut = Version(None) + assert not sut.is_valid() + + sut = Version([]) + assert not sut.is_valid() + + sut = Version([1, 2]) + assert not sut.is_valid() + + sut = Version([1, 2, 3]) + assert sut.is_valid() + + +def test_should_validate_parsing(): + sut = Version.from_str("1.2") + assert not sut.is_valid() + + sut = Version.from_str("1.2.3") + sut.version_list = [2, 2, 2] + assert not sut.is_valid() + + sut = Version.from_str("1.2.3") + assert sut.is_valid() + + sut = Version.from_str("1.2.3-SNAPSHOT") + assert sut.is_valid() + + sut = Version.from_str("1.2.3-dev") + assert sut.is_valid() + + +def test_should_create_patch(): + version = Version.from_str("1.2.3-SNAPSHOT") + sut = version.create_patch() + assert sut.to_string() == "1.2.3" + assert version.to_string() == "1.2.3-SNAPSHOT" + + version = Version.from_str("1.2.3") + sut = version.create_patch() + assert sut.to_string() == "1.2.4" + assert version.to_string() == "1.2.3" + + +def test_should_create_minor(): + version = Version.from_str("1.2.3-SNAPSHOT") + sut = version.create_minor() + assert sut.to_string() == "1.3.0" + + version = Version.from_str("1.2.3") + sut = version.create_minor() + assert sut.to_string() == "1.3.0" + + version = Version.from_str("1.3.0-SNAPSHOT") + sut = version.create_minor() + assert sut.to_string() == "1.3.0" + + version = Version.from_str("1.3.0") + sut = version.create_minor() + assert sut.to_string() == "1.4.0" + + +def test_should_create_major(): + version = Version.from_str("1.2.3-SNAPSHOT") + sut = version.create_major() + assert sut.to_string() == "2.0.0" + + version = Version.from_str("1.2.3") + sut = version.create_major() + assert sut.to_string() == "2.0.0" + + version = Version.from_str("1.0.0-SNAPSHOT") + sut = version.create_major() + assert sut.to_string() == "1.0.0" + + version = Version.from_str("1.0.0") + sut = version.create_major() + assert sut.to_string() == "2.0.0" + + +def test_should_create_bump(): + version = Version.from_str("1.2.3-SNAPSHOT") + sut = version.create_bump() + assert sut.to_string() == "1.2.3-SNAPSHOT" + + version = Version.from_str("1.2.3") + sut = version.create_bump("SNAPSHOT") + assert sut.to_string() == "1.2.4-SNAPSHOT" + + version = Version.from_str("1.0.0") + sut = version.create_bump("SNAPSHOT") + assert sut.to_string() == "1.0.1-SNAPSHOT" diff --git a/src/test/python/release_mixin/__init__.py b/src/test/python/release_mixin/__init__.py deleted file mode 100644 index 1514703..0000000 --- a/src/test/python/release_mixin/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .mock_infrastructure import MockReleaseRepository, MockReleaseTypeRepository, MockVersionRepository -from .mock_infrastructure_api import MockGitApi diff --git a/src/test/python/release_mixin/helper.py b/src/test/python/release_mixin/helper.py deleted file mode 100644 index c9843ca..0000000 --- a/src/test/python/release_mixin/helper.py +++ /dev/null @@ -1,12 +0,0 @@ -from pathlib import Path -from src.main.python.ddadevops.infrastructure import ExecutionApi - -class Helper(): - def __init__(self, file_name = 'config.json'): - self.TEST_FILE_NAME = file_name - self.TEST_FILE_ROOT = Path('src/test/resources/') - self.TEST_FILE_PATH = self.TEST_FILE_ROOT / self.TEST_FILE_NAME - - def copy_files(self, source: Path, target: Path): - api = ExecutionApi() - api.execute(f"cp {source} {target}") diff --git a/src/test/python/release_mixin/mock_infrastructure.py b/src/test/python/release_mixin/mock_infrastructure.py deleted file mode 100644 index 78c731b..0000000 --- a/src/test/python/release_mixin/mock_infrastructure.py +++ /dev/null @@ -1,42 +0,0 @@ -from pathlib import Path - -from src.main.python.ddadevops.domain import ReleaseType, Version, ReleaseContext - -from .mock_infrastructure_api import MockGitApi - -class MockVersionRepository(): - - def __init__(self): - self.file = None - self.file_handler = None - self.write_file_count = 0 - - def load_file(self): - pass - - def write_file(self, version_string): - self.write_file_count += 1 - pass - - def parse_file(self): - pass - - def get_version(self) -> Version: - return Version(Path(), [0,0,0]) - -class MockReleaseTypeRepository(): - def __init__(self, mock_git_api: MockGitApi): - self.git_api = mock_git_api - - def get_release_type(self): - return ReleaseType.MINOR - -class MockReleaseRepository(): - def __init__(self, version_repository: MockVersionRepository, release_type_repository: MockReleaseTypeRepository): - self.version_repository = version_repository - self.release_type_repository = release_type_repository - self.get_release_count = 0 - - def get_release(self, main_branch) -> ReleaseContext: - self.get_release_count += 1 - return ReleaseContext(self.release_type_repository.get_release_type(), self.version_repository.get_version(), main_branch) diff --git a/src/test/python/release_mixin/mock_infrastructure_api.py b/src/test/python/release_mixin/mock_infrastructure_api.py deleted file mode 100644 index 368203b..0000000 --- a/src/test/python/release_mixin/mock_infrastructure_api.py +++ /dev/null @@ -1,73 +0,0 @@ -class MockSystemApi(): - - def __init__(self): - self.stdout = [""] - self.stderr = [""] - - def run(self, args): - pass - - def run_checked(self, *args): - self.run(args) - pass - -class MockGitApi(): - - def __init__(self, commit_string = ""): - self.system_api = MockSystemApi() - self.get_latest_commit_count = 0 - self.commit_string = commit_string - self.tag_annotated_count = 0 - self.add_file_count = 0 - self.commit_count = 0 - self.push_count = 0 - - def get_latest_n_commits(self, n: int): - return " " - - def get_latest_commit(self): - self.get_latest_commit_count += 1 - return self.commit_string - - def tag_annotated(self, annotation: str, message: str, count: int): - self.tag_annotated_count += 1 - return " " - - def tag_annotated_second_last(self, annotation: str, message: str): - self.tag_annotated(annotation, message, 1) - return " " - - def get_latest_tag(self): - return " " - - def get_current_branch(self): - return " " - - def init(self): - pass - - def add_file(self, file_path): - self.add_file_count += 1 - return " " - - def commit(self, commit_message: str): - self.commit_count += 1 - return commit_message - - def push(self): - self.push_count += 1 - return " " - - def checkout(self, branch: str): - return " " - -class MockEnvironmentApi(): - - def __init__(self, environ_map): - self.environ = environ_map - - def get(self, name): - return self.environ.get(name) - - def set(self, name, value): - self.environ[name] = value diff --git a/src/test/python/release_mixin/test_infrastructure.py b/src/test/python/release_mixin/test_infrastructure.py deleted file mode 100644 index 6233c64..0000000 --- a/src/test/python/release_mixin/test_infrastructure.py +++ /dev/null @@ -1,86 +0,0 @@ -from src.main.python.ddadevops.domain import ReleaseType -from src.main.python.ddadevops.infrastructure.release_mixin import ReleaseTypeRepository, VersionRepository, ReleaseContextRepository -from .mock_infrastructure_api import MockGitApi, MockEnvironmentApi -from .helper import Helper - -def test_version_repository(tmp_path): - # init - th = Helper() - th.copy_files(th.TEST_FILE_PATH, tmp_path) - sut = VersionRepository(th.TEST_FILE_PATH) - version = sut.get_version() - - #test - assert version is not None - - -def test_release_repository(tmp_path): - # init - th = Helper() - th.copy_files( th.TEST_FILE_PATH, tmp_path) - version_repo = VersionRepository(th.TEST_FILE_PATH) - release_type_repo = ReleaseTypeRepository.from_git(MockGitApi('MINOR test')) - - # test - sut = ReleaseContextRepository(version_repo, release_type_repo) - - release = sut.get_release('main') - - assert release is not None - - -def test_release_type_repository_git(): - sut = ReleaseTypeRepository.from_git(MockGitApi('MINOR test')) - release_type = sut.get_release_type() - assert release_type is ReleaseType.MINOR - - sut = ReleaseTypeRepository.from_git(MockGitApi('MINOR bla')) - release_type = sut.get_release_type() - assert release_type is ReleaseType.MINOR - - sut = ReleaseTypeRepository.from_git(MockGitApi('Major bla')) - release_type = sut.get_release_type() - assert release_type == ReleaseType.MAJOR - - sut = ReleaseTypeRepository.from_git(MockGitApi('PATCH bla')) - release_type = sut.get_release_type() - assert release_type == ReleaseType.PATCH - - sut = ReleaseTypeRepository.from_git(MockGitApi('SNAPSHOT bla')) - release_type = sut.get_release_type() - assert release_type == ReleaseType.SNAPSHOT - - sut = ReleaseTypeRepository.from_git(MockGitApi('bla')) - release_type = sut.get_release_type() - assert release_type == None - -def test_release_type_repository_env(): - sut = ReleaseTypeRepository.from_environment(MockEnvironmentApi({'DDADEVOPS_RELEASE_TYPE': 'MINOR test'})) - release_type = sut.get_release_type() - assert release_type is ReleaseType.MINOR - - sut = ReleaseTypeRepository.from_environment(MockEnvironmentApi({'DDADEVOPS_RELEASE_TYPE': 'MINOR'})) - release_type = sut.get_release_type() - assert release_type is ReleaseType.MINOR - - sut = ReleaseTypeRepository.from_environment(MockEnvironmentApi({'DDADEVOPS_RELEASE_TYPE': 'Major bla'})) - release_type = sut.get_release_type() - assert release_type == ReleaseType.MAJOR - - sut = ReleaseTypeRepository.from_environment(MockEnvironmentApi({'DDADEVOPS_RELEASE_TYPE': 'Patch bla'})) - release_type = sut.get_release_type() - assert release_type == ReleaseType.PATCH - - sut = ReleaseTypeRepository.from_environment(MockEnvironmentApi({'DDADEVOPS_RELEASE_TYPE': 'Snapshot bla'})) - release_type = sut.get_release_type() - assert release_type == ReleaseType.SNAPSHOT - - sut = ReleaseTypeRepository.from_environment(MockEnvironmentApi({'DDADEVOPS_RELEASE_TYPE': 'Random text'})) - release_type = sut.get_release_type() - assert release_type == None - - sut = ReleaseTypeRepository.from_environment(MockEnvironmentApi({'REL_TYPE': 'Not the right variable'})) - try: - release_type = sut.get_release_type() - except: - assert release_type == None diff --git a/src/test/python/release_mixin/test_infrastructure_api.py b/src/test/python/release_mixin/test_infrastructure_api.py deleted file mode 100644 index f2ba909..0000000 --- a/src/test/python/release_mixin/test_infrastructure_api.py +++ /dev/null @@ -1,103 +0,0 @@ -from pathlib import Path -import pytest as pt - -from src.main.python.ddadevops.infrastructure.release_mixin import GitApi, EnvironmentApi, JsonFileHandler -from src.main.python.ddadevops.infrastructure.release_mixin import VersionRepository -from src.main.python.ddadevops.domain.release import ReleaseType - -from .helper import Helper - -def change_test_dir( tmp_path: Path, monkeypatch: pt.MonkeyPatch): - monkeypatch.chdir(tmp_path) - -def test_environment_api(): - # init - env_api = EnvironmentApi() - key = "TEST_ENV_KEY" - value = "data" - env_api.set(key, value) - - #test - assert env_api.get(key) == value - -def test_git_api(tmp_path: Path, monkeypatch: pt.MonkeyPatch): - # init - th = Helper() - th.copy_files(th.TEST_FILE_PATH, tmp_path) - - # change the context of the script execution to tmp_path - change_test_dir(tmp_path, monkeypatch) - - git_api = GitApi() - git_api.init() - git_api.set_user_config("ex.ample@mail.com", "Ex Ample") - git_api.add_file(th.TEST_FILE_NAME) - git_api.commit("MINOR release") - - # test - latest_commit = git_api.get_latest_commit() - assert "MINOR release" in latest_commit - -# file handler tests -def test_gradle(tmp_path): - # init - th = Helper('config.gradle') - th.copy_files(th.TEST_FILE_PATH, tmp_path) - th.TEST_FILE_PATH = tmp_path / th.TEST_FILE_NAME - - # test - repo = VersionRepository(th.TEST_FILE_PATH) - version = repo.get_version() - version = version.create_release_version(ReleaseType.SNAPSHOT) - repo.write_file(version.get_version_string()) - - # check - assert 'version = "12.4.678-SNAPSHOT"' in th.TEST_FILE_PATH.read_text() - - -def test_json(tmp_path): - # init - th = Helper('config.json') - th.copy_files(th.TEST_FILE_PATH, tmp_path) - th.TEST_FILE_PATH = tmp_path / th.TEST_FILE_NAME - - # test - repo = VersionRepository(th.TEST_FILE_PATH) - version = repo.get_version() - version = version.create_release_version(ReleaseType.SNAPSHOT) - repo.write_file(version.get_version_string()) - - # check - assert '"version": "123.123.456-SNAPSHOT"' in th.TEST_FILE_PATH.read_text() - - -def test_clojure(tmp_path): - # init - th = Helper('config.clj') - th.copy_files(th.TEST_FILE_PATH, tmp_path) - th.TEST_FILE_PATH = tmp_path / th.TEST_FILE_NAME - - # test - repo = VersionRepository(th.TEST_FILE_PATH) - version = repo.get_version() - version = version.create_release_version(ReleaseType.SNAPSHOT) - repo.write_file(version.get_version_string()) - - # check - assert '1.1.3-SNAPSHOT' in th.TEST_FILE_PATH.read_text() - - -def test_python(tmp_path): - # init - th = Helper('config.py') - th.copy_files(th.TEST_FILE_PATH, tmp_path) - th.TEST_FILE_PATH = tmp_path / th.TEST_FILE_NAME - - # test - repo = VersionRepository(th.TEST_FILE_PATH) - version = repo.get_version() - version = version.create_release_version(ReleaseType.SNAPSHOT) - repo.write_file(version.get_version_string()) - - # check - assert '3.1.3-SNAPSHOT' in th.TEST_FILE_PATH.read_text() diff --git a/src/test/python/release_mixin/test_release_mixin.py b/src/test/python/release_mixin/test_release_mixin.py deleted file mode 100644 index 62a890d..0000000 --- a/src/test/python/release_mixin/test_release_mixin.py +++ /dev/null @@ -1,80 +0,0 @@ -import pytest as pt -from pathlib import Path -from pybuilder.core import Project - -from src.main.python.ddadevops.release_mixin import ReleaseMixin -from src.main.python.ddadevops.infrastructure.release_mixin import GitApi, EnvironmentApi -from src.main.python.ddadevops.domain import Devops, ReleaseContext, Release - -from .helper import Helper - -MAIN_BRANCH = 'main' -STAGE = 'test' -PROJECT_ROOT_PATH = '.' -MODULE = 'test' -BUILD_DIR_NAME = "build_dir" - -def change_test_dir( tmp_path: Path, monkeypatch: pt.MonkeyPatch): - monkeypatch.chdir(tmp_path) - -class MyBuild(ReleaseMixin): - pass - -def initialize_with_object(project, CONFIG_FILE): - project.build_depends_on('ddadevops>=3.1.2') - devops = Devops(STAGE, PROJECT_ROOT_PATH, MODULE, "release_test", BUILD_DIR_NAME) - release = Release(devops, MAIN_BRANCH, CONFIG_FILE) - build = MyBuild(project, release) - return build - -def test_release_mixin_git(tmp_path: Path, monkeypatch: pt.MonkeyPatch): - - # init - th = Helper() - th.copy_files(th.TEST_FILE_PATH, tmp_path) - th.TEST_FILE_PATH = tmp_path / th.TEST_FILE_NAME - - change_test_dir(tmp_path, monkeypatch) - project = Project(tmp_path) - - git_api = GitApi() - git_api.init() - git_api.set_user_config("ex.ample@mail.com", "Ex Ample") - git_api.add_file(th.TEST_FILE_NAME) - git_api.commit("MAJOR release") - - build = initialize_with_object(project, th.TEST_FILE_PATH) - build.prepare_release() - release_version = build.release_repo.version_repository.get_version() - - # test - assert "124.0.1-SNAPSHOT" in release_version.get_version_string() - -def test_release_mixin_environment(tmp_path: Path, monkeypatch: pt.MonkeyPatch): - - # init - th = Helper() - th.copy_files(th.TEST_FILE_PATH, tmp_path) - th.TEST_FILE_PATH = tmp_path / th.TEST_FILE_NAME - - change_test_dir(tmp_path, monkeypatch) - project = Project(tmp_path) - - git_api = GitApi() - git_api.init() - git_api.set_user_config("ex.ample@mail.com", "Ex Ample") - git_api.add_file(th.TEST_FILE_NAME) - git_api.commit("Commit Message") - - environment_api = EnvironmentApi() - environment_api.set("DDADEVOPS_RELEASE_TYPE", "MAJOR") - - build = initialize_with_object(project, th.TEST_FILE_PATH) - build.prepare_release() - release_version = build.release_repo.version_repository.get_version() - - # test - assert "124.0.1-SNAPSHOT" in release_version.get_version_string() - - # tear down - environment_api.set("DDADEVOPS_RELEASE_TYPE", "") diff --git a/src/test/python/release_mixin/test_services.py b/src/test/python/release_mixin/test_services.py deleted file mode 100644 index c7dbf80..0000000 --- a/src/test/python/release_mixin/test_services.py +++ /dev/null @@ -1,32 +0,0 @@ -from src.main.python.ddadevops.application import PrepareReleaseService, TagAndPushReleaseService -from src.test.python.release_mixin import MockReleaseRepository, MockReleaseTypeRepository, MockVersionRepository -from src.test.python.release_mixin import MockGitApi - -def test_prepare_release_service(): - # init - mock_release_repo = MockReleaseRepository(MockVersionRepository(), MockReleaseTypeRepository(MockGitApi())) - prepare_release_service = PrepareReleaseService() - prepare_release_service.git_api = MockGitApi() - prepare_release_service.write_and_commit_release(mock_release_repo.get_release("main"), mock_release_repo.version_repository) - - #test - assert prepare_release_service.git_api.add_file_count == 1 - assert prepare_release_service.git_api.commit_count == 1 - - # init - prepare_release_service.write_and_commit_bump(mock_release_repo.get_release("main"), mock_release_repo.version_repository) - - # test - assert prepare_release_service.git_api.add_file_count == 2 - assert prepare_release_service.git_api.commit_count == 2 - -def test_tag_and_push_release_service(): - # init - mock_release_repo = MockReleaseRepository(MockVersionRepository(), MockReleaseTypeRepository(MockGitApi())) - tag_and_push_release_service = TagAndPushReleaseService(MockGitApi(), "main") - tag_and_push_release_service.tag_release(mock_release_repo) - tag_and_push_release_service.push_release() - - #test - assert tag_and_push_release_service.git_api.tag_annotated_count == 1 - assert tag_and_push_release_service.git_api.push_count == 1 diff --git a/src/test/python/resource_helper.py b/src/test/python/resource_helper.py new file mode 100644 index 0000000..295e7fc --- /dev/null +++ b/src/test/python/resource_helper.py @@ -0,0 +1,8 @@ +from pathlib import Path +from src.main.python.ddadevops.infrastructure import ExecutionApi + + +def copy_resource(source: Path, target: Path): + api = ExecutionApi() + res_source = Path('src/test/resources/').joinpath(source) + api.execute(f"cp {str(res_source)} {str(target)}") diff --git a/src/test/python/test_c4k_build.py b/src/test/python/test_c4k_build.py new file mode 100644 index 0000000..f5a3a46 --- /dev/null +++ b/src/test/python/test_c4k_build.py @@ -0,0 +1,42 @@ +import os +from pybuilder.core import Project +from src.main.python.ddadevops.domain import DnsRecord +from src.main.python.ddadevops.c4k_build import C4kBuild, add_c4k_mixin_config +from .domain.helper import ( + CredentialsApiMock, + devops_config, +) + + +def test_c4k_build(tmp_path): + str_tmp_path = str(tmp_path) + project = Project(str_tmp_path, name="name") + + os.environ["C4K_GRAFANA_CLOUD_USER"] = "user" + os.environ["C4K_GRAFANA_CLOUD_PASSWORD"] = "password" + + sut = C4kBuild( + project, + devops_config( + { + "project_root_path": str_tmp_path, + "mixin_types": [], + "build_types": ["C4K"], + "module": "c4k-test", + "c4k_config": {"a": 1, "b": 2}, + "c4k_auth": {"c": 3, "d": 4}, + "c4k_grafana_cloud_user": "user", + "c4k_grafana_cloud_password": "password", + } + ), + ) + + sut.initialize_build_dir() + assert sut.build_path() == f"{str_tmp_path}/target/name/c4k-test" + + sut.update_runtime_config(DnsRecord("test.de", ipv6="::1")) + sut.write_c4k_config() + assert os.path.exists(f"{sut.build_path()}/out_c4k_config.yaml") + + sut.write_c4k_auth() + assert os.path.exists(f"{sut.build_path()}/out_c4k_auth.yaml") diff --git a/src/test/python/test_c4k_mixin.py b/src/test/python/test_c4k_mixin.py deleted file mode 100644 index c4c2b17..0000000 --- a/src/test/python/test_c4k_mixin.py +++ /dev/null @@ -1,46 +0,0 @@ -import os -from pybuilder.core import Project -from src.main.python.ddadevops.domain import DnsRecord -from src.main.python.ddadevops.c4k_mixin import C4kBuild, add_c4k_mixin_config - -class MyC4kBuild(C4kBuild): - pass - -def test_c4k_mixin(tmp_path): - - build_dir = 'build' - project_name = 'testing-project' - module_name = 'c4k-test' - tmp_path_str = str(tmp_path) - - project = Project(tmp_path_str, name=project_name) - - project_config = { - 'stage': 'test', - 'project_root_path': tmp_path_str, - 'module': module_name, - 'build_dir_name': build_dir - } - - config = {'a': 1, 'b': 2} - auth = {'c': 3, 'd': 4} - - add_c4k_mixin_config(project_config, config, auth, grafana_cloud_user='user', grafana_cloud_password='password') - - assert project_config.get('C4kMixin') is not None - - mixin = MyC4kBuild(project, project_config) - mixin.initialize_build_dir() - assert mixin.build_path() == f'{tmp_path_str}/{build_dir}/{project_name}/{module_name}' - - mixin.update_runtime_config(DnsRecord('test.de', ipv6="1::")) - sut = mixin.repo.get_c4k(mixin.project) - assert 'fqdn' in sut.config() - assert 'mon-cfg' in sut.config() - assert 'mon-auth' in sut.c4k_mixin_auth - - mixin.write_c4k_config() - assert os.path.exists(f'{mixin.build_path()}/out_c4k_config.yaml') - - mixin.write_c4k_auth() - assert os.path.exists(f'{mixin.build_path()}/out_c4k_auth.yaml') diff --git a/src/test/python/test_devops_build.py b/src/test/python/test_devops_build.py index ca37caa..54ff6b3 100644 --- a/src/test/python/test_devops_build.py +++ b/src/test/python/test_devops_build.py @@ -1,27 +1,25 @@ import os +from pathlib import Path from pybuilder.core import Project -from src.main.python.ddadevops.domain.common import Devops -from src.main.python.ddadevops.devops_build import DevopsBuild - - -class MyDevopsBuild(DevopsBuild): - pass +from src.main.python.ddadevops import DevopsBuild +from .domain.helper import devops_config +from .resource_helper import copy_resource def test_devops_build(tmp_path): - build_dir = "build" - project_name = "testing-project" - module_name = "c4k-test" - tmp_path_str = str(tmp_path) + str_tmp_path = str(tmp_path) + copy_resource(Path("package.json"), tmp_path) + project = Project(str_tmp_path, name="name") - project = Project(tmp_path_str, name=project_name) - devops = Devops( - stage="test", - project_root_path=tmp_path_str, - module=module_name, - build_dir_name=build_dir, + devops_build = DevopsBuild( + project, + devops_config( + { + "project_root_path": str_tmp_path, + "build_types": [], + "mixin_types": [], + } + ), ) - - devops_build = DevopsBuild(project, devops=devops) devops_build.initialize_build_dir() assert os.path.exists(f"{devops_build.build_path()}") diff --git a/src/test/python/test_image_build.py b/src/test/python/test_image_build.py index 9657085..cfcb561 100644 --- a/src/test/python/test_image_build.py +++ b/src/test/python/test_image_build.py @@ -1,25 +1,24 @@ import os from pybuilder.core import Project -from src.main.python.ddadevops.domain import Image, Devops -from src.main.python.ddadevops.devops_image_build import DevopsImageBuild +from src.main.python.ddadevops import DevopsImageBuild +from .domain.helper import devops_config def test_devops_docker_build(tmp_path): - build_dir = "build" - project_name = "testing-project" - module_name = "docker-test" - tmp_path_str = str(tmp_path) + str_tmp_path = str(tmp_path) + project = Project(str_tmp_path, name="name") - project = Project(tmp_path_str, name=project_name) - devops = Devops( - stage="test", - project_root_path=tmp_path_str, - module=module_name, - name=project_name, - build_dir_name=build_dir + os.environ["IMAGE_DOCKERHUB_USER"] = "user" + os.environ["IMAGE_DOCKERHUB_PASSWORD"] = "password" + + image_build = DevopsImageBuild( + project, + devops_config( + { + "project_root_path": str_tmp_path, + "build_types": ["IMAGE"], + "mixin_types": [], + } + ), ) - image = Image(dockerhub_user="user", dockerhub_password="password", devops=devops) - - docker_build = DevopsImageBuild(project, image=image) - # docker_build.initialize_build_dir() - # assert os.path.exists(f"{docker_build.build_path()}") + assert image_build diff --git a/src/test/python/test_release_mixin.py b/src/test/python/test_release_mixin.py new file mode 100644 index 0000000..a266630 --- /dev/null +++ b/src/test/python/test_release_mixin.py @@ -0,0 +1,82 @@ +import pytest as pt +import os +from pathlib import Path +from pybuilder.core import Project + +from src.main.python.ddadevops.release_mixin import ReleaseMixin +from src.main.python.ddadevops.domain import Devops, Release +from .domain.helper import devops_config +from .resource_helper import copy_resource + +def test_release_mixin(tmp_path): + str_tmp_path = str(tmp_path) + copy_resource(Path('package.json'), tmp_path) + project = Project(str_tmp_path, name="name") + + sut = ReleaseMixin( + project, + devops_config( + { + "project_root_path": str_tmp_path, + "mixin_types": ["RELEASE"], + "build_types": [], + "module": "release-test", + } + ), + ) + + sut.initialize_build_dir() + assert sut.build_path() == f"{str_tmp_path}/target/name/release-test" + + +# def test_release_mixin_git(tmp_path: Path, monkeypatch: pt.MonkeyPatch): +# # init +# th = ResourceHelper() +# th.copy_files(th.TEST_FILE_PATH, tmp_path) +# th.TEST_FILE_PATH = tmp_path / th.TEST_FILE_NAME + +# change_test_dir(tmp_path, monkeypatch) +# project = Project(tmp_path) + +# git_api = GitApi() +# git_api.init() +# git_api.set_user_config("ex.ample@mail.com", "Ex Ample") +# git_api.add_file(th.TEST_FILE_NAME) +# git_api.commit("MAJOR release") + +# build = initialize_with_object(project, th.TEST_FILE_PATH) +# build.prepare_release() +# release_version = build.release_repo.version_repository.get_version() + +# # test +# assert "124.0.1-SNAPSHOT" in release_version.get_version_string() + + +# def test_release_mixin_environment(tmp_path: Path, monkeypatch: pt.MonkeyPatch): + +# # init +# th = Helper() +# th.copy_files(th.TEST_FILE_PATH, tmp_path) +# th.TEST_FILE_PATH = tmp_path / th.TEST_FILE_NAME + +# change_test_dir(tmp_path, monkeypatch) +# project = Project(tmp_path) + +# git_api = GitApi() +# git_api.init() +# git_api.set_user_config("ex.ample@mail.com", "Ex Ample") +# git_api.add_file(th.TEST_FILE_NAME) +# git_api.commit("Commit Message") + +# environment_api = EnvironmentApi() +# environment_api.set("DDADEVOPS_RELEASE_TYPE", "MAJOR") + +# build = initialize_with_object(project, th.TEST_FILE_PATH) +# build.prepare_release() +# release_version = build.release_repo.version_repository.get_version() + +# # test +# assert "124.0.1-SNAPSHOT" in release_version.get_version_string() + +# # tear down +# environment_api.set("DDADEVOPS_RELEASE_TYPE", "") diff --git a/src/test/resources/package.json b/src/test/resources/package.json new file mode 100644 index 0000000..d78ae03 --- /dev/null +++ b/src/test/resources/package.json @@ -0,0 +1,33 @@ +{ + "name": "c4k-jira", + "description": "Generate c4k yaml for a jira deployment.", + "author": "meissa GmbH", + "version": "1.1.5-SNAPSHOT", + "homepage": "https://gitlab.com/domaindrivenarchitecture/c4k-jira#readme", + "repository": "https://www.npmjs.com/package/c4k-jira", + "license": "APACHE2", + "main": "c4k-jira.js", + "bin": { + "c4k-jira": "./c4k-jira.js" + }, + "keywords": [ + "cljs", + "jira", + "k8s", + "c4k", + "deployment", + "yaml", + "convention4kubernetes" + ], + "bugs": { + "url": "https://gitlab.com/domaindrivenarchitecture/c4k-jira/issues" + }, + "dependencies": { + "js-base64": "^3.6.1", + "js-yaml": "^4.0.0" + }, + "devDependencies": { + "shadow-cljs": "^2.11.18", + "source-map-support": "^0.5.19" + } +} diff --git a/src/test/resources/project.clj b/src/test/resources/project.clj new file mode 100644 index 0000000..36ada11 --- /dev/null +++ b/src/test/resources/project.clj @@ -0,0 +1,47 @@ +(defproject org.domaindrivenarchitecture/c4k-jira "1.1.5-SNAPSHOT" + :description "jira c4k-installation package" + :url "https://domaindrivenarchitecture.org" + :license {:name "Apache License, Version 2.0" + :url "https://www.apache.org/licenses/LICENSE-2.0.html"} + :dependencies [[org.clojure/clojure "1.11.1"] + [org.clojure/tools.reader "1.3.6"] + [org.domaindrivenarchitecture/c4k-common-clj "2.0.3"] + [hickory "0.7.1"]] + :target-path "target/%s/" + :source-paths ["src/main/cljc" + "src/main/clj"] + :resource-paths ["src/main/resources"] + :repositories [["snapshots" :clojars] + ["releases" :clojars]] + :deploy-repositories [["snapshots" {:sign-releases false :url "https://clojars.org/repo"}] + ["releases" {:sign-releases false :url "https://clojars.org/repo"}]] + :profiles {:test {:test-paths ["src/test/cljc"] + :resource-paths ["src/test/resources"] + :dependencies [[dda/data-test "0.1.1"]]} + :dev {:plugins [[lein-shell "0.5.0"]]} + :uberjar {:aot :all + :main dda.c4k-jira.uberjar + :uberjar-name "c4k-jira-standalone.jar" + :dependencies [[org.clojure/tools.cli "1.0.214"] + [ch.qos.logback/logback-classic "1.4.5" + :exclusions [com.sun.mail/javax.mail]] + [org.slf4j/jcl-over-slf4j "2.0.6"]]}} + :release-tasks [["test"] + ["vcs" "assert-committed"] + ["change" "version" "leiningen.release/bump-version" "release"] + ["vcs" "commit"] + ["vcs" "tag" "v" "--no-sign"] + ["change" "version" "leiningen.release/bump-version"]] + :aliases {"native" ["shell" + "native-image" + "--report-unsupported-elements-at-runtime" + "--initialize-at-build-time" + "-jar" "target/uberjar/c4k-jira-standalone.jar" + "-H:ResourceConfigurationFiles=graalvm-resource-config.json" + "-H:Log=registerResource" + "-H:Name=target/graalvm/${:name}"] + "inst" ["shell" "sudo" + "install" + "-m=755" + "target/uberjar/c4k-jira-standalone.jar" + "/usr/local/bin/c4k-jira-standalone.jar"]})