Merge branch 'introduce-domain-factories' into 'ddd-intro'

refactoring to ddd

See merge request domaindrivenarchitecture/dda-devops-build!12
This commit is contained in:
Michael Jerger 2023-05-20 11:44:52 +00:00
commit 22ad95737e
60 changed files with 2235 additions and 1557 deletions

View file

@ -4,6 +4,9 @@ before_script:
- python --version - python --version
- python -m pip install --upgrade pip - python -m pip install --upgrade pip
- pip install -r requirements.txt - pip install -r requirements.txt
- export IMAGE_TAG=$CI_IMAGE_TAG
- export IMAGE_DOCKERHUB_USER=&DOCKERHUB_USER
- export IMAGE_DOCKERHUB_PASSWORD=$DOCKERHUB_PASSWORD
stages: stages:
- lint&test - lint&test

View file

@ -28,7 +28,7 @@ use_plugin("python.distutils")
default_task = "publish" default_task = "publish"
name = "ddadevops" 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" summary = "tools to support builds combining gopass, terraform, dda-pallet, aws & hetzner-cloud"
description = __doc__ description = __doc__
authors = [Author("meissa GmbH", "buero@meissa-gmbh.de")] authors = [Author("meissa GmbH", "buero@meissa-gmbh.de")]

View file

