Compare commits

...

24 commits

Author SHA1 Message Date
2682cca424 Update with real local dev options 2024-06-27 16:23:34 +02:00
edbe56055d Empty 2024-06-27 16:23:16 +02:00
d6a1f2b655 Update command for live exec 2024-06-27 16:23:01 +02:00
eb18eb03bc Add todo 2024-06-27 16:22:22 +02:00
8c8219b5fa Sleep 3 secs 2024-06-27 15:40:05 +02:00
24579506ae Linting 2024-06-27 14:24:53 +02:00
07169c5b3d Remove todos 2024-06-27 14:19:27 +02:00
872b29179e Update tests 2024-06-27 14:19:19 +02:00
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
9cdd647514 Add resource files 2024-06-26 16:52:59 +02:00
f9f2389c95 Update provider with correct vars 2024-06-26 16:52:40 +02:00
ce9d3fdee6 Pass env credentials to start command 2024-06-26 16:52:04 +02:00
4eadad3940 Rename to TERRAFORM_BACKEND_GIT 2024-06-26 15:25:34 +02:00
6e5275b56b Implement backend git api and respective checks 2024-06-26 15:23:20 +02:00
14db8bed54 Adhere to naming convention 2024-06-26 13:12:08 +02:00
4539b84d6c WIP implement tf backend git provider 2024-06-26 10:35:53 +02:00
15 changed files with 215 additions and 17 deletions

View file

