From b4caa16a47faae7c52d0157d4b4c99d005fcd9c3 Mon Sep 17 00:00:00 2001 From: d10s <79284025+D10S0VSkY-OSS@users.noreply.github.com> Date: Sun, 10 Dec 2023 04:14:00 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A5feat:=20Add=20PoC=20terragrunt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/stacks/domain/entities/stacks.py | 2 +- .../src/worker/domain/entities/actions.py | 16 +++ .../src/worker/domain/interfaces/actions.py | 24 ++++ .../src/worker/domain/interfaces/provider.py | 7 +- .../src/worker/domain/services/provider.py | 63 +++------- .../src/worker/providers/hashicorp/actions.py | 113 +++++++++++++----- .../worker/providers/hashicorp/download.py | 27 ++++- sld-dashboard/app/home/forms.py | 3 +- .../app/home/templates/deploys-list.html | 2 + 9 files changed, 170 insertions(+), 87 deletions(-) create mode 100644 sld-api-backend/src/worker/domain/entities/actions.py create mode 100644 sld-api-backend/src/worker/domain/interfaces/actions.py diff --git a/sld-api-backend/src/stacks/domain/entities/stacks.py b/sld-api-backend/src/stacks/domain/entities/stacks.py index ea1a2c85..a5f07893 100644 --- a/sld-api-backend/src/stacks/domain/entities/stacks.py +++ b/sld-api-backend/src/stacks/domain/entities/stacks.py @@ -9,7 +9,7 @@ class StackBase(BaseModel): git_repo: constr(strip_whitespace=True) branch: constr(strip_whitespace=True) = "main" squad_access: List[str] = ["*"] - iac_type: Optional[Literal["terraform", "tofu"]] = "terraform" + iac_type: Optional[Literal["terraform", "tofu", "terragrunt"]] = "terraform" tf_version: constr(strip_whitespace=True) = "1.6.5" tags: Optional[List[str]] = [] project_path: Optional[constr(strip_whitespace=True)] = Field("", example="") diff --git a/sld-api-backend/src/worker/domain/entities/actions.py b/sld-api-backend/src/worker/domain/entities/actions.py new file mode 100644 index 00000000..1752c70b --- /dev/null +++ b/sld-api-backend/src/worker/domain/entities/actions.py @@ -0,0 +1,16 @@ +from pydantic import BaseModel +from typing import Optional, Dict + + +class StructActionsBase(BaseModel): + name: str + stack_name: str + branch: str + environment: str + squad: str + iac_type: Optional[str] = "terraform" + version: str + secreto: Dict + variables_file: Optional[str] + project_path: Optional[str] + task_id: str \ No newline at end of file diff --git a/sld-api-backend/src/worker/domain/interfaces/actions.py b/sld-api-backend/src/worker/domain/interfaces/actions.py new file mode 100644 index 00000000..93c9ebec --- /dev/null +++ b/sld-api-backend/src/worker/domain/interfaces/actions.py @@ -0,0 +1,24 @@ +from typing import Type +from abc import ABC, abstractmethod +from src.worker.domain.services.command import command +from src.worker.domain.entities.actions import StructActionsBase + + +class Actions(ABC): + def __init__(self, params: StructActionsBase, command: Type[command] = command) -> None: + self.name = params.name + self.stack_name = params.stack_name + self.branch = params.branch + self.environment = params.environment + self.squad = params.squad + self.iac_type = params.iac_type + self.version = params.version + self.secreto = params.secreto + self.variables_file = params.variables_file + self.project_path = params.project_path + self.task_id = params.task_id + self.command = command + + @abstractmethod + def execute_deployer_command(self, action: str) -> dict: + pass diff --git a/sld-api-backend/src/worker/domain/interfaces/provider.py b/sld-api-backend/src/worker/domain/interfaces/provider.py index e2dc83bf..24551bf8 100644 --- a/sld-api-backend/src/worker/domain/interfaces/provider.py +++ b/sld-api-backend/src/worker/domain/interfaces/provider.py @@ -1,6 +1,7 @@ -from abc import ABC, abstractmethod +from abc import ABC + + class IProviderArtifactRequirements(ABC): @staticmethod def artifact_download(name, stack_name, environment, squad, git_repo, branch, project_path): - # logic to download artifact - pass \ No newline at end of file + pass diff --git a/sld-api-backend/src/worker/domain/services/provider.py b/sld-api-backend/src/worker/domain/services/provider.py index 2e3354fa..286e6879 100644 --- a/sld-api-backend/src/worker/domain/services/provider.py +++ b/sld-api-backend/src/worker/domain/services/provider.py @@ -1,9 +1,11 @@ # DI terraform provider -from src.worker.providers.hashicorp.actions import Actions +from src.worker.providers.hashicorp.actions import Terraform, Actions from src.worker.providers.hashicorp.artifact import Artifact from src.worker.providers.hashicorp.download import BinaryDownload from src.worker.providers.hashicorp.templates import Backend, GetVars, Tfvars from src.worker.domain.entities.worker import DeployParams, DownloadBinaryParams +from typing import Type + class ProviderRequirements: @@ -73,53 +75,20 @@ def json_vars( class ProviderActions: """ - This class contains the typical methods of a deployment + This class contains the typical methods of a deployment. """ - def plan(params: DeployParams, action: Actions = Actions) -> dict: - config_action = action( - params.name, - params.stack_name, - params.branch, - params.environment, - params.squad, - params.iac_type, - params.version, - params.secreto, - params.variables_file, - params.project_path, - params.task_id, - ) - return config_action.execute_terraform_command("plan") + @staticmethod + def plan(params: DeployParams, action: Type[Actions] = Terraform) -> dict: + config_action = action(params) + return config_action.execute_deployer_command("plan") - def apply(params: DeployParams, action: Actions = Actions) -> dict: - config_action = action( - params.name, - params.stack_name, - params.branch, - params.environment, - params.squad, - params.iac_type, - params.version, - params.secreto, - params.variables_file, - params.project_path, - params.task_id, - ) - return config_action.execute_terraform_command("apply") + @staticmethod + def apply(params: DeployParams, action: Type[Actions] = Terraform) -> dict: + config_action = action(params) + return config_action.execute_deployer_command("apply") - def destroy(params: DeployParams, action: Actions = Actions) -> dict: - config_action = action( - params.name, - params.stack_name, - params.branch, - params.environment, - params.squad, - params.iac_type, - params.version, - params.secreto, - params.variables_file, - params.project_path, - params.task_id, - ) - return config_action.execute_terraform_command("destroy") + @staticmethod + def destroy(params: DeployParams, action: Type[Actions] = Terraform) -> dict: + config_action = action(params) + return config_action.execute_deployer_command("destroy") diff --git a/sld-api-backend/src/worker/providers/hashicorp/actions.py b/sld-api-backend/src/worker/providers/hashicorp/actions.py index dde645c1..2f7a7355 100644 --- a/sld-api-backend/src/worker/providers/hashicorp/actions.py +++ b/sld-api-backend/src/worker/providers/hashicorp/actions.py @@ -1,32 +1,81 @@ import os -from dataclasses import dataclass -import jmespath -import requests -from config.api import settings - +from src.worker.domain.interfaces.actions import Actions from src.worker.security.providers_credentials import secret, unsecret -from src.worker.domain.services.command import command - -@dataclass -class StructBase: - name: str - stack_name: str - branch: str - environment: str - squad: str - - -@dataclass -class Actions(StructBase): - iac_type: str - version: str - secreto: dict - variables_file: str - project_path: str - task_id: str - subprocess_handler: command = command - - def execute_terraform_command(self, action: str) -> dict: + + +class Terraform(Actions): + + def execute_deployer_command(self, action: str) -> dict: + channel = self.task_id + try: + secret(self.stack_name, self.environment, self.squad, self.name, self.secreto) + deploy_state = f"{self.environment}_{self.stack_name}_{self.squad}_{self.name}" + + variables_files = ( + f"{self.name}.tfvars.json" + if not self.variables_file + else self.variables_file + ) + + if not self.project_path: + os.chdir(f"/tmp/{self.stack_name}/{self.environment}/{self.squad}/{self.name}") + else: + os.chdir(f"/tmp/{self.stack_name}/{self.environment}/{self.squad}/{self.name}/{self.project_path}") + + init_command = f"/tmp/{self.version}/{self.iac_type} init -no-color -input=false --upgrade" + plan_command = f"/tmp/{self.version}/{self.iac_type} plan -input=false -refresh -no-color -var-file={variables_files} -out={self.name}.tfplan" + apply_command = f"/tmp/{self.version}/{self.iac_type} apply -input=false -auto-approve -no-color {self.name}.tfplan" + destroy_command = f"/tmp/{self.version}/{self.iac_type} destroy -input=false -auto-approve -no-color -var-file={variables_files}" + output = [] + if action == "plan": + result, output_init = self.command(init_command, channel=channel) + result, output_plan = self.command(plan_command, channel=channel) + output = output_init + output_plan + if action == "apply": + result, output_apply = self.command(apply_command, channel=channel) + output = output + output_apply + elif action == "destroy": + result, output_init = self.command(init_command, channel=channel) + result, output_destroy = self.command(destroy_command, channel=channel) + output = output_init + output_destroy + + unsecret(self.stack_name, self.environment, self.squad, self.name, self.secreto) + + rc = result + + output_data = { + "command": action, + "deploy": self.name, + "squad": self.squad, + "stack_name": self.stack_name, + "environment": self.environment, + "rc": rc, + "tfvars_files": self.variables_file, + "remote_state": f"http://remote-state:8080/terraform_state/{deploy_state}", + "project_path": f"/tmp/{self.stack_name}/{self.environment}/{self.squad}/{self.name}/{self.project_path}", + "stdout": output, + } + + return output_data + + except Exception: + return { + "command": action, + "deploy": self.name, + "squad": self.squad, + "stack_name": self.stack_name, + "environment": self.environment, + "rc": 1, + "tfvars_files": self.variables_file, + "project_path": f"/tmp/{self.stack_name}/{self.environment}/{self.squad}/{self.name}/{self.project_path}", + "remote_state": f"http://remote-state:8080/terraform_state/{deploy_state}", + "stdout": output, + } + + +class TerraGrunt(Actions): + + def execute_deployer_command(self, action: str) -> dict: channel = self.task_id try: secret(self.stack_name, self.environment, self.squad, self.name, self.secreto) @@ -49,15 +98,15 @@ def execute_terraform_command(self, action: str) -> dict: destroy_command = f"/tmp/{self.version}/{self.iac_type} destroy -input=false -auto-approve -no-color -var-file={variables_files}" output = [] if action == "plan": - result, output_init = command(init_command, channel=channel) - result, output_plan = self.subprocess_handler(plan_command, channel=channel) + result, output_init = self.command(init_command, channel=channel) + result, output_plan = self.command(plan_command, channel=channel) output = output_init + output_plan if action == "apply": - result, output_apply = self.subprocess_handler(apply_command, channel=channel) + result, output_apply = self.command(apply_command, channel=channel) output = output + output_apply elif action == "destroy": - result, output_init = command(init_command, channel=channel) - result, output_destroy = self.subprocess_handler(destroy_command, channel=channel) + result, output_init = self.command(init_command, channel=channel) + result, output_destroy = self.command(destroy_command, channel=channel) output = output_init + output_destroy unsecret(self.stack_name, self.environment, self.squad, self.name, self.secreto) diff --git a/sld-api-backend/src/worker/providers/hashicorp/download.py b/sld-api-backend/src/worker/providers/hashicorp/download.py index 044e6e12..7858d226 100644 --- a/sld-api-backend/src/worker/providers/hashicorp/download.py +++ b/sld-api-backend/src/worker/providers/hashicorp/download.py @@ -54,7 +54,30 @@ def get(self) -> dict: "rc": 0, "stdout": "Download Binary file", } - + elif self.iac_type == "terragrunt": + logging.info(f"Downloading binary iac_type {self.iac_type} version {self.version}") + binary_directory = f"/tmp/{self.version}" + downloaded_binary_path = f"{binary_directory}/terragrunt_linux_amd64" + renamed_binary_path = f"{binary_directory}/terragrunt" + # Download Terraform binary if not already downloaded + if not os.path.exists(f"/tmp/{self.version}"): + os.mkdir(f"/tmp/{self.version}") + if not os.path.isfile(f"/tmp/{self.version}/terragrunt"): + binary_url = f"https://github.com/gruntwork-io/terragrunt/releases/download/{self.version}/terragrunt_linux_amd64" + response = requests.get(binary_url, stream=True, verify=False) + with open(downloaded_binary_path, 'wb') as file: + for chunk in response.iter_content(chunk_size=90000000): + file.write(chunk) + os.chmod(downloaded_binary_path, os.stat(downloaded_binary_path).st_mode | stat.S_IEXEC) + os.rename(downloaded_binary_path, renamed_binary_path) + else: + logging.error(f"Failed to download Terragrunt binary from {binary_url}") + return {"command": "binaryDownload", "rc": 1, "stdout": "Failed to download binary file"} + return { + "command": "binaryDownload", + "rc": 0, + "stdout": "Download Binary file", + } except Exception as err: + logging.error(f"Error downloading binary {self.iac_type} error: {err}") return {"command": self.iac_type + "Download", "rc": 1, "stdout": err} - diff --git a/sld-dashboard/app/home/forms.py b/sld-dashboard/app/home/forms.py index 7f4a1814..db199a9c 100644 --- a/sld-dashboard/app/home/forms.py +++ b/sld-dashboard/app/home/forms.py @@ -40,7 +40,7 @@ class StackForm(FlaskForm): ) iac_type = SelectField( "IaC Type", - choices=[('', 'Select an IaC Type'), ('terraform', 'Terraform'), ('openTofu', 'openTofu')], + choices=[('', 'Select an IaC Type'), ('terraform', 'Terraform'), ('openTofu', 'openTofu'), ('terragrunt', 'TerraGrunt')], validators=[validators.DataRequired()], coerce=lambda x: 'tofu' if x == 'openTofu' else x ) @@ -71,7 +71,6 @@ class StackForm(FlaskForm): icon_selector = SelectField('Icon Selector', choices=[], validators=[DataRequired()]) - class DeployForm(FlaskForm): deploy_name = StringField( "Deploy Name", diff --git a/sld-dashboard/app/home/templates/deploys-list.html b/sld-dashboard/app/home/templates/deploys-list.html index 3051f7fc..8d31fb35 100644 --- a/sld-dashboard/app/home/templates/deploys-list.html +++ b/sld-dashboard/app/home/templates/deploys-list.html @@ -213,9 +213,11 @@