@ -3,25 +3,26 @@
```mermaid ```mermaid
classDiagram classDiagram
class Devops { class Devops {
stage <<AggregateRoot>>
name name
project_root_path
module module
stage
build_dir_name build_dir_name
project_root_path
} }
class Image { class Image {
dockerhub_user image_dockerhub_user
dockerhub_password image_dockerhub_password
build_dir_name image_publish_tag
use_package_common_files image_build_dir_name
build_commons_path image_use_package_common_files
docker_build_commons_dir_name image_build_commons_path
docker_publish_tag image_build_commons_dir_name
} }
class C4k { class C4k {
executabel_name c4k_executabel_name
c4k_mixin_config c4k_mixin_config
c4k_mixin_auth c4k_mixin_auth
} }
@ -33,18 +34,49 @@ classDiagram
} }
class Release { class Release {
main_branch
config_file
}
class ReleaseContext {
release_type release_type
release_main_branch
release_current_branch
version version
current_branch }
class Credentials {
<<AggregateRoot>>
}
class CredentialMapping {
name
gopass_path
gopass_field
gopass_type()
name_for_input()
name_for_environment ()
} }
class BuildFile {
<<AggregateRoot>>
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 C4k *-- DnsRecord
Image *-- Devops Credentials *-- "0..n" CredentialMapping: mappings
Release *-- "0..1" ReleaseContext
``` ```

View file

@ -3,38 +3,25 @@ from pybuilder.core import task, init
from ddadevops import * from ddadevops import *
name = "clojure" name = "clojure"
MODULE = "docker" MODULE = "image"
PROJECT_ROOT_PATH = "../.." PROJECT_ROOT_PATH = "../.."
@init @init
def initialize(project): 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") 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( build = DevopsImageBuild(project, input)
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.initialize_build_dir() build.initialize_build_dir()

View file

@ -3,38 +3,25 @@ from pybuilder.core import task, init
from ddadevops import * from ddadevops import *
name = "devops-build" name = "devops-build"
MODULE = "docker" MODULE = "image"
PROJECT_ROOT_PATH = "../.." PROJECT_ROOT_PATH = "../.."
@init @init
def initialize(project): 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") 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( build = DevopsImageBuild(project, input)
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.initialize_build_dir() build.initialize_build_dir()

View file

@ -5,3 +5,4 @@ dda-python-terraform==2.0.1
packaging packaging
boto3 boto3
pyyaml pyyaml
inflection

View file

@ -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_rds_pg_mixin import AwsRdsPgMixin, add_aws_rds_pg_mixin_config
from .aws_mfa_mixin import AwsMfaMixin, add_aws_mfa_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 .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 .exoscale_mixin import ExoscaleMixin, add_exoscale_mixin_config
from .digitalocean_backend_properties_mixin import DigitaloceanBackendPropertiesMixin, add_digitalocean_backend_properties_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 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 .credential import gopass_password_from_path, gopass_field_from_path
from .release_mixin import ReleaseMixin from .release_mixin import ReleaseMixin
from .domain import Validateable, DnsRecord, Devops, Image, Release, ReleaseContext
__version__ = "${version}" __version__ = "${version}"

View file

@ -1,2 +1,2 @@
from .image_build_service import ImageBuildService from .image_build_service import ImageBuildService
from .release_mixin_services import TagAndPushReleaseService, PrepareReleaseService from .release_mixin_services import ReleaseService

View file

@ -1,54 +1,66 @@
from src.main.python.ddadevops.domain import Image from ..domain import Image, Devops, BuildType
from src.main.python.ddadevops.infrastructure import FileApi, ResourceApi, ImageApi from ..infrastructure import FileApi, ResourceApi, ImageApi
class ImageBuildService: class ImageBuildService:
def __init__(self): def __init__(self, file_api: FileApi, resource_api: ResourceApi, image_api: ImageApi):
self.file_api = FileApi() self.file_api = file_api
self.resource_api = ResourceApi() self.resource_api = resource_api
self.docker_api = ImageApi() 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}") data = self.resource_api.read_resource(f"src/main/resources/docker/{resource_name}")
self.file_api.write_data_to_file( 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__( 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( 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): def initialize_build_dir(self, devops: Devops):
build_path = docker.devops.build_path() image = devops.specialized_builds[BuildType.IMAGE]
build_path = devops.build_path()
self.file_api.clean_dir(f"{build_path}/image/resources") self.file_api.clean_dir(f"{build_path}/image/resources")
if docker.use_package_common_files: if image.image_use_package_common_files:
self.__copy_build_resources_from_package__(docker) self.__copy_build_resources_from_package__(devops)
else: 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("image", build_path)
self.file_api.cp_recursive("test", build_path) self.file_api.cp_recursive("test", build_path)
def image(self, docker: Image): def image(self, devops: Devops):
self.docker_api.image(docker.devops.name, docker.devops.build_path()) self.image_api.image(devops.name, devops.build_path())
def drun(self, docker: Image): def drun(self, devops: Devops):
self.docker_api.drun(docker.devops.name) self.image_api.drun(devops.name)
def dockerhub_login(self, docker: Image): def dockerhub_login(self, devops: Devops):
self.docker_api.dockerhub_login( image = devops.specialized_builds[BuildType.IMAGE]
docker.dockerhub_user, docker.dockerhub_password self.image_api.dockerhub_login(
image.image_dockerhub_user, image.image_dockerhub_password
) )
def dockerhub_publish(self, docker: Image): def dockerhub_publish(self, devops: Devops):
self.docker_api.dockerhub_publish( image = devops.specialized_builds[BuildType.IMAGE]
docker.devops.name, docker.dockerhub_user, docker.docker_publish_tag self.image_api.dockerhub_publish(
devops.name, image.image_dockerhub_user, image.image_tag
) )
def test(self, docker: Image): def test(self, devops: Devops):
self.docker_api.test(docker.devops.name, docker.devops.build_path()) self.image_api.test(devops.name, devops.build_path())

View file

@ -1,35 +1,61 @@
from src.main.python.ddadevops.infrastructure.release_mixin import ReleaseContextRepository, VersionRepository, GitApi from typing import Optional, List
from src.main.python.ddadevops.domain import Version, Release from pathlib import Path
from ..infrastructure import GitApi, BuildFileRepository
from ..domain import Version, Release, ReleaseType
class PrepareReleaseService(): class ReleaseService:
def __init__(self, git_api: GitApi, build_file_repository: BuildFileRepository):
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):
self.git_api = git_api self.git_api = git_api
self.main_branch = main_branch self.build_file_repository = build_file_repository
def tag_release(self, release_repo: ReleaseContextRepository): @classmethod
annotation = 'v' + release_repo.get_release(self.main_branch).version.get_version_string() def prod(cls, base_dir: str):
message = 'Release ' + annotation return cls(
self.git_api.tag_annotated_second_last(annotation, message) 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() 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)

View file

@ -1,17 +1,16 @@
import deprecation import deprecation
from .domain import C4k, DnsRecord from .domain import BuildType, DnsRecord
from .devops_build import DevopsBuild from .devops_build import DevopsBuild
from .credential import gopass_field_from_path, gopass_password_from_path from .credential import gopass_field_from_path, gopass_password_from_path
from .infrastructure import ExecutionApi from .infrastructure import ExecutionApi
@deprecation.deprecated(deprecated_in="3.2") @deprecation.deprecated(deprecated_in="3.2", details="use direct dict instead")
# create objects direct instead
def add_c4k_mixin_config( def add_c4k_mixin_config(
config, config,
c4k_config_dict, c4k_config_dict,
c4k_auth_dict, c4k_auth_dict,
executabel_name=None, executable_name=None,
grafana_cloud_user=None, grafana_cloud_user=None,
grafana_cloud_password=None, grafana_cloud_password=None,
grafana_cloud_url="https://prometheus-prod-01-eu-west-0.grafana.net/api/prom/push", 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( config.update(
{ {
"C4kMixin": { "C4kMixin": {
"executabel_name": executabel_name, "executable_name": executable_name,
"config": c4k_config_dict, "config": c4k_config_dict,
"auth": c4k_auth_dict, "auth": c4k_auth_dict,
} }
@ -48,27 +47,31 @@ class C4kBuild(DevopsBuild):
def __init__(self, project, config): def __init__(self, project, config):
super().__init__(project, config) super().__init__(project, config)
self.execution_api = ExecutionApi() self.execution_api = ExecutionApi()
c4k_build = C4k(config) devops = self.devops_repo.get_devops(self.project)
self.repo.set_c4k(self.project, c4k_build) if BuildType.C4K not in devops.specialized_builds:
raise ValueError(f"C4kBuild requires BuildType.C4K")
def update_runtime_config(self, dns_record: DnsRecord): def update_runtime_config(self, dns_record: DnsRecord):
c4k_build = self.repo.get_c4k(self.project) devops = self.devops_repo.get_devops(self.project)
c4k_build.update_runtime_config(dns_record) devops.specialized_builds[BuildType.C4K].update_runtime_config(dns_record)
self.repo.set_c4k(self.project, c4k_build) self.devops_repo.set_devops(self.project, devops)
def write_c4k_config(self): def write_c4k_config(self):
build = self.repo.get_devops(self.project) devops = self.devops_repo.get_devops(self.project)
c4k_build = self.repo.get_c4k(self.project) path = devops.build_path() + "/out_c4k_config.yaml"
path = build.build_path() + "/out_c4k_config.yaml" self.file_api.write_yaml_to_file(
self.file_api.write_yaml_to_file(path, c4k_build.config()) path, devops.specialized_builds[BuildType.C4K].config()
)
def write_c4k_auth(self): def write_c4k_auth(self):
build = self.repo.get_devops(self.project) devops = self.devops_repo.get_devops(self.project)
c4k_build = self.repo.get_c4k(self.project) path = devops.build_path() + "/out_c4k_auth.yaml"
path = build.build_path() + "/out_c4k_auth.yaml" self.file_api.write_yaml_to_file(
self.file_api.write_yaml_to_file(path, c4k_build.c4k_mixin_auth) path, devops.specialized_builds[BuildType.C4K].auth()
)
def c4k_apply(self, dry_run=False): def c4k_apply(self, dry_run=False):
build = self.repo.get_devops(self.project) devops = self.devops_repo.get_devops(self.project)
c4k_build = self.repo.get_c4k(self.project) return self.execution_api.execute(
return self.execution_api.execute(c4k_build.command(build), dry_run) devops.specialized_builds[BuildType.C4K].command(devops), dry_run
)

View file

@ -1,15 +1,24 @@
import deprecation
from .python_util import execute 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 credential = None
if path and field: if path and field:
print('get field for: ' + path + ', ' + field) print("get field for: " + path + ", " + field)
credential = execute(['gopass', 'show', path, field]) credential = execute(["gopass", "show", path, field])
return credential 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 credential = None
if path: if path:
print('get password for: ' + path) print("get password for: " + path)
credential = execute(['gopass', 'show', '--password', path]) credential = execute(["gopass", "show", "--password", path])
return credential return credential

View file

@ -1,7 +1,7 @@
from typing import Optional from typing import Optional
import deprecation import deprecation
from .domain import Devops from .domain import Devops, InitService
from .infrastructure import ProjectRepository, FileApi from .infrastructure import DevopsRepository, FileApi
@deprecation.deprecated(deprecated_in="3.2", details="create objects direct instead") @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, "build_dir_name": build_dir_name,
} }
def get_devops_build(project): def get_devops_build(project):
return project.get_property("devops_build") return project.get_property("build")
class DevopsBuild: class DevopsBuild:
def __init__(self, project, config: Optional[dict] = None, devops: Optional[Devops] = None): def __init__(self, project, input: dict):
self.project = project self.project = project
self.file_api = FileApi() self.file_api = FileApi()
self.repo = ProjectRepository() self.init_service = InitService.prod(project.basedir)
if not devops: self.devops_repo = DevopsRepository()
if not config: devops = self.init_service.initialize(input)
raise ValueError("Build parameters could not be set!") self.devops_repo.set_devops(self.project, devops)
devops = Devops( self.project.set_property("build", self)
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)
def name(self): def name(self):
devops = self.repo.get_devops(self.project) devops = self.devops_repo.get_devops(self.project)
return devops.name return devops.name
def build_path(self): def build_path(self):
devops = self.repo.get_devops(self.project) devops = self.devops_repo.get_devops(self.project)
return devops.build_path() return devops.build_path()
def initialize_build_dir(self): 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()) self.file_api.clean_dir(devops.build_path())

View file

@ -1,11 +1,11 @@
from typing import Optional from typing import Optional
import deprecation import deprecation
from .domain import Image from .domain import BuildType
from .application import ImageBuildService from .application import ImageBuildService
from .devops_build import DevopsBuild, create_devops_build_config 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( def create_devops_docker_build_config(
stage, stage,
project_root_path, project_root_path,
@ -33,46 +33,34 @@ def create_devops_docker_build_config(
class DevopsImageBuild(DevopsBuild): class DevopsImageBuild(DevopsBuild):
def __init__(self, project, config: Optional[dict] = None, image: Optional[Image] = None): def __init__(self, project, input: dict):
self.image_build_service = ImageBuildService() super().__init__(project, input)
if not image: self.image_build_service = ImageBuildService.prod()
if not config: devops = self.devops_repo.get_devops(self.project)
raise ValueError("Image parameters could not be set.") if BuildType.IMAGE not in devops.specialized_builds:
super().__init__(project, config=config) raise ValueError(f"ImageBuild requires BuildType.IMAGE")
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 initialize_build_dir(self): def initialize_build_dir(self):
super().initialize_build_dir() super().initialize_build_dir()
image = self.repo.get_docker(self.project) devops = self.devops_repo.get_devops(self.project)
self.image_build_service.initialize_build_dir(image) self.image_build_service.initialize_build_dir(devops)
def image(self): def image(self):
image = self.repo.get_docker(self.project) devops = self.devops_repo.get_devops(self.project)
self.image_build_service.image(image) self.image_build_service.image(devops)
def drun(self): def drun(self):
image = self.repo.get_docker(self.project) devops = self.devops_repo.get_devops(self.project)
self.image_build_service.drun(image) self.image_build_service.drun(devops)
def dockerhub_login(self): def dockerhub_login(self):
image = self.repo.get_docker(self.project) devops = self.devops_repo.get_devops(self.project)
self.image_build_service.dockerhub_login(image) self.image_build_service.dockerhub_login(devops)
def dockerhub_publish(self): def dockerhub_publish(self):
image = self.repo.get_docker(self.project) devops = self.devops_repo.get_devops(self.project)
self.image_build_service.dockerhub_publish(image) self.image_build_service.dockerhub_publish(devops)
def test(self): def test(self):
image = self.repo.get_docker(self.project) devops = self.devops_repo.get_devops(self.project)
self.image_build_service.test(image) self.image_build_service.test(devops)

View file

@ -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 .image import Image
from .c4k import C4k 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

View file

@ -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}")

View file

@ -5,17 +5,20 @@ from .common import (
Devops, Devops,
) )
class C4k(Validateable): class C4k(Validateable):
def __init__(self, config: dict): def __init__(self, input: dict):
tmp_executabel_name = config["C4kMixin"]["executabel_name"] self.module = input.get("module")
if not tmp_executabel_name: self.stage = input.get("stage")
tmp_executabel_name = config["module"] self.c4k_executable_name = input.get("c4k_executable_name", input.get("module"))
self.executabel_name = tmp_executabel_name self.c4k_config = input.get("c4k_config", {})
self.c4k_mixin_config = config["C4kMixin"]["config"] self.c4k_grafana_cloud_url = input.get(
self.c4k_mixin_auth = config["C4kMixin"]["auth"] "c4k_grafana_cloud_url",
tmp = self.c4k_mixin_config["mon-cfg"] "https://prometheus-prod-01-eu-west-0.grafana.net/api/prom/push",
tmp.update({"cluster-name": config["module"], "cluster-stage": config["stage"]}) )
self.c4k_mixin_config.update({"mon-cfg": tmp}) 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 self.dns_record: Optional[DnsRecord] = None
# TODO: these functions should be located at TerraformBuild later on. # TODO: these functions should be located at TerraformBuild later on.
@ -24,20 +27,39 @@ class C4k(Validateable):
def validate(self) -> List[str]: def validate(self) -> List[str]:
result = [] 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: if self.dns_record:
result += self.dns_record.validate() result += self.dns_record.validate()
return result return result
def config(self): def config(self):
fqdn = self.dns_record.fqdn if not self.dns_record:
self.c4k_mixin_config.update({"fqdn": fqdn}) raise ValueError("dns_reqord was not set.")
return self.c4k_mixin_config 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): def auth(self):
module = build.module result = self.c4k_auth.copy()
build_path = build.build_path() 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" config_path = f"{build_path}/out_c4k_config.yaml"
auth_path = f"{build_path}/out_c4k_auth.yaml" auth_path = f"{build_path}/out_c4k_auth.yaml"
output_path = f"{build_path}/out_{module}.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}"

View file

@ -1,17 +1,46 @@
from typing import List
import deprecation import deprecation
from enum import Enum
from typing import List, TypedDict
import deprecation
def filter_none(list_to_filter): def filter_none(list_to_filter):
return [x for x in list_to_filter if x is not None] 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 [] 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]: def validate(self) -> List[str]:
return [] return []
@ -20,9 +49,10 @@ class Validateable:
def throw_if_invalid(self): def throw_if_invalid(self):
if not self.is_valid(): if not self.is_valid():
issues = '\n'.join(self.validate()) issues = "\n".join(self.validate())
raise ValueError(f"Invalid Validateable: {issues}") raise ValueError(f"Invalid Validateable: {issues}")
class DnsRecord(Validateable): class DnsRecord(Validateable):
def __init__(self, fqdn, ipv4=None, ipv6=None): def __init__(self, fqdn, ipv4=None, ipv6=None):
self.fqdn = fqdn self.fqdn = fqdn
@ -39,28 +69,37 @@ class DnsRecord(Validateable):
class Devops(Validateable): class Devops(Validateable):
def __init__( 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.stage = input.get("stage")
self.name = name self.project_root_path = input.get("project_root_path")
self.project_root_path = project_root_path self.module = input.get("module")
self.module = module self.name = input.get("name", self.module)
if not name: self.build_dir_name = input.get("build_dir_name", "target")
self.name = module self.specialized_builds = specialized_builds
self.build_dir_name = build_dir_name self.mixins = mixins
# 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
def build_path(self): def build_path(self):
path = [self.project_root_path, self.build_dir_name, self.name, self.module] path = [self.project_root_path, self.build_dir_name, self.name, self.module]
return "/".join(filter_none(path)) 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): def __put__(self, key, value):
self.stack[key] = value self.stack[key] = value

View file

@ -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

View file

@ -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

View file

@ -1,28 +1,38 @@
from typing import Optional, List
from .common import ( from .common import (
filter_none, filter_none,
Validateable, Validateable,
Devops,
) )
class Image(Validateable): class Image(Validateable):
def __init__( def __init__(
self, self,
dockerhub_user, input: dict,
dockerhub_password,
devops: Devops,
use_package_common_files=True,
build_commons_path=None,
docker_build_commons_dir_name="docker",
docker_publish_tag=None,
): ):
self.dockerhub_user = dockerhub_user self.image_dockerhub_user = input.get("image_dockerhub_user")
self.dockerhub_password = dockerhub_password self.image_dockerhub_password = input.get("image_dockerhub_password")
self.use_package_common_files = use_package_common_files self.image_tag = input.get("image_tag")
self.build_commons_path = build_commons_path self.image_build_commons_path = input.get("image_build_commons_path")
self.docker_build_commons_dir_name = docker_build_commons_dir_name self.image_use_package_common_files = input.get(
self.docker_publish_tag = docker_publish_tag "image_use_package_common_files", True
self.devops = devops )
self.image_build_commons_dir_name = input.get(
"image_build_commons_dir_name", "docker"
)
def docker_build_commons_path(self): def validate(self) -> List[str]:
commons_path = [self.build_commons_path, self.docker_build_commons_dir_name] 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)) + "/" return "/".join(filter_none(commons_path)) + "/"

View file

@ -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

View file

@ -1,121 +1,60 @@
from enum import Enum from enum import Enum
from typing import Optional from typing import Optional, List
from pathlib import Path from pathlib import Path
from .common import ( from .common import (
Validateable, Validateable,
Devops, Devops,
ReleaseType,
)
from .version import (
Version,
) )
class ReleaseType(Enum):
MAJOR = 0
MINOR = 1
PATCH = 2
SNAPSHOT = 3
BUMP = None
class EnvironmentKeys(Enum): class Release(Validateable):
DDADEVOPS_RELEASE_TYPE = 0 def __init__(self, input: dict, version: Version):
self.release_type = ReleaseType[input.get("release_type", "NONE")]
class Version(): self.release_main_branch = input.get("release_main_branch", "main")
self.release_current_branch = input.get("release_current_branch")
def __init__(self, path: Path, version_list: list): self.release_primary_build_file = input.get(
self.path = path "release_primary_build_file", "./project.clj"
self.version_list = version_list )
self.version_string: Optional[str | None] = None self.release_secondary_build_files = input.get(
self.is_snapshot: Optional[bool | None] = None "release_secondary_build_files", []
)
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
self.version = version 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): def validate(self):
result = [] result = []
result += self.__validate_is_not_empty__("release_type") 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__("version")
result += self.__validate_is_not_empty__("current_branch") try:
return result Path(self.release_primary_build_file)
except Exception as e:
def validate_branch(self, main_branch: str): result.append(
result = [] f"release_primary_build_file must be a valid path but was {e}"
if self.release_type is not None and main_branch != self.current_branch: )
result.append(f"Releases are allowed only on {main_branch}") for path in self.release_secondary_build_files:
return result try:
Path(path)
class Release(Validateable): except Exception as e:
def __init__( result.append(
self, f"release_secondary_build_file must be contain valid paths but was {e}"
devops: Devops, )
main_branch: str, if self.version:
config_file: str, 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
): ):
self.devops = devops result.append(f"Releases are allowed only on {self.release_main_branch}")
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 return result
def build_files(self) -> List[str]:
result = [self.release_primary_build_file]
result += self.release_secondary_build_files
return result

