From 52a35782f796d104c874f5081fb190228950a337 Mon Sep 17 00:00:00 2001 From: d10s <79284025+D10S0VSkY-OSS@users.noreply.github.com> Date: Sun, 26 Nov 2023 16:31:07 +0100 Subject: [PATCH 1/9] =?UTF-8?q?=F0=9F=94=A7refactor:=20terraform=20command?= =?UTF-8?q?s=20action=20class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/worker/domain/services/provider.py | 6 +- .../src/worker/providers/hashicorp/actions.py | 243 +++--------------- 2 files changed, 37 insertions(+), 212 deletions(-) diff --git a/sld-api-backend/src/worker/domain/services/provider.py b/sld-api-backend/src/worker/domain/services/provider.py index 1e307766..dcd41db2 100644 --- a/sld-api-backend/src/worker/domain/services/provider.py +++ b/sld-api-backend/src/worker/domain/services/provider.py @@ -98,7 +98,7 @@ def plan( variables_file, project_path, ) - return config_action.plan_execute() + return config_action.execute_terraform_command("plan") def apply( name: str, @@ -123,7 +123,7 @@ def apply( variables_file, project_path, ) - return config_action.apply_execute() + return config_action.execute_terraform_command("apply") def destroy( name: str, @@ -148,7 +148,7 @@ def destroy( variables_file, project_path, ) - return config_action.destroy_execute() + return config_action.execute_terraform_command("destroy") def output( stack_name: str, squad: str, environment: str, name: str, action=SimpleActions diff --git a/sld-api-backend/src/worker/providers/hashicorp/actions.py b/sld-api-backend/src/worker/providers/hashicorp/actions.py index 1dc4ac94..b635d973 100644 --- a/sld-api-backend/src/worker/providers/hashicorp/actions.py +++ b/sld-api-backend/src/worker/providers/hashicorp/actions.py @@ -24,66 +24,42 @@ class Actions(StructBase): secreto: dict variables_file: str project_path: str - """ - In this class are all the methods equivalent to the terraform commands - """ - def plan_execute(self) -> dict: + def execute_terraform_command(self, command: str) -> dict: 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}" - ) - # Execute task + 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.stack_name}.tfvars.json" - if self.variables_file == "" or self.variables_file == None + 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}" - ) - os.chdir( - f"/tmp/{self.stack_name}/{self.environment}/{self.squad}/{self.name}/{self.project_path}" - ) - result = subprocess.run( - f"/tmp/{self.version}/terraform init -input=false --upgrade", - shell=True, - capture_output=True, - encoding="utf8", - ) - result = subprocess.run( - f"/tmp/{self.version}/terraform plan -input=false -refresh -no-color -var-file={variables_files} -out={self.stack_name}.tfplan", - shell=True, - capture_output=True, - encoding="utf8", - ) - unsecret( - self.stack_name, self.environment, self.squad, self.name, self.secreto - ) + 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}/terraform init -input=false --upgrade" + plan_command = f"/tmp/{self.version}/terraform {command} -input=false -refresh -no-color -var-file={variables_files} -out={self.stack_name}.tfplan" + apply_command = f"/tmp/{self.version}/terraform {command} -input=false -auto-approve -no-color {self.stack_name}.tfplan" + destroy_command = f"/tmp/{self.version}/terraform {command} -input=false -auto-approve -no-color -var-file={variables_files}" + + result = subprocess.run(init_command, shell=True, capture_output=True, encoding="utf8") + result = subprocess.run(plan_command, shell=True, capture_output=True, encoding="utf8") + + if command == "apply": + result = subprocess.run(apply_command, shell=True, capture_output=True, encoding="utf8") + elif command == "destroy": + result = subprocess.run(destroy_command, shell=True, capture_output=True, encoding="utf8") + + unsecret(self.stack_name, self.environment, self.squad, self.name, self.secreto) - # Capture events rc = result.returncode - # check result - if rc != 0: - return { - "command": "plan", - "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": result.stderr.split("\n"), - } - return { - "command": "plan", + + output_data = { + "command": command, "deploy": self.name, "squad": self.squad, "stack_name": self.stack_name, @@ -92,175 +68,24 @@ def plan_execute(self) -> dict: "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": result.stdout.split("\n"), + "stdout": result.stderr.split("\n") if rc != 0 else result.stdout.split("\n"), } - except Exception: + + return output_data + + except Exception as e: return { - "command": "plan", + "command": command, "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": result.stderr.split("\n"), - } - - def apply_execute(self) -> dict: - 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}" - ) - # Execute task - - if not self.project_path: - os.chdir( - f"/tmp/{self.stack_name}/{self.environment}/{self.squad}/{self.name}" - ) - os.chdir( - f"/tmp/{self.stack_name}/{self.environment}/{self.squad}/{self.name}/{self.project_path}" - ) - - result = subprocess.run( - f"/tmp/{self.version}/terraform init -input=false --upgrade", - shell=True, - capture_output=True, - encoding="utf8", - ) - result = subprocess.run( - f"/tmp/{self.version}/terraform apply -input=false -auto-approve -no-color {self.stack_name}.tfplan", - shell=True, - capture_output=True, - encoding="utf8", - ) - unsecret( - self.stack_name, self.environment, self.squad, self.name, self.secreto - ) - - # Capture events - rc = result.returncode - # check result - if rc != 0: - return { - "command": "apply", - "deploy": self.name, - "self.squad": self.squad, - "self.stack_name": self.stack_name, - "self.environment": self.environment, - "rc": rc, - "tfvars_files": self.variables_file, - "remote_state": f"http://remote-state:8080/terraform_state/{deploy_state}", - "self.project_path": f"/tmp/{self.stack_name}/{self.environment}/{self.squad}/{self.name}/{self.project_path}", - "stdout": result.stderr.split("\n"), - } - return { - "command": "apply", - "deploy": self.name, - "self.squad": self.squad, - "self.stack_name": self.stack_name, - "self.environment": self.environment, - "rc": rc, - "tfvars_files": self.variables_file, - "self.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": result.stdout.split("\n"), - } - except Exception: - return { - "command": "apply", - "deploy": self.name, - "self.squad": self.squad, - "self.stack_name": self.stack_name, - "self.environment": self.environment, "rc": 1, "tfvars_files": self.variables_file, - "self.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": "ko", - } - - def destroy_execute(self) -> dict: - 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}" - ) - # Execute task - variables_files = ( - f"{self.stack_name}.tfvars.json" - if self.variables_file == "" or self.variables_file == None - else self.variables_file - ) - if not self.project_path: - os.chdir( - f"/tmp/{self.stack_name}/{self.environment}/{self.squad}/{self.name}" - ) - os.chdir( - f"/tmp/{self.stack_name}/{self.environment}/{self.squad}/{self.name}/{self.project_path}" - ) - - result = subprocess.run( - f"/tmp/{self.version}/terraform init -input=false --upgrade", - shell=True, - capture_output=True, - encoding="utf8", - ) - result = subprocess.run( - f"/tmp/{self.version}/terraform destroy -input=false -auto-approve -no-color -var-file={variables_files}", - shell=True, - capture_output=True, - encoding="utf8", - ) - unsecret( - self.stack_name, self.environment, self.squad, self.name, self.secreto - ) - # Capture events - rc = result.returncode - # check result - if rc != 0: - return { - "command": "destroy", - "deploy": self.name, - "self.squad": self.squad, - "self.stack_name": self.stack_name, - "self.environment": self.environment, - "rc": rc, - "tfvars_files": self.variables_file, - "remote_state": f"http://remote-state:8080/terraform_state/{deploy_state}", - "self.project_path": f"/tmp/{self.stack_name}/{self.environment}/{self.squad}/{self.name}/{self.project_path}", - "stdout": result.stderr.split("\n"), - } - return { - "command": "destroy", - "deploy": self.name, - "self.squad": self.squad, - "self.stack_name": self.stack_name, - "self.environment": self.environment, - "rc": rc, - "tfvars_files": self.variables_file, - "self.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": result.stdout.split("\n"), - } - except Exception: - return { - "command": "destroy", - "deploy": self.name, - "self.squad": self.squad, - "self.stack_name": self.stack_name, - "self.environment": self.environment, - "rc": 1, - "tfvars_files": self.variables_file, - "self.project_path": f"/tmp/{self.stack_name}/{self.environment}/{self.squad}/{self.name}/{self.project_path}", + "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": "ko", + "error_message": str(e), } From 782c00a98bc625af0045a71bc20f01261d6ff6c6 Mon Sep 17 00:00:00 2001 From: d10s <79284025+D10S0VSkY-OSS@users.noreply.github.com> Date: Sun, 26 Nov 2023 18:43:49 +0100 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=94=A7refactor:=20pydantic=20provider?= =?UTF-8?q?=20validate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/worker/domain/entities/worker.py | 64 ++++++++++++- .../src/worker/domain/services/pipeline.py | 94 +++++++++---------- .../src/worker/domain/services/provider.py | 94 ++++++------------- .../src/worker/providers/hashicorp/actions.py | 50 ++++++++-- .../src/worker/tasks/terraform_worker.py | 7 +- sld-dashboard/app/home/routes.py | 1 + 6 files changed, 179 insertions(+), 131 deletions(-) diff --git a/sld-api-backend/src/worker/domain/entities/worker.py b/sld-api-backend/src/worker/domain/entities/worker.py index 74c1d0a1..b2a1634e 100644 --- a/sld-api-backend/src/worker/domain/entities/worker.py +++ b/sld-api-backend/src/worker/domain/entities/worker.py @@ -1,13 +1,48 @@ from pydantic import BaseModel -from typing import Optional, Any +from typing import Optional, Any, Dict + + +import logging +from typing import List, Tuple +from subprocess import Popen, PIPE + + +class SubprocessHandler: + def run_command(self, command: str) -> Tuple[int, List[str]]: + try: + process = Popen( + command, + shell=True, + stdout=PIPE, + stderr=PIPE, + universal_newlines=True + ) + + # Read stdout and stderr in real-time + output_lines = [] + while True: + line = process.stdout.readline() + logging.info(line.rstrip('\n')) + if not line: + break + output_lines.append(line.strip()) + + # Wait for the process to finish + returncode = process.wait() + return returncode, output_lines + + except Exception as e: + return 1, [str(e)] + class DownloadBinaryParams(BaseModel): version: str - #url: Optional[str] - + + # url: Optional[str] class Config: frozenset = True + class DeployParamsBase(BaseModel): name: str stack_name: str @@ -30,6 +65,7 @@ class TfvarsParams(DeployParamsBase): class config: frozenset = True + class DeployParams(BaseModel): git_repo: str name: str @@ -38,11 +74,12 @@ class DeployParams(BaseModel): squad: str branch: str version: str - variables: Any + variables: Optional[Dict[str, Any]] = {} secreto: Any variables_file: Optional[str] = "" project_path: Optional[str] = "" user: Optional[str] = "" + task_id: Optional[str] = "" class Config: frozenset = True @@ -60,6 +97,7 @@ class DownloadGitRepoParams(BaseModel): class Config: frozenset = True + class ApplyParams(DeployParamsBase, DownloadBinaryParams): branch: str secreto: Any @@ -69,8 +107,24 @@ class ApplyParams(DeployParamsBase, DownloadBinaryParams): class Config: frozenset = True + class PlanParams(ApplyParams): pass + class DestroyParams(ApplyParams): - pass \ No newline at end of file + pass + + +class ActionBase(BaseModel): + name: str + stack_name: str + branch: str + environment: str + squad: str + version: str + secreto: dict + project_path: Optional[str] = "" + variables_file: Optional[str] = "" + task_id: Optional[str] = "" + subprocess_handler: Optional[Any] = SubprocessHandler() diff --git a/sld-api-backend/src/worker/domain/services/pipeline.py b/sld-api-backend/src/worker/domain/services/pipeline.py index 90560021..4f5a3eb0 100644 --- a/sld-api-backend/src/worker/domain/services/pipeline.py +++ b/sld-api-backend/src/worker/domain/services/pipeline.py @@ -14,6 +14,7 @@ decode_responses=True, ) + class DownloadGitRepo: def __init__(self, params: DownloadGitRepoParams, provider: ProviderRequirements): self.params = params @@ -29,6 +30,8 @@ def __call__(self): self.params.branch, self.params.project_path ) + + class DownloadBinary: def __init__(self, params: DownloadBinaryParams, provider: ProviderRequirements): self.params = params @@ -39,6 +42,7 @@ def __call__(self): self.params.version, ) + class RemoteState: def __init__(self, params: RemoteStateParams, provider: ProviderRequirements): self.params = params @@ -53,6 +57,7 @@ def __call__(self): self.params.project_path ) + class Tfvars: def __init__(self, params: TfvarsParams, provider: ProviderRequirements): self.params = params @@ -68,6 +73,7 @@ def __call__(self): self.params.variables ) + class GetVariables: def __init__(self, params: GetVariablesParams, provider: ProviderGetVars): self.params = params @@ -75,30 +81,22 @@ def __init__(self, params: GetVariablesParams, provider: ProviderGetVars): def __call__(self): return self.provider.json_vars( - self.params.name, - self.params.stack_name, - self.params.environment, + self.params.name, + self.params.stack_name, + self.params.environment, self.params.squad, self.params.project_path ) + class plan: def __init__(self, params: PlanParams, provider: ProviderActions): self.params = params self.provider = provider def __call__(self): - return self.provider.plan( - self.params.name, - self.params.stack_name, - self.params.branch, - self.params.environment, - self.params.squad, - self.params.version, - self.params.secreto, - self.params.variables_file, - self.params.project_path, - ) + return self.provider.plan(self.params) + class apply: def __init__(self, params: ApplyParams, provider: ProviderActions): @@ -106,40 +104,23 @@ def __init__(self, params: ApplyParams, provider: ProviderActions): self.provider = provider def __call__(self): - return self.provider.apply( - self.params.name, - self.params.stack_name, - self.params.branch, - self.params.environment, - self.params.squad, - self.params.version, - self.params.secreto, - self.params.variables_file, - self.params.project_path, - ) + return self.provider.apply(self.params) + class destroy: - def __init__(self, params: DestroyParams, provider: ProviderActions): + def __init__(self, params: DeployParams, provider: ProviderActions): self.params = params self.provider = provider def __call__(self): - return self.provider.destroy( - self.params.name, - self.params.stack_name, - self.params.branch, - self.params.environment, - self.params.squad, - self.params.version, - self.params.secreto, - self.params.variables_file, - self.params.project_path, - ) - + return self.provider.destroy(self.params) + + # PIPELINE class Pipeline: def __init__(self, params: DeployParams): self.params = params + def locked_task(self): logging.info(f"Checking if task {self.params.name}-{self.params.squad}-{self.params.environment} is locked in cache server {settings.CACHE_SERVER}") logging.info(f"Locking task {self.params.name}-{self.params.squad}-{self.params.environment}") @@ -148,8 +129,7 @@ def locked_task(self): def unlock_task(self): r.delete(f"{self.params.name}-{self.params.squad}-{self.params.environment}") - - + # Git clone repo def download_git_repo(self): git_params = DownloadGitRepoParams( @@ -166,8 +146,7 @@ def download_git_repo(self): if download_git_repo_result["rc"] != 0: raise Exception(download_git_repo_result) return download_git_repo_result - - + # Download terrafom def download_binary(self): binari_params = DownloadBinaryParams( @@ -209,7 +188,6 @@ def set_tfvars(self): if tfvars_result["rc"] != 0: raise Exception(tfvars_result) return tfvars_result - def get_variables(self): get_variables_params = GetVariablesParams( @@ -227,16 +205,20 @@ def get_variables(self): # Plan execute def execute_plan(self): - plan_params = PlanParams( + plan_params = DeployParams( + git_repo=self.params.git_repo, name=self.params.name, stack_name=self.params.stack_name, - branch=self.params.branch, environment=self.params.environment, squad=self.params.squad, + branch=self.params.branch, version=self.params.version, + variables=self.params.variables, + project_path=self.params.project_path, secreto=self.params.secreto, variables_file=self.params.variables_file, - project_path=self.params.project_path, + user=self.params.user, + task_id=self.params.task_id ) plan_execute = plan(params=plan_params, provider=ProviderActions) plan_result = plan_execute() @@ -246,16 +228,20 @@ def execute_plan(self): # Apply execute def execute_apply(self): - apply_params = ApplyParams( + apply_params = DeployParams( + git_repo=self.params.git_repo, name=self.params.name, stack_name=self.params.stack_name, - branch=self.params.branch, environment=self.params.environment, squad=self.params.squad, + branch=self.params.branch, version=self.params.version, + variables=self.params.variables, + project_path=self.params.project_path, secreto=self.params.secreto, variables_file=self.params.variables_file, - project_path=self.params.project_path, + user=self.params.user, + task_id=self.params.task_id ) apply_execute = apply(params=apply_params, provider=ProviderActions) apply_result = apply_execute() @@ -265,16 +251,20 @@ def execute_apply(self): # Destroy execute def execute_destroy(self): - destroy_params = DestroyParams( + destroy_params = DeployParams( + git_repo=self.params.git_repo, name=self.params.name, stack_name=self.params.stack_name, - branch=self.params.branch, environment=self.params.environment, squad=self.params.squad, + branch=self.params.branch, version=self.params.version, + variables=self.params.variables, + project_path=self.params.project_path, secreto=self.params.secreto, variables_file=self.params.variables_file, - project_path=self.params.project_path, + user=self.params.user, + task_id=self.params.task_id ) destroy_execute = destroy(params=destroy_params, provider=ProviderActions) destroy_result = destroy_execute() diff --git a/sld-api-backend/src/worker/domain/services/provider.py b/sld-api-backend/src/worker/domain/services/provider.py index dcd41db2..bd008bbf 100644 --- a/sld-api-backend/src/worker/domain/services/provider.py +++ b/sld-api-backend/src/worker/domain/services/provider.py @@ -3,6 +3,7 @@ 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 class ProviderRequirements: @@ -75,78 +76,45 @@ class ProviderActions: This class contains the typical methods of a deployment """ - def plan( - name: str, - stack_name: str, - branch: str, - environment: str, - squad: str, - version: str, - secreto: dict, - variables_file: str = "", - project_path: str = "", - action=Actions, - ) -> dict: + def plan(params: DeployParams, action: Actions = Actions) -> dict: config_action = action( - name, - stack_name, - branch, - environment, - squad, - version, - secreto, - variables_file, - project_path, + params.name, + params.stack_name, + params.branch, + params.environment, + params.squad, + params.version, + params.secreto, + params.variables_file, + params.project_path, ) return config_action.execute_terraform_command("plan") - def apply( - name: str, - stack_name: str, - branch: str, - environment: str, - squad: str, - version: str, - secreto: dict, - variables_file: str = "", - project_path: str = "", - action=Actions, - ) -> dict: + def apply(params: DeployParams, action: Actions = Actions) -> dict: config_action = action( - name, - stack_name, - branch, - environment, - squad, - version, - secreto, - variables_file, - project_path, + params.name, + params.stack_name, + params.branch, + params.environment, + params.squad, + params.version, + params.secreto, + params.variables_file, + params.project_path, ) return config_action.execute_terraform_command("apply") - def destroy( - name: str, - stack_name: str, - branch: str, - environment: str, - squad: str, - version: str, - secreto: dict, - variables_file: str = "", - project_path: str = "", - action=Actions, - ) -> dict: + def destroy(params: DeployParams, action: Actions = Actions) -> dict: config_action = action( - name, - stack_name, - branch, - environment, - squad, - version, - secreto, - variables_file, - project_path, + params.name, + params.stack_name, + params.branch, + params.environment, + params.squad, + params.version, + params.secreto, + params.variables_file, + params.project_path, ) return config_action.execute_terraform_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 b635d973..ddd1900e 100644 --- a/sld-api-backend/src/worker/providers/hashicorp/actions.py +++ b/sld-api-backend/src/worker/providers/hashicorp/actions.py @@ -1,7 +1,9 @@ import os +import logging +from typing import List, Tuple import subprocess from dataclasses import dataclass - +from subprocess import Popen, PIPE, STDOUT import jmespath import requests from config.api import settings @@ -9,6 +11,34 @@ from src.worker.security.providers_credentials import secret, unsecret +class SubprocessHandler: + def run_command(self, command: str) -> Tuple[int, List[str]]: + try: + process = Popen( + command, + shell=True, + stdout=PIPE, + stderr=PIPE, + universal_newlines=True + ) + + # Read stdout and stderr in real-time + output_lines = [] + while True: + line = process.stdout.readline() + logging.info(line.rstrip('\n')) + if not line: + break + output_lines.append(line.strip()) + + # Wait for the process to finish + returncode = process.wait() + return returncode, output_lines + + except Exception as e: + return 1, [str(e)] + + @dataclass class StructBase: name: str @@ -24,6 +54,7 @@ class Actions(StructBase): secreto: dict variables_file: str project_path: str + subprocess_handler = SubprocessHandler() def execute_terraform_command(self, command: str) -> dict: try: @@ -41,22 +72,21 @@ def execute_terraform_command(self, command: str) -> dict: else: os.chdir(f"/tmp/{self.stack_name}/{self.environment}/{self.squad}/{self.name}/{self.project_path}") - init_command = f"/tmp/{self.version}/terraform init -input=false --upgrade" + init_command = f"/tmp/{self.version}/terraform init -no-color -input=false --upgrade" plan_command = f"/tmp/{self.version}/terraform {command} -input=false -refresh -no-color -var-file={variables_files} -out={self.stack_name}.tfplan" apply_command = f"/tmp/{self.version}/terraform {command} -input=false -auto-approve -no-color {self.stack_name}.tfplan" destroy_command = f"/tmp/{self.version}/terraform {command} -input=false -auto-approve -no-color -var-file={variables_files}" - result = subprocess.run(init_command, shell=True, capture_output=True, encoding="utf8") - result = subprocess.run(plan_command, shell=True, capture_output=True, encoding="utf8") + result, output = self.subprocess_handler.run_command(init_command) + result, output = self.subprocess_handler.run_command(plan_command) if command == "apply": - result = subprocess.run(apply_command, shell=True, capture_output=True, encoding="utf8") + result, output = self.subprocess_handler.run_command(apply_command) elif command == "destroy": - result = subprocess.run(destroy_command, shell=True, capture_output=True, encoding="utf8") + result, output = self.subprocess_handler.run_command(destroy_command) unsecret(self.stack_name, self.environment, self.squad, self.name, self.secreto) - - rc = result.returncode + rc = result output_data = { "command": command, @@ -68,7 +98,9 @@ def execute_terraform_command(self, command: str) -> dict: "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": result.stderr.split("\n") if rc != 0 else result.stdout.split("\n"), + "stdout": output, + + } return output_data diff --git a/sld-api-backend/src/worker/tasks/terraform_worker.py b/sld-api-backend/src/worker/tasks/terraform_worker.py index 8ea130d6..daa566de 100644 --- a/sld-api-backend/src/worker/tasks/terraform_worker.py +++ b/sld-api-backend/src/worker/tasks/terraform_worker.py @@ -7,10 +7,10 @@ from celery.utils.log import get_task_logger from config.api import settings from config.celery_config import celery_app -from src.worker.domain.entities.worker import DeployParams, DestroyParams, PlanParams, DownloadGitRepoParams, GetVariablesParams +from src.worker.domain.entities.worker import DeployParams, DownloadGitRepoParams from src.worker.domain.services.pipeline import Pipeline -from src.worker.domain.services.provider import ProviderActions, ProviderGetVars, ProviderRequirements +from src.worker.domain.services.provider import ProviderActions from src.worker.tasks.helpers.folders import Utils from src.worker.tasks.helpers.metrics import push_metric from src.worker.tasks.helpers.schedule import request_url @@ -36,6 +36,7 @@ def pipeline_deploy( ): try: + params["task_id"] = self.request.id params = DeployParams(**params) pipeline = Pipeline(params=params) pipeline.locked_task() @@ -74,6 +75,7 @@ def pipeline_destroy( params: DeployParams, ): try: + params["task_id"] = self.request.id params = DeployParams(**params) pipeline = Pipeline(params=params) pipeline.locked_task() @@ -107,6 +109,7 @@ def pipeline_plan( params: DeployParams, ): try: + params["task_id"] = self.request.id params = DeployParams(**params) pipeline = Pipeline(params=params) pipeline.locked_task() diff --git a/sld-dashboard/app/home/routes.py b/sld-dashboard/app/home/routes.py index 66e7d4a3..eefe5095 100644 --- a/sld-dashboard/app/home/routes.py +++ b/sld-dashboard/app/home/routes.py @@ -546,6 +546,7 @@ def clone_deploy(deploy_id): "branch", "tfvar_file", "project_path", + "deploy_name", "squad", "environment", ] From 6c96c557f7adbbe462933da9af631ef415db262b Mon Sep 17 00:00:00 2001 From: d10s <79284025+D10S0VSkY-OSS@users.noreply.github.com> Date: Mon, 27 Nov 2023 02:07:16 +0100 Subject: [PATCH 3/9] =?UTF-8?q?=F0=9F=90=9Bfix:=20when=20terraform=20error?= =?UTF-8?q?=20not=20output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sld-api-backend/config/api.py | 2 +- sld-api-backend/config/celery_config.py | 3 +- .../src/worker/domain/services/command.py | 35 ++++++++++++++ .../src/worker/providers/hashicorp/actions.py | 46 ++++--------------- .../worker/providers/hashicorp/templates.py | 4 +- .../app/home/templates/deploy-edit.html | 2 +- 6 files changed, 50 insertions(+), 42 deletions(-) create mode 100644 sld-api-backend/src/worker/domain/services/command.py diff --git a/sld-api-backend/config/api.py b/sld-api-backend/config/api.py index 36164151..482ed62c 100644 --- a/sld-api-backend/config/api.py +++ b/sld-api-backend/config/api.py @@ -50,7 +50,7 @@ class Settings(BaseSettings): AWS_CONGIG_DEFAULT_FOLDER: str = f"{os.environ['HOME']}/.aws" AWS_SHARED_CREDENTIALS_FILE: str = f"{AWS_CONGIG_DEFAULT_FOLDER}/credentials" AWS_SHARED_CONFIG_FILE: str = f"{AWS_CONGIG_DEFAULT_FOLDER}/config" - TASK_MAX_RETRY: int = os.getenv("SLD_TASK_MAX_RETRY", 1) + TASK_MAX_RETRY: int = os.getenv("SLD_TASK_MAX_RETRY", 0) TASK_RETRY_INTERVAL: int = os.getenv("SLD_TASK_RETRY_INTERVAL", 20) TASK_LOCKED_EXPIRED: int = os.getenv("SLD_TASK_LOCKED_EXPIRED", 300) TASK_ROUTE: bool = os.getenv("SLD_TASK_ROUTE", False) diff --git a/sld-api-backend/config/celery_config.py b/sld-api-backend/config/celery_config.py index f8f3f0cf..22e01b8c 100644 --- a/sld-api-backend/config/celery_config.py +++ b/sld-api-backend/config/celery_config.py @@ -38,4 +38,5 @@ celery_app.conf.update(task_track_started=True) celery_app.conf.update(result_extended=True) celery_app.conf.broker_transport_options = {"visibility_timeout": 28800} # 8 hours. -celery_app.conf.result_expires = os.getenv("SLD_RESULT_EXPIRE", "259200") \ No newline at end of file +celery_app.conf.result_expires = os.getenv("SLD_RESULT_EXPIRE", "259200") +celery_app.conf.broker_connection_retry_on_startup = True \ No newline at end of file diff --git a/sld-api-backend/src/worker/domain/services/command.py b/sld-api-backend/src/worker/domain/services/command.py new file mode 100644 index 00000000..f65df2c2 --- /dev/null +++ b/sld-api-backend/src/worker/domain/services/command.py @@ -0,0 +1,35 @@ +import logging +import subprocess +from subprocess import Popen, PIPE +from typing import Tuple, List + + +class SubprocessHandler: + def run_command(self, command: str) -> Tuple[int, List[str], List[str]]: + try: + process = Popen( + command, + shell=True, + stdout=PIPE, + stderr=PIPE, + universal_newlines=True + ) + + stdout_lines, stderr_lines = process.communicate() + + output_lines = stdout_lines.strip().split('\n') if stdout_lines else [] + error_lines = stderr_lines.strip().split('\n') if stderr_lines else [] + + if process.returncode == 0: + return process.returncode, output_lines + return process.returncode, error_lines + + except subprocess.CalledProcessError as err: + logging.error( + f"Error executing command, code: {err.returncode}. Error:\n{err.output}" + ) + return err.returncode, [], [err.output] + + except Exception as e: + logging.error(f"An error occurred: {e}") + return 1, [], [str(e)] diff --git a/sld-api-backend/src/worker/providers/hashicorp/actions.py b/sld-api-backend/src/worker/providers/hashicorp/actions.py index ddd1900e..27a14af5 100644 --- a/sld-api-backend/src/worker/providers/hashicorp/actions.py +++ b/sld-api-backend/src/worker/providers/hashicorp/actions.py @@ -1,42 +1,13 @@ import os -import logging -from typing import List, Tuple -import subprocess from dataclasses import dataclass -from subprocess import Popen, PIPE, STDOUT import jmespath import requests from config.api import settings from src.worker.security.providers_credentials import secret, unsecret +from src.worker.domain.services.command import SubprocessHandler -class SubprocessHandler: - def run_command(self, command: str) -> Tuple[int, List[str]]: - try: - process = Popen( - command, - shell=True, - stdout=PIPE, - stderr=PIPE, - universal_newlines=True - ) - - # Read stdout and stderr in real-time - output_lines = [] - while True: - line = process.stdout.readline() - logging.info(line.rstrip('\n')) - if not line: - break - output_lines.append(line.strip()) - - # Wait for the process to finish - returncode = process.wait() - return returncode, output_lines - - except Exception as e: - return 1, [str(e)] @dataclass @@ -54,7 +25,7 @@ class Actions(StructBase): secreto: dict variables_file: str project_path: str - subprocess_handler = SubprocessHandler() + subprocess_handler: SubprocessHandler = SubprocessHandler() def execute_terraform_command(self, command: str) -> dict: try: @@ -62,7 +33,7 @@ def execute_terraform_command(self, command: str) -> dict: deploy_state = f"{self.environment}_{self.stack_name}_{self.squad}_{self.name}" variables_files = ( - f"{self.stack_name}.tfvars.json" + f"{self.name}.tfvars.json" if not self.variables_file else self.variables_file ) @@ -73,8 +44,8 @@ def execute_terraform_command(self, command: str) -> dict: os.chdir(f"/tmp/{self.stack_name}/{self.environment}/{self.squad}/{self.name}/{self.project_path}") init_command = f"/tmp/{self.version}/terraform init -no-color -input=false --upgrade" - plan_command = f"/tmp/{self.version}/terraform {command} -input=false -refresh -no-color -var-file={variables_files} -out={self.stack_name}.tfplan" - apply_command = f"/tmp/{self.version}/terraform {command} -input=false -auto-approve -no-color {self.stack_name}.tfplan" + plan_command = f"/tmp/{self.version}/terraform {command} -input=false -refresh -no-color -var-file={variables_files} -out={self.name}.tfplan" + apply_command = f"/tmp/{self.version}/terraform {command} -input=false -auto-approve -no-color {self.name}.tfplan" destroy_command = f"/tmp/{self.version}/terraform {command} -input=false -auto-approve -no-color -var-file={variables_files}" result, output = self.subprocess_handler.run_command(init_command) @@ -86,6 +57,7 @@ def execute_terraform_command(self, command: str) -> dict: result, output = self.subprocess_handler.run_command(destroy_command) unsecret(self.stack_name, self.environment, self.squad, self.name, self.secreto) + rc = result output_data = { @@ -105,7 +77,7 @@ def execute_terraform_command(self, command: str) -> dict: return output_data - except Exception as e: + except Exception as err: return { "command": command, "deploy": self.name, @@ -116,8 +88,8 @@ def execute_terraform_command(self, command: str) -> dict: "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": "ko", - "error_message": str(e), + "stdout": output, + "error_message": err, } diff --git a/sld-api-backend/src/worker/providers/hashicorp/templates.py b/sld-api-backend/src/worker/providers/hashicorp/templates.py index 8af4273b..cbdbd2f9 100644 --- a/sld-api-backend/src/worker/providers/hashicorp/templates.py +++ b/sld-api-backend/src/worker/providers/hashicorp/templates.py @@ -58,9 +58,9 @@ class Tfvars(StructBase): def save(self) -> dict: try: - file_path = f"/tmp/{self.stack_name}/{self.environment}/{self.squad}/{self.name}/{self.stack_name}.tfvars.json" + file_path = f"/tmp/{self.stack_name}/{self.environment}/{self.squad}/{self.name}/{self.name}.tfvars.json" if self.project_path: - file_path = f"/tmp/{self.stack_name}/{self.environment}/{self.squad}/{self.name}/{self.project_path}/{self.stack_name}.tfvars.json" + file_path = f"/tmp/{self.stack_name}/{self.environment}/{self.squad}/{self.name}/{self.project_path}/{self.name}.tfvars.json" with open(file_path, "w") as tfvars_json: json.dump(self.variables, tfvars_json) return {"command": "tfvars", "rc": 0, "stdout": self.variables} diff --git a/sld-dashboard/app/home/templates/deploy-edit.html b/sld-dashboard/app/home/templates/deploy-edit.html index fce3d407..0ae08b27 100644 --- a/sld-dashboard/app/home/templates/deploy-edit.html +++ b/sld-dashboard/app/home/templates/deploy-edit.html @@ -108,7 +108,7 @@
This is a beta version of the feature. Feedback is welcome!
+