@ -1,15 +1,26 @@
# Dev Setup
## For local development
# For local development ```bash
```
python3 -m venv ~/.venv --upgrade python3 -m venv ~/.venv --upgrade
source ~/.venv/bin/activate source ~/.venv/bin/activate
pip3 install --upgrade -r dev_requirements.txt pip3 install --upgrade -r dev_requirements.txt
pip3 install --upgrade -r requirements.txt pip3 install --upgrade -r requirements.txt
``` ```
# For testing a dev version ## For testing a dev version
```
With uploading to pypi
```bash
pyb publish upload pyb publish upload
pip3 install --upgrade ddadevops --pre pip3 install --upgrade ddadevops --pre
``` ```
With locally installing the package
```bash
pyb publish
pip3 install --upgrade -e /home/${USER}/repo/opensource/dda-devops-build/target/dist/ddadevops-4.12.1-dev/
```

View file

@ -3,17 +3,18 @@ from dda_python_terraform import Terraform, IsFlagged
from packaging import version from packaging import version
from ..domain import Devops, BuildType from ..domain import Devops, BuildType
from ..infrastructure import FileApi, ResourceApi, TerraformApi from ..infrastructure import FileApi, ResourceApi, TerraformApi, TerraformBackendGitApi
# 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 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_api
@classmethod @classmethod
def prod(cls): def prod(cls):
@ -21,6 +22,7 @@ class TerraformService:
FileApi(), FileApi(),
ResourceApi(), ResourceApi(),
TerraformApi(), TerraformApi(),
TerraformBackendGitApi(),
) )
def initialize_build_dir(self, devops: Devops): def initialize_build_dir(self, devops: Devops):
@ -35,6 +37,15 @@ class TerraformService:
self.file_api.cp("*.tfvars", devops.build_path(), check=False) self.file_api.cp("*.tfvars", devops.build_path(), check=False)
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):
terraform_domain = devops.specialized_builds[BuildType.TERRAFORM]
credentials = terraform_domain.env_credentials()
self.tf_backend_git_api.start(credentials)
def uses_backend_git(self, devops: Devops) -> bool:
terraform_domain = devops.specialized_builds[BuildType.TERRAFORM]
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]
return self.file_api.read_json_fro_file( return self.file_api.read_json_fro_file(
@ -154,6 +165,7 @@ 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()
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,44 +13,52 @@ 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
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):
self.initialize_build_dir()
devops = self.devops_repo.get_devops(self.project)
if self.terraform_service.uses_backend_git(devops):
self.terraform_service.start_tf_backend_git_daemon(devops)
# TODO: Do we want a time.sleep(1) for usability?
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(
@ -59,5 +67,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
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, as the backend block can not reference vars.
# This is another reason to introduce a 'backend' object
def project_vars(self) -> Dict[str, Any]:
return {
"": ""
}
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,10 +13,12 @@ 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
AWS = 2 AWS = 2
TERRAFORM_BACKEND_GIT = 3
class MixinType(Enum): class MixinType(Enum):

View file

@ -6,6 +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 .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
@ -60,6 +61,8 @@ class InitService:
default_mappings += Digitalocean.get_mapping_default() default_mappings += Digitalocean.get_mapping_default()
if ProviderType.HETZNER in provider_types: if ProviderType.HETZNER in provider_types:
default_mappings += Hetzner.get_mapping_default() default_mappings += Hetzner.get_mapping_default()
if ProviderType.TERRAFORM_BACKEND_GIT in provider_types:
default_mappings += TerraformBackendGit.get_mapping_default()
if MixinType.RELEASE in mixin_types: if MixinType.RELEASE in mixin_types:
primary_build_file_id = inp.get( primary_build_file_id = inp.get(

View file

@ -8,6 +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 .backend_tf_backend_git import TerraformBackendGit
class TerraformDomain(Validateable): class TerraformDomain(Validateable):
@ -42,6 +43,8 @@ class TerraformDomain(Validateable):
self.providers[ProviderType.HETZNER] = Hetzner(inp) self.providers[ProviderType.HETZNER] = Hetzner(inp)
if ProviderType.AWS in provider_types: if ProviderType.AWS in provider_types:
self.providers[ProviderType.AWS] = Aws(inp) self.providers[ProviderType.AWS] = Aws(inp)
if ProviderType.TERRAFORM_BACKEND_GIT in provider_types:
self.providers[ProviderType.TERRAFORM_BACKEND_GIT] = TerraformBackendGit(inp)
def validate(self) -> List[str]: def validate(self) -> List[str]:
result = [] result = []
@ -86,6 +89,21 @@ class TerraformDomain(Validateable):
result = result and provider.is_local_state() result = result and provider.is_local_state()
return result return result
def uses_backend_git(self) -> bool:
if ProviderType.TERRAFORM_BACKEND_GIT in self.providers:
return True
return False
# TODO: Add ssh method case and make this default
def env_credentials(self) -> Dict[str, str]:
tf_backend_git = self.providers[ProviderType.TERRAFORM_BACKEND_GIT]
if tf_backend_git.git_backend_token != "":
return {
"GITHUB_TOKEN": tf_backend_git.git_backend_token,
"GIT_USERNAME": tf_backend_git.git_backend_username,
}
return {"": ""}
def backend_config(self) -> Dict[str, Any]: def backend_config(self) -> Dict[str, Any]:
result = {} result = {}
for provider in self.providers.values(): for provider in self.providers.values():

View file

@ -7,6 +7,7 @@ from .infrastructure import (
CredentialsApi, CredentialsApi,
GitApi, GitApi,
TerraformApi, TerraformApi,
TerraformBackendGitApi,
ArtifactDeploymentApi, ArtifactDeploymentApi,
) )
from .repository import DevopsRepository, BuildFileRepository from .repository import DevopsRepository, BuildFileRepository

View file

@ -1,4 +1,5 @@
from subprocess import Popen, PIPE, run, CalledProcessError from subprocess import Popen, PIPE, run, CalledProcessError
from typing import Dict
from pathlib import Path from pathlib import Path
from sys import stdout from sys import stdout
from os import chmod, environ from os import chmod, environ
@ -216,6 +217,20 @@ class TerraformApi:
pass pass
class TerraformBackendGitApi:
def __init__(self):
self.execution_api = ExecutionApi()
def start(self, credentials: Dict[str, str]):
env = ""
for key in credentials:
env = env + f'{key}' + "=" + f'{credentials[key]}' + " "
self.execution_api.execute_live(f"{env} nohup terraform-backend-git &")
def stop(self):
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

@ -0,0 +1,3 @@
terraform {
backend "http" {}
}

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():
@ -77,6 +78,7 @@ def test_should_calculate_project_vars():
{ {
"do_as_backend": False, "do_as_backend": False,
"aws_as_backend": False, "aws_as_backend": False,
"tf_provider_types": ["DIGITALOCEAN", "HETZNER", "AWS"]
} }
) )
sut = TerraformDomain(config) sut = TerraformDomain(config)
@ -166,6 +168,17 @@ def test_should_calculate_resources_from_package():
"aws_provider_vars.tf", "aws_provider_vars.tf",
"aws_backend_wkms_vars.tf", "aws_backend_wkms_vars.tf",
"my.file", "my.file",
"tf_backend_git_backend_vars.tf",
"tf_backend_git_backend.tf",
} == sut.resources_from_package()
config = devops_config({"tf_provider_types": ["TERRAFORM_BACKEND_GIT"]})
sut = TerraformDomain(config)
assert {
"versions.tf",
"terraform_build_vars.tf",
"tf_backend_git_backend_vars.tf",
"tf_backend_git_backend.tf",
} == sut.resources_from_package() } == sut.resources_from_package()
@ -188,3 +201,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 sut.uses_backend_git()