Compare commits

...

9 commits

Author SHA1 Message Date
a0dbb79d30 Add initial tests 2024-06-27 12:43:51 +02:00
26da85487f Add TODOs 2024-06-27 12:43:38 +02:00
d6b6cb7a72 Lint 2024-06-27 12:43:24 +02:00
ff61ff383e Rename to backend 2024-06-27 11:48:17 +02:00
170d9bc6d0 Implement missing methods 2024-06-27 11:42:09 +02:00
7f2c78ba97 Remove unused import, arguments 2024-06-27 11:41:42 +02:00
e72ac8882e Rename for better semantics 2024-06-27 11:40:01 +02:00
de23855d65 All upper case TODO 2024-06-27 11:39:45 +02:00
a6b6a6b8bc Typo 2024-06-27 10:18:44 +02:00
12 changed files with 152 additions and 95 deletions

View file

@ -9,12 +9,12 @@ from ..infrastructure import FileApi, ResourceApi, TerraformApi, TerraformBacken
# TODO: mv more fkt to Terraform_api ? # TODO: mv more fkt to Terraform_api ?
class TerraformService: class TerraformService:
def __init__( def __init__(
self, file_api: FileApi, resource_api: ResourceApi, terraform_api: TerraformApi, tf_backend_git_api: TerraformBackendGitApi self, file_api: FileApi, resource_api: ResourceApi, terraform_api: TerraformApi, tf_backend_api: TerraformBackendGitApi
): ):
self.file_api = file_api self.file_api = file_api
self.resource_api = resource_api self.resource_api = resource_api
self.terraform_api = terraform_api self.terraform_api = terraform_api
self.tf_backend_git_api = tf_backend_git_api self.tf_backend_git_api = tf_backend_api
@classmethod @classmethod
def prod(cls): def prod(cls):
@ -38,13 +38,13 @@ class TerraformService:
self.file_api.cp_recursive("scripts", devops.build_path(), check=False) self.file_api.cp_recursive("scripts", devops.build_path(), check=False)
def start_tf_backend_git_daemon(self, devops: Devops): def start_tf_backend_git_daemon(self, devops: Devops):
terraform = devops.specialized_builds[BuildType.TERRAFORM] terraform_domain = devops.specialized_builds[BuildType.TERRAFORM]
credentials = terraform.env_credentials() credentials = terraform_domain.env_credentials()
self.tf_backend_git_api.start(credentials) self.tf_backend_git_api.start(credentials)
def uses_backend_git(self, devops: Devops) -> bool: def uses_backend_git(self, devops: Devops) -> bool:
terraform = devops.specialized_builds[BuildType.TERRAFORM] terraform_domain = devops.specialized_builds[BuildType.TERRAFORM]
return terraform.uses_backend_git() return terraform_domain.uses_backend_git()
def read_output(self, devops: Devops) -> map: def read_output(self, devops: Devops) -> map:
terraform_domain = devops.specialized_builds[BuildType.TERRAFORM] terraform_domain = devops.specialized_builds[BuildType.TERRAFORM]
@ -166,7 +166,6 @@ class TerraformService:
def post_build(self, devops: Devops): def post_build(self, devops: Devops):
self.__rescue_local_state__(devops) self.__rescue_local_state__(devops)
self.tf_backend_git_api.stop() self.tf_backend_git_api.stop()
def __copy_build_resource_file_from_package__(self, resource_name, devops: Devops): def __copy_build_resource_file_from_package__(self, resource_name, devops: Devops):
data = self.resource_api.read_resource( data = self.resource_api.read_resource(

View file

@ -13,50 +13,51 @@ class DevopsTerraformBuild(DevopsBuild):
inp["mixin_types"] = config.get("mixin_types", []) inp["mixin_types"] = config.get("mixin_types", [])
super().__init__(project, inp) super().__init__(project, inp)
project.build_depends_on("dda-python-terraform") project.build_depends_on("dda-python-terraform")
self.teraform_service = TerraformService.prod() self.terraform_service = TerraformService.prod()
# ToDo: we might want to make this private in the future, keeping this for compatibility
# TODO: we might want to make this private in the future, keeping this for compatibility
def initialize_build_dir(self): def initialize_build_dir(self):
super().initialize_build_dir() super().initialize_build_dir()
devops = self.devops_repo.get_devops(self.project) devops = self.devops_repo.get_devops(self.project)
self.teraform_service.initialize_build_dir(devops) self.terraform_service.initialize_build_dir(devops)
def pre_build(self): def pre_build(self):
self.initialize_build_dir() self.initialize_build_dir()
devops = self.devops_repo.get_devops(self.project) devops = self.devops_repo.get_devops(self.project)
if self.teraform_service.uses_backend_git(devops): if self.terraform_service.uses_backend_git(devops):
self.teraform_service.start_tf_backend_git_daemon() self.terraform_service.start_tf_backend_git_daemon(devops)
def post_build(self): def post_build(self):
devops = self.devops_repo.get_devops(self.project) devops = self.devops_repo.get_devops(self.project)
self.teraform_service.post_build(devops) self.terraform_service.post_build(devops)
def read_output_json(self) -> map: def read_output_json(self) -> map:
devops = self.devops_repo.get_devops(self.project) devops = self.devops_repo.get_devops(self.project)
return self.teraform_service.read_output(devops) return self.terraform_service.read_output(devops)
def plan(self): def plan(self):
devops = self.devops_repo.get_devops(self.project) devops = self.devops_repo.get_devops(self.project)
self.teraform_service.plan(devops) self.terraform_service.plan(devops)
self.post_build() self.post_build()
def plan_fail_on_diff(self): def plan_fail_on_diff(self):
devops = self.devops_repo.get_devops(self.project) devops = self.devops_repo.get_devops(self.project)
self.teraform_service.plan(devops, fail_on_diff=True) self.terraform_service.plan(devops, fail_on_diff=True)
self.post_build() self.post_build()
def apply(self, auto_approve=False): def apply(self, auto_approve=False):
devops = self.devops_repo.get_devops(self.project) devops = self.devops_repo.get_devops(self.project)
self.teraform_service.apply(devops, auto_approve=auto_approve) self.terraform_service.apply(devops, auto_approve=auto_approve)
self.post_build() self.post_build()
def refresh(self): def refresh(self):
devops = self.devops_repo.get_devops(self.project) devops = self.devops_repo.get_devops(self.project)
self.teraform_service.refresh(devops) self.terraform_service.refresh(devops)
self.post_build() self.post_build()
def destroy(self, auto_approve=False): def destroy(self, auto_approve=False):
devops = self.devops_repo.get_devops(self.project) devops = self.devops_repo.get_devops(self.project)
self.teraform_service.destroy(devops, auto_approve=auto_approve) self.terraform_service.destroy(devops, auto_approve=auto_approve)
self.post_build() self.post_build()
def tf_import( def tf_import(
@ -65,5 +66,5 @@ class DevopsTerraformBuild(DevopsBuild):
tf_import_resource, tf_import_resource,
): ):
devops = self.devops_repo.get_devops(self.project) devops = self.devops_repo.get_devops(self.project)
self.teraform_service.tf_import(devops, tf_import_name, tf_import_resource) self.terraform_service.tf_import(devops, tf_import_name, tf_import_resource)
self.post_build() self.post_build()

View file

@ -15,6 +15,7 @@ from .terraform import TerraformDomain
from .provider_digitalocean import Digitalocean from .provider_digitalocean import Digitalocean
from .provider_hetzner import Hetzner from .provider_hetzner import Hetzner
from .provider_aws import Aws from .provider_aws import Aws
from .backend_tf_backend_git import TerraformBackendGit
from .provs_k3s import K3s from .provs_k3s import K3s
from .release import Release from .release import Release
from .artifact import Artifact from .artifact import Artifact

View file

@ -0,0 +1,72 @@
from typing import List, Dict, Set, Any
from .common import Validateable, CredentialMappingDefault
# TODO: Add a 'git_as_backend' option
# TODO: Check this option at the right places
class TerraformBackendGit(Validateable, CredentialMappingDefault):
def __init__(
self,
inp: dict,
):
self.stage = inp.get("stage")
self.module = inp.get("module")
self.git_backend_repo = inp.get("git_backend_repo")
self.git_backend_ref = inp.get("git_backend_ref")
self.git_backend_state = inp.get("git_backend_state")
self.git_backend_username = inp.get("git_backend_username")
self.git_backend_token = inp.get("git_backend_token")
def validate(self) -> List[str]:
result = []
result += self.__validate_is_not_empty__("stage")
result += self.__validate_is_not_empty__("module")
result += self.__validate_is_not_empty__("git_backend_repo")
result += self.__validate_is_not_empty__("git_backend_ref")
result += self.__validate_is_not_empty__("git_backend_state")
result += self.__validate_is_not_empty__("git_backend_username")
result += self.__validate_is_not_empty__("git_backend_token")
return result
# See: https://developer.hashicorp.com/terraform/language/settings/backends/configuration#command-line-key-value-pairs
# and https://github.com/plumber-cd/terraform-backend-git?tab=readme-ov-file#standalone-terraform-http-backend-mode
def backend_config(self) -> Dict[str, Any]:
return {
"address": self.__make_http_backend_address__(),
"lock_address": self.__make_http_backend_address__(),
"unlock_address": self.__make_http_backend_address__(),
}
def resources_from_package(self) -> Set[str]:
return {"tf_backend_git_backend.tf", "tf_backend_git_backend_vars.tf"}
# TODO: This can not be used for backend config, as the backend block can not reference vars.
def project_vars(self) -> Dict[str, Any]:
return {
"http_backend_address": self.__make_http_backend_address__()
}
def is_local_state(self):
return False
def __make_http_backend_address__(self) -> str:
# TODO Should we make this configurable?
base_string = "http://localhost:6061/?type=git"
state = f"{self.stage}/{self.module}/{self.git_backend_state}"
return f"{base_string}&repository={self.git_backend_repo}&ref={self.git_backend_ref}&state={state}"
# TODO: Implement ssh auth too
@classmethod
def get_mapping_default(cls) -> List[Dict[str, str]]:
return [
{
"gopass_path": "server/meissa/repo/terraform-backend-git-test",
"gopass_field": "user",
"name": "git_backend_username",
},
{
"gopass_path": "server/meissa/repo/terraform-backend-git-test",
"gopass_field": "token",
"name": "git_backend_token",
},
]

View file

@ -13,6 +13,7 @@ class BuildType(Enum):
TERRAFORM = 3 TERRAFORM = 3
# TODO: We could follow domain implications and make a 'BackendType' enum
class ProviderType(Enum): class ProviderType(Enum):
DIGITALOCEAN = 0 DIGITALOCEAN = 0
HETZNER = 1 HETZNER = 1

View file

@ -6,7 +6,7 @@ from .devops_factory import DevopsFactory
from .terraform import TerraformDomain from .terraform import TerraformDomain
from .provider_digitalocean import Digitalocean from .provider_digitalocean import Digitalocean
from .provider_hetzner import Hetzner from .provider_hetzner import Hetzner
from .provider_tf_backend_git import TerraformBackendGit from .backend_tf_backend_git import TerraformBackendGit
from .c4k import C4k from .c4k import C4k
from .image import Image from .image import Image
from .release import ReleaseType, Release from .release import ReleaseType, Release

View file

@ -1,63 +0,0 @@
from typing import List, Dict, Set, Any
from .common import Validateable, CredentialMappingDefault
class TerraformBackendGit(Validateable, CredentialMappingDefault):
def __init__(
self,
inp: dict,
):
self.stage = inp.get("stage")
self.module = inp.get("module")
self.git_backend_repo = inp.get("git_backend_repo")
self.git_backend_ref = inp.get("git_backend_ref")
self.git_backend_state = inp.get("git_backend_state")
self.git_backend_username = inp.get("git_backend_username")
self.git_backend_token = inp.get("git_backend_token")
def validate(self) -> List[str]:
result = []
result += self.__validate_is_not_empty__("stage")
result += self.__validate_is_not_empty__("module")
result += self.__validate_is_not_empty__("git_backend_repo")
result += self.__validate_is_not_empty__("git_backend_ref")
result += self.__validate_is_not_empty__("git_backend_state")
result += self.__validate_is_not_empty__("git_backend_username")
result += self.__validate_is_not_empty__("git_backend_token")
return result
# See: https://developer.hashicorp.com/terraform/language/settings/backends/configuration#command-line-key-value-pairs
# and https://github.com/plumber-cd/terraform-backend-git?tab=readme-ov-file#standalone-terraform-http-backend-mode
def backend_config(self) -> Dict[str, Any]:
return {
"address": self.__make_http_backend_address__(self.git_backend_repo, self.git_backend_ref, self.git_backend_state),
"lock_address": self.__make_http_backend_address__(self.git_backend_repo, self.git_backend_ref, self.git_backend_state),
"unlock_address": self.__make_http_backend_address__(self.git_backend_repo, self.git_backend_ref, self.git_backend_state),
}
def resources_from_package(self) -> List[str]:
return {"tf_backend_git_backend.tf", "tf_backend_git_backend_vars.tf"}
# ToDo: This can not be used for backend config, as the backend block can not reference vars.
def project_vars(self) -> Dict[str, Any]:
return {
"http_backend_address": self.__make_http_backend_address__(self.git_backend_ref, self.git_backend_repo, self.git_backend_state)
}
def is_local_state(self):
return False
# ToDo:
def __bucket_key__(self):
pass
# ToDo:
def __make_http_backend_address__(self):
pass
# ToDo:
@classmethod
def get_mapping_default():
pass

View file

@ -8,7 +8,7 @@ from .common import (
from .provider_digitalocean import Digitalocean from .provider_digitalocean import Digitalocean
from .provider_hetzner import Hetzner from .provider_hetzner import Hetzner
from .provider_aws import Aws from .provider_aws import Aws
from .provider_tf_backend_git import TerraformBackendGit from .backend_tf_backend_git import TerraformBackendGit
class TerraformDomain(Validateable): class TerraformDomain(Validateable):
@ -88,20 +88,22 @@ class TerraformDomain(Validateable):
for provider in self.providers.values(): for provider in self.providers.values():
result = result and provider.is_local_state() result = result and provider.is_local_state()
return result return result
def uses_backend_git(self) -> bool: def uses_backend_git(self) -> bool:
return ProviderType.TERRAFORM_BACKEND_GIT in self.providers.keys() if ProviderType.TERRAFORM_BACKEND_GIT in self.providers.keys():
return True
# ToDo: Add ssh method case and make this default return False
# ToDo: How do we get to the credentials?
# TODO: Add ssh method case and make this default
# TODO: How do we get to the credentials?
def env_credentials(self) -> Dict[str, str]: def env_credentials(self) -> Dict[str, str]:
tf_backend_git = self.providers[ProviderType.TERRAFORM_BACKEND_GIT] tf_backend_git = self.providers[ProviderType.TERRAFORM_BACKEND_GIT]
if tf_backend_git.git_backend_token != "": if tf_backend_git.git_backend_token != "":
return { return {
"GITHUB_TOKEN" : tf_backend_git.git_backend_token, "GITHUB_TOKEN": tf_backend_git.git_backend_token,
"GIT_USERNAME" : tf_backend_git.git_backend_username, "GIT_USERNAME": tf_backend_git.git_backend_username,
} }
return {"" : ""} return {"": ""}
def backend_config(self) -> Dict[str, Any]: def backend_config(self) -> Dict[str, Any]:
result = {} result = {}

View file

@ -216,17 +216,21 @@ class GitApi:
class TerraformApi: class TerraformApi:
pass pass
class TerraformBackendGitApi: class TerraformBackendGitApi:
def __init__(self): def __init__(self):
self.execution_api = ExecutionApi() self.execution_api = ExecutionApi()
def start(self, credentials: Dict[str, str]): def start(self, credentials: Dict[str, str]):
env = "" env = ""
for key in credentials: for key in credentials:
env = env + f'{key}' + "=" + f'{credentials[key]}' + " " env = env + f'{key}' + "=" + f'{credentials[key]}' + " "
self.execution_api.execute(f'{env}' + " " + "terraform-backend-git &") self.execution_api.execute(f'{env}' + " " + "terraform-backend-git &")
def stop(self): def stop(self):
self.execution_api.execute("terraform-backend-git stop") self.execution_api.execute("terraform-backend-git stop")
class ArtifactDeploymentApi: class ArtifactDeploymentApi:
def __init__(self): def __init__(self):
self.execution_api = ExecutionApi() self.execution_api = ExecutionApi()

View file

@ -24,7 +24,7 @@ def devops_config(overrides: dict) -> dict:
"k3s_letsencrypt_endpoint": "k3s_letsencrypt_endpoint", "k3s_letsencrypt_endpoint": "k3s_letsencrypt_endpoint",
"k3s_enable_echo": "false", "k3s_enable_echo": "false",
"k3s_app_filename_to_provision": "k3s_app.yaml", "k3s_app_filename_to_provision": "k3s_app.yaml",
"tf_provider_types": ["DIGITALOCEAN", "HETZNER", "AWS"], "tf_provider_types": ["DIGITALOCEAN", "HETZNER", "AWS", "TERRAFORM_BACKEND_GIT"],
"tf_additional_vars": [], "tf_additional_vars": [],
"tf_output_json_name": "the_out.json", "tf_output_json_name": "the_out.json",
"tf_use_workspace": None, "tf_use_workspace": None,
@ -58,6 +58,11 @@ def devops_config(overrides: dict) -> dict:
"release_artifact_server_url": None, "release_artifact_server_url": None,
"release_organisation": None, "release_organisation": None,
"release_repository_name": None, "release_repository_name": None,
"git_backend_repo": "https://repo.example.com/meissa/infra-states",
"git_backend_ref": "main",
"git_backend_state": "test.json",
"git_backend_username": "tf_backend_user",
"git_backend_token": "324asd234df435sfdgh",
"credentials_mappings": [ "credentials_mappings": [
{ {
"gopass_path": "a/path", "gopass_path": "a/path",

View file

@ -0,0 +1,30 @@
from pybuilder.core import Project
from pathlib import Path
from src.main.python.ddadevops.domain import (
BuildType,
TerraformBackendGit,
)
from .helper import devops_config
def test_tf_backend_git_creation():
sut = TerraformBackendGit(
{
"module": "module",
"stage": "test",
"git_backend_repo": "https://repo.example.com/meissa/infra-states",
"git_backend_ref": "main",
"git_backend_state": "test.json",
"git_backend_username": "tf_backend_user",
"git_backend_token": "324asd234df435sfdgh",
}
)
assert sut is not None
assert sut.is_valid()
def test_should_calculate_backend_config():
sut = TerraformBackendGit(devops_config({}))
assert {
"address": "http://localhost:6061/?type=git&repository=https://repo.example.com/meissa/infra-states&ref=main&state=test/module/test.json",
"lock_address": "http://localhost:6061/?type=git&repository=https://repo.example.com/meissa/infra-states&ref=main&state=test/module/test.json",
"unlock_address": "http://localhost:6061/?type=git&repository=https://repo.example.com/meissa/infra-states&ref=main&state=test/module/test.json",
} == sut.backend_config()

View file

@ -17,6 +17,7 @@ def test_creation():
assert sut.providers[ProviderType.DIGITALOCEAN] assert sut.providers[ProviderType.DIGITALOCEAN]
assert sut.providers[ProviderType.HETZNER] assert sut.providers[ProviderType.HETZNER]
assert sut.providers[ProviderType.AWS] assert sut.providers[ProviderType.AWS]
assert sut.providers[ProviderType.TERRAFORM_BACKEND_GIT]
def test_should_calculate_output_json_name(): def test_should_calculate_output_json_name():
@ -188,3 +189,7 @@ def test_should_calculate_local_state_handling():
) )
) )
assert not sut.is_local_state() assert not sut.is_local_state()
def test_should_use_backend_git():
sut = TerraformDomain(devops_config({}))
assert TerraformDomain.uses_backend_git()