View file

@ -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)

View file

@ -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

View file

@ -1,58 +1,30 @@
from pathlib import Path from pathlib import Path
from sys import stdout from sys import stdout
from os import chmod from os import chmod, environ
from subprocess import run
from pkg_resources import resource_string from pkg_resources import resource_string
import yaml import yaml
from ..domain import Devops, Image, C4k, Release from subprocess import check_output, Popen, PIPE, run
from ..python_util import execute from ..domain import Devops, Image, C4k, Release, BuildFile
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)
class ResourceApi: class ResourceApi:
def read_resource(self, path: str) -> bytes: def read_resource(self, path: str) -> bytes:
return resource_string(__name__, path) return resource_string('ddadevops', path)
class FileApi: class FileApi:
def __init__(self):
self.execution_api = ExecutionApi()
def clean_dir(self, directory: str): def clean_dir(self, directory: str):
execute("rm -rf " + directory, shell=True) self.execution_api.execute("rm -rf " + directory)
execute("mkdir -p " + directory, shell=True) self.execution_api.execute("mkdir -p " + directory)
def cp_force(self, src: str, target_dir: str): 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): 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): def write_data_to_file(self, path: Path, data: bytes):
with open(path, "w", encoding="utf-8") as output_file: with open(path, "w", encoding="utf-8") as output_file:
@ -74,7 +46,7 @@ class ImageApi:
def drun(self, name: str): def drun(self, name: str):
run( run(
f"docker run -it --entrypoint=\"\" {name} /bin/bash", f'docker run -it --entrypoint="" {name} /bin/bash',
shell=True, shell=True,
check=True, check=True,
) )
@ -118,11 +90,93 @@ class ImageApi:
class ExecutionApi: class ExecutionApi:
def execute(self, command: str, dry_run=False): def execute(self, command: str, dry_run=False, shell=True):
output = "" output = ""
if dry_run: if dry_run:
print(command) print(command)
else: else:
output = execute(command, True) output = check_output(command, encoding="UTF-8", shell=shell)
print(output) output = output.rstrip()
return output 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}")

