-
Notifications
You must be signed in to change notification settings - Fork 1
Mtor/prod 15381 #443
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Mtor/prod 15381 #443
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import os | ||
| import subprocess | ||
| import sys | ||
| from logging import getLogger | ||
| from pathlib import Path | ||
|
|
||
| from click import echo, style | ||
|
|
||
| from Babylon.commands.macro.deploy import dict_to_tfvars | ||
| from Babylon.utils.environment import Environment | ||
|
|
||
| logger = getLogger(__name__) | ||
| env = Environment() | ||
|
|
||
|
|
||
| def deploy_webapp(namespace: str, file_content: str): | ||
| echo(style(f"\n🚀 Deploying webapp in namespace: {env.environ_id}", bold=True, fg="cyan")) | ||
|
|
||
| env.get_ns_from_text(content=namespace) | ||
| state = env.retrieve_state_func() | ||
| content = env.fill_template(data=file_content, state=state) | ||
| payload: dict = content.get("spec").get("payload", {}) | ||
| current_os = sys.platform | ||
| tf_dir = Path(str(env.working_dir)).parent / "terraform-webapp" | ||
| tfvars_path = tf_dir / "terraform.tfvars" | ||
|
|
||
| if "win" in current_os: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. win could be cygwin and I don't think we want this. From the docs https://docs.python.org/3/library/sys.html#sys.platform the comparisson should probably be something like: |
||
| script_name = "_run-terraform.ps1" | ||
| executable = ["powershell.exe", "-ExecutionPolicy", "Bypass", "-File", f"./{script_name}"] | ||
| else: | ||
| script_name = "_run-terraform.sh" | ||
| executable = ["/bin/bash", f"./{script_name}"] | ||
| if (tf_dir / script_name).exists(): | ||
| os.chmod(tf_dir / script_name, 0o755) | ||
|
|
||
| script_path = tf_dir / script_name | ||
|
|
||
| if not script_path.exists(): | ||
| logger.error(f" [bold red]✘[/bold red]Script not found at {script_path}") | ||
| return | ||
| try: | ||
| hcl_content = dict_to_tfvars(payload) | ||
| with open(tfvars_path, "w") as f: | ||
| f.write(hcl_content) | ||
| except Exception as e: | ||
| logger.error(f" [bold red]✘[/bold red] Failed to write tfvars: {e}") | ||
| return | ||
| logger.info(" [dim]→ Running Terraform deployment...[/dim]") | ||
| try: | ||
| process = subprocess.Popen( | ||
| executable, cwd=tf_dir, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1 | ||
| ) | ||
| for line in process.stdout: | ||
| clean_line = line.strip() | ||
| if not clean_line: | ||
| continue | ||
| if any(key in clean_line for key in ["Initializing", "Upgrading", "Finding", "Refreshing"]): | ||
| echo(style(f" {clean_line}", fg="white")) | ||
| elif any(key in clean_line for key in ["Success", "complete", "Resources:"]): | ||
| echo(style(f" {clean_line}", fg="green")) | ||
| elif "Error" in clean_line or "error" in clean_line: | ||
| echo(style(f" {clean_line}", fg="red", bold=True)) | ||
| else: | ||
| echo(style(f" {clean_line}", fg="white")) | ||
| return_code = process.wait() | ||
| webapp_name = payload.get("webapp_name") | ||
| cluster_domain = payload.get("cluster_domain") | ||
| tenant_name = payload.get("tenant") | ||
| webapp_url = f"https://{cluster_domain}/{tenant_name}/webapp-{webapp_name}" | ||
| services = state.get("services", {}) | ||
| if "webapp" not in services: | ||
| services["webapp"] = {} | ||
| services["webapp"]["webapp_name"] = f"webapp-{webapp_name}" | ||
| services["webapp"]["webapp_url"] = webapp_url | ||
| if return_code == 0: | ||
| logger.info( | ||
| f" [bold green]✔[/bold green] WebApp [bold white]{webapp_name}[/bold white] deployed successfully" | ||
| ) | ||
| env.store_state_in_local(state) | ||
| if env.remote: | ||
| env.store_state_in_cloud(state) | ||
| else: | ||
| logger.error(f" [bold red]✘[/bold red] Deployment failed with exit code {return_code}") | ||
| except Exception as e: | ||
| logger.error(f" [bold red]✘[/bold red] Execution error: {e}") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,6 @@ | ||
| import subprocess | ||
| from logging import getLogger | ||
| from pathlib import Path | ||
| from typing import Callable | ||
|
|
||
| from click import command, echo, option, style | ||
|
|
@@ -16,6 +18,53 @@ | |
| env = Environment() | ||
|
|
||
|
|
||
| def _destroy_webapp(state: dict): | ||
| """Run the Terraform destroy process for WebApp resources""" | ||
| logger.info(" [dim]→ Running Terraform destroy for WebApp resources...[/dim]") | ||
| webapp_state = state.get("services", {}).get("webapp", {}) | ||
| webapp_neme = webapp_state.get("webapp_name") | ||
| if not webapp_neme: | ||
| logger.warning(" [yellow]⚠[/yellow] [dim]No WebApp found in state! skipping deletion [dim]") | ||
| return | ||
| tf_dir = Path(str(env.working_dir)).parent / "terraform-webapp" | ||
|
|
||
| if not tf_dir.exists(): | ||
| logger.error(f" [bold red]✘[/bold red] Terraform directory not found at {tf_dir}") | ||
| return | ||
| try: | ||
| process = subprocess.Popen( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not the simpler subprocess.run ? do we need an advanced behavior here?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there are way too many levels of if/else here, sonarqube will flag this as too complex |
||
| ["terraform", "destroy", "-auto-approve"], | ||
| cwd=tf_dir, | ||
| stdout=subprocess.PIPE, | ||
| stderr=subprocess.STDOUT, | ||
| text=True, | ||
| bufsize=1, | ||
| ) | ||
| for line in process.stdout: | ||
| clean_line = line.strip() | ||
| if not clean_line: | ||
| continue | ||
|
|
||
| if "Destroy complete!" in clean_line or "Resources:" in clean_line: | ||
| echo(style(f" {clean_line}", fg="green")) | ||
| elif "Error" in clean_line: | ||
| echo(style(f" {clean_line}", fg="red", bold=True)) | ||
| else: | ||
| echo(style(f" {clean_line}", fg="white")) | ||
|
|
||
| process.wait() | ||
| if process.returncode == 0: | ||
| # Nettoyage du state webapp | ||
| state["services"]["webapp"]["webapp_name"] = "" | ||
| state["services"]["webapp"]["webapp_url"] = "" | ||
| logger.info(f" [green]✔[/green] WebApp [magenta]{webapp_neme}[/magenta] destroyed") | ||
| else: | ||
| logger.error(f" [bold red]✘[/bold red] Terraform destroy failed (Code {process.returncode})") | ||
|
|
||
| except Exception as e: | ||
| logger.error(f" [bold red]✘[/bold red] Error during WebApp destruction: {e}") | ||
|
|
||
|
|
||
| def _delete_resource( | ||
| api_call: Callable[..., None], resource_name: str, org_id: str | None, resource_id: str, state: dict, state_key: str | ||
| ): | ||
|
|
@@ -34,7 +83,14 @@ def _delete_resource( | |
| logger.info(f" [bold green]✔[/bold green] {resource_name} [magenta]{resource_id}[/magenta] deleted") | ||
| state["services"]["api"][state_key] = "" | ||
| except Exception as e: | ||
| logger.error(f" [bold red]✘[/bold red]] Error deleting {resource_name.lower()} {resource_id} reason: {e}") | ||
| error_msg = str(e) | ||
| if "404" in error_msg or "Not Found" in error_msg: | ||
| logger.info( | ||
| f" [bold yellow]⚠[/bold yellow] {resource_name} [magenta]{resource_id}[/magenta] already deleted (404)" | ||
| ) | ||
| state["services"]["api"][state_key] = "" | ||
| else: | ||
| logger.error(f" [bold red]✘[/bold red] Error deleting {resource_name.lower()} {resource_id} reason: {e}") | ||
|
|
||
|
|
||
| @command() | ||
|
|
@@ -48,7 +104,7 @@ def destroy( | |
| exclude: tuple[str], | ||
| ): | ||
| """Macro Destroy""" | ||
| organization, solution, workspace = resolve_inclusion_exclusion(include, exclude) | ||
| organization, solution, workspace, webapp = resolve_inclusion_exclusion(include, exclude) | ||
| # Header for the destructive operation | ||
| echo(style(f"\n🔥 Starting Destruction Process in namespace: {env.environ_id}", bold=True, fg="red")) | ||
| keycloak_token, config = get_keycloak_token() | ||
|
|
@@ -57,7 +113,6 @@ def destroy( | |
| api_state = state["services"]["api"] | ||
| org_id = api_state["organization_id"] | ||
|
|
||
| # 1. Targeted Deletions using the helper | ||
| if solution: | ||
| api = get_solution_api_instance(config=config, keycloak_token=keycloak_token) | ||
| _delete_resource(api.delete_solution, "Solution", org_id, api_state["solution_id"], state, "solution_id") | ||
|
|
@@ -69,6 +124,9 @@ def destroy( | |
| api = get_organization_api_instance(config=config, keycloak_token=keycloak_token) | ||
| _delete_resource(api.delete_organization, "Organization", None, org_id, state, "organization_id") | ||
|
|
||
| if webapp: | ||
| _destroy_webapp(state) | ||
|
|
||
| # --- State Persistence --- | ||
| env.store_state_in_local(state=state) | ||
| if state.get("remote"): | ||
|
|
@@ -87,10 +145,15 @@ def destroy( | |
| # We check if the ID is now empty (which means it was deleted) | ||
| status = "DELETED" if not value else value | ||
| color = "red" if status == "DELETED" else "green" | ||
|
|
||
| styled_label = style(f"{label_text:<20}:", fg="cyan", bold=True) | ||
| styled_status = style(status, fg=color) | ||
| echo(f"{styled_label} {styled_status}") | ||
| echo(f"{style(f'{label_text:<20}:', fg='cyan', bold=True)} {style(status, fg=color)}") | ||
|
|
||
| # Affichage WebApp | ||
| webapp_data = services.get("webapp", {}) | ||
| webapp_id = webapp_data.get("webapp_name") | ||
| label_text = " • Webapp Name" | ||
| status = "DELETED" if not webapp_id else webapp_id | ||
| color = "red" if status == "DELETED" else "green" | ||
| echo(f"{style(f'{label_text:<20}:', fg='cyan', bold=True)} {style(status, fg=color)}") | ||
|
|
||
| echo(style("\n✨ Cleanup process complete", fg="white", bold=True)) | ||
| return CommandResponse.success() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import subprocess | ||
| from logging import getLogger | ||
| from os import getcwd | ||
| from pathlib import Path | ||
|
|
@@ -26,7 +27,23 @@ def init(project_folder: str, variables_file: str): | |
| if variables_path.exists(): | ||
| logger.warning(f"Configuration file [bold]{variables_file}[/bold] already exists.") | ||
| return None | ||
| project_yaml_files = ["Organization.yaml", "Solution.yaml", "Workspace.yaml", "Dataset.yaml", "Runner.yaml"] | ||
| tf_webapp_path = Path(getcwd()) / "terraform-webapp" | ||
| repo_url = "https://github.com/Cosmo-Tech/terraform-webapp.git" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are introducing some hidden dependencies here. Now Babylon depends on terraform-webapp which depends on terraform etc and this is not visible in the Babylon metadata (in pyprojects.toml). This will make tools that automatically scan dependencies (e.g. dependency track) to have a blind spot, and also users installing Babylon will not know they need to configure this dependency. |
||
| if not tf_webapp_path.exists(): | ||
| logger.info(" [dim]→ Cloning Terraform WebApp module...[/dim]") | ||
| try: | ||
| subprocess.run(["git", "clone", "-q", repo_url, str(tf_webapp_path)], check=True, stdout=subprocess.DEVNULL) | ||
| logger.info(" [green]✔[/green] Terraform WebApp module cloned") | ||
| except Exception as e: | ||
| logger.error(f" [bold red]✘[/bold red] Failed to clone Terraform repo: {e}") | ||
| project_yaml_files = [ | ||
| "Organization.yaml", | ||
| "Solution.yaml", | ||
| "Workspace.yaml", | ||
| "Dataset.yaml", | ||
| "Runner.yaml", | ||
| "Webapp.yaml", | ||
| ] | ||
| try: | ||
| # Create project directory | ||
| project_path.mkdir(parents=True, exist_ok=True) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| kind: Webapp | ||
| namespace: | ||
| remote: false | ||
| spec: | ||
| payload: | ||
| cloud_provider: "azure" | ||
| cluster_name: "{{cluster_name}}" | ||
| cluster_domain: "{{cluster_domain}}" | ||
| tenant: "{{tenant}}" | ||
| webapp_name: "{{webapp_name}}" | ||
| organization_id: "{{services['api.organization_id']}}" | ||
| azure_subscription_id: "{{azure_subscription_id}}" | ||
| azure_entra_tenant_id: "{{azure_entra_tenant_id}}" | ||
| powerbi_app_deploy: false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest to break this in smaller steps. First expose the terraform to the end user to avoid hard bugs, when the codebase is more mature we can then hide it and make things modular.