diff --git a/doc/architecture/Domain.md b/doc/architecture/Domain.md index d65a55e..ce9f80b 100644 --- a/doc/architecture/Domain.md +++ b/doc/architecture/Domain.md @@ -45,7 +45,7 @@ classDiagram Devops *-- "0..1" Image: spcialized_builds Devops *-- "0..1" C4k: spcialized_builds - Devops *-- Release: release + Devops *-- "0..1" Release: mixins C4k *-- DnsRecord Release *-- "0..1" ReleaseContext diff --git a/src/main/python/ddadevops/domain/common.py b/src/main/python/ddadevops/domain/common.py index 1a3594c..0a599cf 100644 --- a/src/main/python/ddadevops/domain/common.py +++ b/src/main/python/ddadevops/domain/common.py @@ -4,13 +4,20 @@ from typing import List, TypedDict import logging import deprecation + def filter_none(list_to_filter): return [x for x in list_to_filter if x is not None] + class BuildType(Enum): IMAGE = 0 C4K = 1 + +class MixinType(Enum): + RELEASE = 0 + + class Validateable: def __validate_is_not_none__(self, field_name: str) -> List[str]: value = self.__dict__[field_name] @@ -32,9 +39,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 @@ -50,13 +58,19 @@ class DnsRecord(Validateable): class Devops(Validateable): - def __init__(self, input: dict, specialized_builds: dict[BuildType, Validateable]): - 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 + def __init__( + self, + input: dict, + specialized_builds: dict[BuildType, Validateable], + mixins: dict[MixinType, Validateable], + ): + 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] @@ -71,6 +85,9 @@ class Devops(Validateable): 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): diff --git a/src/main/python/ddadevops/domain/devops_factory.py b/src/main/python/ddadevops/domain/devops_factory.py index a16a0b9..caac032 100644 --- a/src/main/python/ddadevops/domain/devops_factory.py +++ b/src/main/python/ddadevops/domain/devops_factory.py @@ -1,9 +1,11 @@ import deprecation from enum import Enum from typing import List -from .common import Devops, BuildType +from .common import Devops, BuildType, MixinType from .image import Image from .c4k import C4k +from .release import Release, ReleaseContext + class DevopsFactory: def __init__(self): @@ -11,13 +13,20 @@ class DevopsFactory: def build_devops(self, input) -> 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) - devops = Devops(input, specialized_builds=specialized_builds) + mixins = {} + if MixinType.RELEASE in mixin_types: + release_context = ReleaseContext(input) + mixins[MixinType.RELEASE] = Release(input, release_context=release_context) + + devops = Devops(input, specialized_builds=specialized_builds, mixins=mixins) devops.throw_if_invalid() @@ -31,3 +40,9 @@ class DevopsFactory: 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/release.py b/src/main/python/ddadevops/domain/release.py index 2c27b4a..b10a085 100644 --- a/src/main/python/ddadevops/domain/release.py +++ b/src/main/python/ddadevops/domain/release.py @@ -6,18 +6,21 @@ from .common import ( Devops, ) + class ReleaseType(Enum): MAJOR = 0 MINOR = 1 PATCH = 2 SNAPSHOT = 3 BUMP = None + NONE = 15 + class EnvironmentKeys(Enum): DDADEVOPS_RELEASE_TYPE = 0 -class Version(): +class Version(Validateable): def __init__(self, path: Path, version_list: list): self.path = path self.version_list = version_list @@ -62,11 +65,25 @@ class Version(): 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 - self.version = version - self.current_branch = current_branch + def __init__( + self, + input: dict, + ): + self.release_type = ReleaseType[input.get("release_type", "SNAPSHOT")] + self.release_current_version = input.get("release_current_version") + self.release_current_branch = input.get("release_current_branch") + self.version = self.__version_from_str__() + + # TODO: mv version parsing to version + def __version_from_str__(self): + if not self.release_current_version: + return + version_parsed = [] + for x in self.release_current_version.split("."): + version_parsed += [int(x)] + return Version("unused", version_parsed) def release_version(self) -> Version: return self.version.create_release_version(self.release_type) @@ -77,30 +94,32 @@ class ReleaseContext(Validateable): def validate(self): result = [] result += self.__validate_is_not_empty__("release_type") - result += self.__validate_is_not_empty__("version") - result += self.__validate_is_not_empty__("current_branch") + result += self.__validate_is_not_empty__("release_current_version") + result += self.__validate_is_not_empty__("release_current_branch") + if self.version: + result += self.version.validate() return result def validate_branch(self, main_branch: str): result = [] - if self.release_type is not None and main_branch != self.current_branch: + if ( + self.release_type is not None + and self.release_type != ReleaseType.NONE + and main_branch != self.release_current_branch + ): result.append(f"Releases are allowed only on {main_branch}") return result + class Release(Validateable): def __init__( self, - devops: Devops, - main_branch: str, - config_file: str, + input: dict, + release_context: ReleaseContext, ): - 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 + self.release_main_branch = input.get("release_main_branch", "main") + self.release_config_file = input.get("release_config_file", "project.clj") + self.release_context = release_context def release_version(self): return self.release_context.release_version() @@ -108,14 +127,12 @@ class Release(Validateable): 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_main_branch") + result += self.__validate_is_not_empty__("release_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) + result += self.release_context.validate_branch(self.release_main_branch) return result - \ No newline at end of file diff --git a/src/test/python/domain/test_devops_factory.py b/src/test/python/domain/test_devops_factory.py index 056e75c..ab4e414 100644 --- a/src/test/python/domain/test_devops_factory.py +++ b/src/test/python/domain/test_devops_factory.py @@ -22,12 +22,13 @@ def test_devops_factory(): "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 != None + assert sut is not None sut = DevopsFactory().build_devops( { @@ -36,8 +37,26 @@ def test_devops_factory(): "module": "test_image", "project_root_path": "../../..", "build_types": ["C4K"], + "mixin_types": [], "c4k_grafana_cloud_user": "user", "c4k_grafana_cloud_password": "password", } ) - assert sut != None + 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_main_branch": "main", + "release_config_file": "project.clj", + "release_type": "NONE", + "release_current_version": "1.0.0", + "release_current_branch": "my_feature", + } + ) + assert sut is not None diff --git a/src/test/python/domain/test_helper.py b/src/test/python/domain/test_helper.py index 4d96b4e..391f729 100644 --- a/src/test/python/domain/test_helper.py +++ b/src/test/python/domain/test_helper.py @@ -9,14 +9,20 @@ def devops_config(overrides: dict) -> dict: "project_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_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': {}, + "c4k_auth": {}, + "release_main_branch": "main", + "release_config_file": "project.clj", + "release_type": "NONE", + "release_current_version": "1.0.0", + "release_current_branch": "my_feature", } input = default.copy() input.update(overrides) diff --git a/src/test/python/domain/test_release.py b/src/test/python/domain/test_release.py index 5af241b..b81d29a 100644 --- a/src/test/python/domain/test_release.py +++ b/src/test/python/domain/test_release.py @@ -5,6 +5,7 @@ from src.main.python.ddadevops.domain.common import ( DnsRecord, Devops, BuildType, + MixinType, ) from src.main.python.ddadevops.domain import ( Version, @@ -13,7 +14,8 @@ from src.main.python.ddadevops.domain import ( ReleaseContext, ) from src.main.python.ddadevops.domain.image import Image -from .test_helper import build_devops +from .test_helper import build_devops, devops_config + def test_version(tmp_path: Path): version = Version(tmp_path, [1, 2, 3]) @@ -48,22 +50,26 @@ def test_version(tmp_path: Path): 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" + sut = ReleaseContext( + devops_config( + { + "release_type": "MINOR", + "release_current_version": "1.2.3", + "release_current_branch": "main", + } + ) + ) + assert sut.release_version().get_version_string() == "1.3.0" + assert sut.bump_version().get_version_string() == "1.3.1-SNAPSHOT" def test_release(tmp_path): - devops = build_devops({}) - 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") + sut = build_devops( + { + "release_type": "MINOR", + "release_current_version": "1.2.3", + "release_current_branch": "main", + } ) + assert sut.mixins[MixinType.RELEASE] assert sut.is_valid()