View file

@ -1,2 +0,0 @@
from .infrastructure_api import FileHandler, EnvironmentApi, GitApi, JsonFileHandler, GradleFileHandler, PythonFileHandler, ClojureFileHandler
from .repo import VersionRepository, ReleaseContextRepository, ReleaseTypeRepository

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -1,15 +1,22 @@
from subprocess import check_output, Popen, PIPE from subprocess import check_output, Popen, PIPE
import deprecation
@deprecation.deprecated(deprecated_in="3.2", details="use ExecutionApi instead")
def execute(cmd, shell=False): 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() return output.rstrip()
@deprecation.deprecated(deprecated_in="3.2", details="use ExecutionApi instead")
def execute_live(cmd): def execute_live(cmd):
process = Popen(cmd, stdout=PIPE) process = Popen(cmd, stdout=PIPE)
for line in iter(process.stdout.readline, b''): for line in iter(process.stdout.readline, b""):
print(line.decode('utf-8'), end='') print(line.decode("utf-8"), end="")
process.stdout.close() process.stdout.close()
process.wait() process.wait()
@deprecation.deprecated(deprecated_in="3.2", details="use domain.filter_none instead")
def filter_none(list_to_filter): def filter_none(list_to_filter):
return [x for x in list_to_filter if x is not None] return [x for x in list_to_filter if x is not None]

View file

@ -1,36 +1,23 @@
from pybuilder.core import Project from pybuilder.core import Project
from src.main.python.ddadevops.devops_build import DevopsBuild from .devops_build import DevopsBuild
from src.main.python.ddadevops.infrastructure.release_mixin import ReleaseContextRepository, ReleaseTypeRepository, VersionRepository, GitApi, EnvironmentApi from .application import ReleaseService
from src.main.python.ddadevops.application import PrepareReleaseService, TagAndPushReleaseService from .domain import MixinType
from src.main.python.ddadevops.domain import Release, EnvironmentKeys
class ReleaseMixin(DevopsBuild): class ReleaseMixin(DevopsBuild):
def __init__(self, project: Project, release: Release): def __init__(self, project: Project, input: dict):
super().__init__(project, devops=release.devops) super().__init__(project, input)
self.repo.set_release(self.project, release) self.release_service = ReleaseService.prod(project.basedir)
self.main_branch = release.main_branch devops = self.devops_repo.get_devops(self.project)
if MixinType.RELEASE not in devops.mixins:
git_api = GitApi() raise ValueError(f"ReleaseMixin requires MixinType.RELEASE")
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 prepare_release(self): def prepare_release(self):
release = self.release_repo.get_release(self.main_branch) devops = self.devops_repo.get_devops(self.project)
self.prepare_release_service.write_and_commit_release(release, self.release_repo.version_repository) release = devops.mixins[MixinType.RELEASE]
self.prepare_release_service.write_and_commit_bump(release, self.release_repo.version_repository) self.release_service.prepare_release(release)
def tag_and_push_release(self): def tag_bump_and_push_release(self):
self.tag_and_push_release_service.tag_release(self.release_repo) devops = self.devops_repo.get_devops(self.project)
self.tag_and_push_release_service.push_release() release = devops.mixins[MixinType.RELEASE]
self.release_service.tag_bump_and_push_release(release)

View file

@ -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

View file

@ -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

View file

@ -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)
)

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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"

View file

@ -1,2 +0,0 @@
from .mock_infrastructure import MockReleaseRepository, MockReleaseTypeRepository, MockVersionRepository
from .mock_infrastructure_api import MockGitApi

View file

@ -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}")

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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", "")

View file

@ -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

View file

@ -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)}")

View file

@ -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")

View file

@ -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')

View file

@ -1,27 +1,25 @@
import os import os
from pathlib import Path
from pybuilder.core import Project from pybuilder.core import Project
from src.main.python.ddadevops.domain.common import Devops from src.main.python.ddadevops import DevopsBuild
from src.main.python.ddadevops.devops_build import DevopsBuild from .domain.helper import devops_config
from .resource_helper import copy_resource
class MyDevopsBuild(DevopsBuild):
pass
def test_devops_build(tmp_path): def test_devops_build(tmp_path):
build_dir = "build" str_tmp_path = str(tmp_path)
project_name = "testing-project" copy_resource(Path("package.json"), tmp_path)
module_name = "c4k-test" project = Project(str_tmp_path, name="name")
tmp_path_str = str(tmp_path)
project = Project(tmp_path_str, name=project_name) devops_build = DevopsBuild(
devops = Devops( project,
stage="test", devops_config(
project_root_path=tmp_path_str, {
module=module_name, "project_root_path": str_tmp_path,
build_dir_name=build_dir, "build_types": [],
"mixin_types": [],
}
),
) )
devops_build = DevopsBuild(project, devops=devops)
devops_build.initialize_build_dir() devops_build.initialize_build_dir()
assert os.path.exists(f"{devops_build.build_path()}") assert os.path.exists(f"{devops_build.build_path()}")

View file

@ -1,25 +1,24 @@
import os import os
from pybuilder.core import Project from pybuilder.core import Project
from src.main.python.ddadevops.domain import Image, Devops from src.main.python.ddadevops import DevopsImageBuild
from src.main.python.ddadevops.devops_image_build import DevopsImageBuild from .domain.helper import devops_config
def test_devops_docker_build(tmp_path): def test_devops_docker_build(tmp_path):
build_dir = "build" str_tmp_path = str(tmp_path)
project_name = "testing-project" project = Project(str_tmp_path, name="name")
module_name = "docker-test"
tmp_path_str = str(tmp_path)
project = Project(tmp_path_str, name=project_name) os.environ["IMAGE_DOCKERHUB_USER"] = "user"
devops = Devops( os.environ["IMAGE_DOCKERHUB_PASSWORD"] = "password"
stage="test",
project_root_path=tmp_path_str, image_build = DevopsImageBuild(
module=module_name, project,
name=project_name, devops_config(
build_dir_name=build_dir {
"project_root_path": str_tmp_path,
"build_types": ["IMAGE"],
"mixin_types": [],
}
),
) )
image = Image(dockerhub_user="user", dockerhub_password="password", devops=devops) assert image_build
docker_build = DevopsImageBuild(project, image=image)
# docker_build.initialize_build_dir()
# assert os.path.exists(f"{docker_build.build_path()}")

View file

@ -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", "")

View file

@ -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"
}
}

View file

@ -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"]})