From 4f1cf4e5620dbcc8111f0c01dbd9e6983cd45691 Mon Sep 17 00:00:00 2001 From: Eliezer Rodrigues Soares Date: Tue, 8 Oct 2024 11:41:24 -0300 Subject: [PATCH] add rollback and destroy action --- runtime-destroy-action/.stkignore | 1 - runtime-destroy-action/action.yaml | 32 +--- runtime-destroy-action/docs/pt-br/docs.md | 46 ----- runtime-destroy-action/script.py | 109 +++++++++++ runtime-destroy-action/templates/script.sh | 30 --- runtime-rollback-action/.stkignore | 1 - runtime-rollback-action/docs/pt-br/docs.md | 54 ------ runtime-rollback-action/script.py | 202 +++++++++++---------- script.py | 24 ++- 9 files changed, 243 insertions(+), 256 deletions(-) delete mode 100644 runtime-destroy-action/.stkignore delete mode 100644 runtime-destroy-action/docs/pt-br/docs.md create mode 100644 runtime-destroy-action/script.py delete mode 100644 runtime-destroy-action/templates/script.sh delete mode 100644 runtime-rollback-action/.stkignore delete mode 100644 runtime-rollback-action/docs/pt-br/docs.md diff --git a/runtime-destroy-action/.stkignore b/runtime-destroy-action/.stkignore deleted file mode 100644 index e6045cc..0000000 --- a/runtime-destroy-action/.stkignore +++ /dev/null @@ -1 +0,0 @@ -# Este arquivo pode ser usado para ignorar arquivos e pastas ao publicar um artefato. \ No newline at end of file diff --git a/runtime-destroy-action/action.yaml b/runtime-destroy-action/action.yaml index acf2dfc..5cc6a68 100644 --- a/runtime-destroy-action/action.yaml +++ b/runtime-destroy-action/action.yaml @@ -6,16 +6,16 @@ metadata: description: runtime-destroy-action version: 0.0.1 spec: - type: shell + type: python docs: - pt-br: docs/pt-br/docs.md en-us: docs/en-us/docs.md repository: https://github.com/stack-spot/workflow-stackspot-actions-runtime-selfhosted.git inputs: - label: "Features Log Level" name: features_level_log type: text - required: true + required: false + - label: "CLIENT ID" name: client_id type: text @@ -56,20 +56,17 @@ spec: name: run_task_id type: text required: true - - label: "Deploy Container url" - name: container_url - type: text - default: "stackspot/runtime-job-destroy:latest" - required: false + - label: "Features Terraform Modules" name: features_terraform_modules type: text required: false + - label: "Path to mount inside the docker" name: path_to_mount type: text - default: "." - required: true + required: false + - label: "If Runtimes will allow execution of the local-exec command within terraform" name: localexec_enabled type: bool @@ -79,16 +76,7 @@ spec: name: tf_log_provider type: text required: false - shell: + + python: workdir: . - - script: - linux: | - chmod +x {{component_path}}/script.sh - sh {{component_path}}/script.sh - mac: | - chmod +x {{component_path}}/script.sh - sh {{component_path}}/script.sh - windows: | - echo "Not supported" - \ No newline at end of file + script: script.py \ No newline at end of file diff --git a/runtime-destroy-action/docs/pt-br/docs.md b/runtime-destroy-action/docs/pt-br/docs.md deleted file mode 100644 index 01b6299..0000000 --- a/runtime-destroy-action/docs/pt-br/docs.md +++ /dev/null @@ -1,46 +0,0 @@ -Preencha corretamente este template para que os usuários consigam utilizar o seu conteúdo. As informações serão expostas na página da Action no Portal da StackSpot. - -## Nome Action - -Escreva uma descrição clara e breve do conteúdo da Action. - -Exemplo: -> Esta Action contém instruções de como preencher as informações para usar as Actions na plataforma StackSpot. - -## Pré-requisitos - -- Descreva em uma lista todos os itens e ações necessárias antes de executar a Action. - -Exemplo: -1. Instalar dependências -2. Crie o arquivo de configuração -3. Crie a pasta **template** - -## Uso - -Descreva as etapas para o usuário utilizar esta Action: - -1. Quais as entradas -2. Quais os métodos usar -3. Quais os recursos -4. E se necessário, adicione as dependências de sua Action. - -Exemplo: -Na pasta do seu aplicativo, execute a **action-doc-template** para preencher os arquivos abaixo: - -1. Execute o comando: -` -stk run action /Users/Home/action-doc-template -` - -2. Preencha as informações da Action seguindo os exemplos de modelo de arquivo x; - -## Release Notes - -Esta seção só é necessária se você publicar uma nova versão da Action. Apenas adicione o que foi modificado ou adicionado. - -Exemplo: -### action-doc-template v1.0.0 - -#### Features -Novos templates foram adicionados. diff --git a/runtime-destroy-action/script.py b/runtime-destroy-action/script.py new file mode 100644 index 0000000..6f9d631 --- /dev/null +++ b/runtime-destroy-action/script.py @@ -0,0 +1,109 @@ +import os +import sys +import subprocess +import logging +from typing import List + +# Configure logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + + +STK_IAM_DOMAIN = os.getenv("STK_IAM_DOMAIN", "https://idm.stackspot.com") +STK_RUNTIME_MANAGER_DOMAIN = os.getenv("STK_RUNTIME_MANAGER_DOMAIN", "https://runtime-manager.v1.stackspot.com") +CONTAINER_DESTROY_URL = os.getenv("CONTAINER_DESTROY_URL", "stackspot/runtime-job-destroy:latest") + +FEATURES_BASEPATH_TMP = "/tmp/runtime/deploys" +FEATURES_BASEPATH_EBS = "/opt/runtime" +FEATURES_TEMPLATES_FILEPATH = "/app/" +FEATURES_BASEPATH_TERRAFORM = "/root/.asdf/shims/terraform" + + +def check(result: subprocess.Popen) -> None: + """ + Checks the result of a subprocess execution. If the return code is non-zero, + it logs an error message and exits the program. + + Args: + result (subprocess.Popen): The result of the subprocess execution. + """ + result.wait() # Wait for the process to complete + if result.returncode != 0: + logging.error(f"Failed to execute: {result.args}") + logging.error(f"Error output: {result.stderr.read()}") + sys.exit(1) + + +def run_command(command: List[str]) -> subprocess.Popen: + """ + Runs a command using subprocess.Popen and returns the result. + + Args: + command (List[str]): The command to be executed as a list of strings. + + Returns: + subprocess.Popen: The result of the command execution. + """ + try: + logging.info(f"Running command: {' '.join(command)}") + # Start the process + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + + # Read and print output in real-time + for line in process.stdout: + print(line, end="") # Print each line as it is produced + + # Check the result after the process completes + check(process) + return process + except Exception as e: + logging.error(f"Exception occurred while running command: {command}") + logging.error(str(e)) + sys.exit(1) + + +def build_flags(inputs: dict) -> list: + + docker_flags: dict = dict( + FEATURES_LEVEL_LOG=inputs.get("features_level_log") or "info", + FEATURES_TERRAFORM_LOGPROVIDER=inputs.get("tf_log_provider") or "info", + FEATURES_RELEASE_LOCALEXEC=inputs.get("localexec_enabled") or "False", + FEATURES_TERRAFORM_MODULES=inputs.get("features_terraform_modules") or '[]', + + AWS_ACCESS_KEY_ID=inputs['aws_access_key_id'], + AWS_SECRET_ACCESS_KEY=inputs['aws_secret_access_key'], + AWS_SESSION_TOKEN=inputs['aws_session_token'], + AUTHENTICATE_CLIENT_ID=inputs["client_id"], + AUTHENTICATE_CLIENT_SECRET=inputs["client_key"], + AUTHENTICATE_CLIENT_REALMS=inputs["client_realm"], + REPOSITORY_NAME=inputs["repository_name"], + AWS_REGION=inputs["aws_region"], + + AUTHENTICATE_URL=STK_IAM_DOMAIN, + FEATURES_API_MANAGER=STK_RUNTIME_MANAGER_DOMAIN, + FEATURES_BASEPATH_TMP=FEATURES_BASEPATH_TMP, + FEATURES_BASEPATH_EBS=FEATURES_BASEPATH_EBS, + FEATURES_TEMPLATES_FILEPATH=FEATURES_TEMPLATES_FILEPATH, + FEATURES_BASEPATH_TERRAFORM=FEATURES_BASEPATH_TERRAFORM + ) + flags = [] + for k, v in docker_flags.items(): + flags += ["-e", f"{k}={v}"] + + return flags + + +def run(metadata): + inputs: dict = metadata.inputs + run_task_id: str = inputs["run_task_id"] + path_to_mount: str = inputs.get("path_to_mount") or "." + path_to_mount = f"{path_to_mount}:/app-volume" + + flags = build_flags(inputs) + cmd = ["docker", "run", "--rm", "-v", path_to_mount] + flags + [ + "--entrypoint=/app/stackspot-runtime-job-destroy", + CONTAINER_DESTROY_URL, + "start", + f"--run-task-id={run_task_id}", + ] + + run_command(cmd) \ No newline at end of file diff --git a/runtime-destroy-action/templates/script.sh b/runtime-destroy-action/templates/script.sh deleted file mode 100644 index a86c673..0000000 --- a/runtime-destroy-action/templates/script.sh +++ /dev/null @@ -1,30 +0,0 @@ -FLAGS=$(echo "-v {{ inputs.path_to_mount }}:/app-volume \ --e FEATURES_LEVEL_LOG={{ inputs.features_level_log }} \ --e AUTHENTICATE_CLIENT_ID={{ inputs.client_id }} \ --e AUTHENTICATE_CLIENT_SECRET={{ inputs.client_key }} \ --e AUTHENTICATE_CLIENT_REALMS={{ inputs.client_realm }} \ --e AUTHENTICATE_URL=https://idm.stackspot.com \ --e REPOSITORY_NAME={{ inputs.repository_name }} \ --e FEATURES_API_MANAGER=https://runtime-manager.v1.stackspot.com \ --e FEATURES_BASEPATH_TMP=/tmp/runtime/deploys \ --e FEATURES_BASEPATH_EBS=/opt/runtime \ --e FEATURES_TEMPLATES_FILEPATH=/app/ \ --e FEATURES_BASEPATH_TERRAFORM=/root/.asdf/shims/terraform \ --e AWS_REGION={{ inputs.aws_region }} \ --e FEATURES_RELEASE_LOCALEXEC={{ inputs.localexec_enabled }}") - -if [ -z "{{ inputs.aws_role_arn }}" ]; then - FLAGS=$(echo "$FLAGS -e AWS_ACCESS_KEY_ID={{ inputs.aws_access_key_id }}") - FLAGS=$(echo "$FLAGS -e AWS_SECRET_ACCESS_KEY={{ inputs.aws_secret_access_key }}") - FLAGS=$(echo "$FLAGS -e AWS_SESSION_TOKEN={{ inputs.aws_session_token }}") -fi - -if [ ! -z "{{ inputs.tf_log_provider }}" ]; then - FLAGS=$(echo "$FLAGS -e FEATURES_TERRAFORM_LOGPROVIDER={{ inputs.tf_log_provider }}") -fi - -docker run --rm \ -$FLAGS \ --e FEATURES_TERRAFORM_MODULES='{{ inputs.features_terraform_modules }}' \ ---entrypoint=/app/stackspot-runtime-job-destroy \ -{{ inputs.container_url }} start --run-task-id="{{ inputs.run_task_id }}" \ No newline at end of file diff --git a/runtime-rollback-action/.stkignore b/runtime-rollback-action/.stkignore deleted file mode 100644 index e6045cc..0000000 --- a/runtime-rollback-action/.stkignore +++ /dev/null @@ -1 +0,0 @@ -# Este arquivo pode ser usado para ignorar arquivos e pastas ao publicar um artefato. \ No newline at end of file diff --git a/runtime-rollback-action/docs/pt-br/docs.md b/runtime-rollback-action/docs/pt-br/docs.md deleted file mode 100644 index 9778ea6..0000000 --- a/runtime-rollback-action/docs/pt-br/docs.md +++ /dev/null @@ -1,54 +0,0 @@ - -## Nome Action - - - -## Pré-requisitos - - - -## Uso - - - -## Release Notes - - \ No newline at end of file diff --git a/runtime-rollback-action/script.py b/runtime-rollback-action/script.py index 1bf2ee8..19a6534 100644 --- a/runtime-rollback-action/script.py +++ b/runtime-rollback-action/script.py @@ -1,3 +1,4 @@ +import os import json from pathlib import Path from ruamel.yaml import YAML @@ -5,6 +6,16 @@ from typing import Dict from oscli.core.http import post_with_authorization from oscli.core.http import get_with_authorization +from sys import exit + +# Constants +STK_RUNTIME_MANAGER_DOMAIN = os.getenv("STK_RUNTIME_MANAGER_DOMAIN", "https://runtime-manager.v1.stackspot.com") +STK_WORKSPACE_DOMAIN = os.getenv("STK_WORKSPACE_DOMAIN", "https://runtime-manager.v1.stackspot.com") +STK_FILE = '.stk/stk.yaml' +OUTPUT_FILE = 'rollback-output.log' +HEADERS = {'Content-Type': 'application/json'} +TIMEOUT = 20 + def yaml() -> YAML: yml = YAML() @@ -14,120 +25,123 @@ def yaml() -> YAML: yml.preserve_quotes = True return yml - def safe_load(content: str) -> dict: yml = yaml() return yml.load(StringIO(content)) -def save_output(name: str, value: str): - with open("rollback-output.log", 'a') as output_file: - print(f'{name}={value}', file=output_file) - - -def get_env_id(slug): - - env_request = get_with_authorization(url=f"https://workspace-workspace-api.stg.stackspot.com/v1/environments", - headers={'Content-Type': 'application/json' }, - timeout=20) - - if env_request.status_code != 200: - print("Unable to fetch Environments data") - print("- Status:", env_request.status_code) - print("- Error:", env_request.reason) - print("- Response:", env_request.text) +def get_stk_yaml() -> dict: + try: + with open(Path(STK_FILE), 'r') as file: + stk_yaml = file.read() + stk_yaml = safe_load(stk_yaml) + return stk_yaml + except FileNotFoundError: + print(f"> Error: {STK_FILE} not found.") + exit(1) + except Exception as e: + print(f"> Error reading {STK_FILE}: {e}") exit(1) - - env_list = env_request.json() - - for env in env_list: - if env["name"] == slug: - return env["id"] - - print(f"Unable to find environment: {slug}") - exit(1) -def run(metadata): +def save_output(value: dict): - TF_STATE_BUCKET_NAME = metadata.inputs.get("tf_state_bucket_name") - TF_STATE_REGION = metadata.inputs.get("tf_state_region") - IAC_BUCKET_NAME = metadata.inputs.get("iac_bucket_name") - IAC_REGION = metadata.inputs.get("iac_region") - VERBOSE = metadata.inputs.get("verbose") - VERSION_TAG = metadata.inputs.get("version_tag") - ENVIRONMENT = metadata.inputs.get("environment") + with open(OUTPUT_FILE, 'w') as output_file: + json.dump(value, output_file, indent=4) + print(f"> Output saved to {OUTPUT_FILE}") + - inputs_list = [ENVIRONMENT, VERSION_TAG, TF_STATE_BUCKET_NAME, TF_STATE_REGION, IAC_BUCKET_NAME, IAC_REGION] +def build_request(action_inputs: dict, env_id: str, stk_yaml: dict) -> dict: - if None in inputs_list: - print("- Some mandatory input is empty. Please, check the input list.") + try: + # Extract the type of deployment (app or infra) from the stack YAML configuration + type = get_type(stk_yaml) + + # Define a parser to map the type to the appropriate key and value for the request payload + type_parser = { + "app": {"key": "appId", "value": stk_yaml["spec"].get("app-id")}, + "infra": {"key": "infraId", "value": stk_yaml["spec"].get("infra-id")} + } + + # Build the request payload using the provided inputs and the parsed type information + request_data = { + f"{type_parser[type]['key']}": type_parser[type]['value'], + "envId": env_id, + "tag": action_inputs["version_tag"], + "config": { + "tfstate": { + "bucket": action_inputs["tf_state_bucket_name"], + "region": action_inputs["tf_state_region"] + }, + "iac": { + "bucket": action_inputs["iac_bucket_name"], + "region": action_inputs["iac_region"] + } + }, + "pipelineUrl": "http://stackspot.com", + } + + print(f"> Runtime manager run self hosted rollback request data:\n{json.dumps(request_data, indent=4)}") + return request_data + except KeyError as e: + print(f"> Error: Missing required input {e}") exit(1) - with open(Path('.stk/stk.yaml'), 'r') as file: - stk_yaml = file.read() +def get_type(stk_yaml: dict) -> str: + return stk_yaml["spec"]["type"] - stk_dict = safe_load(stk_yaml) +def runtime_manager_run_self_hosted_rollback(request_data: dict, stk_yaml: dict, version_tag: str): - if VERBOSE is not None: - print("- stk.yaml:", stk_dict) - - stk_yaml_type = stk_dict["spec"]["type"] - app_or_infra_id = stk_dict["spec"]["infra-id"] if stk_yaml_type == "infra" else stk_dict["spec"]["app-id"] - - print(f"{stk_yaml_type} project identified, with ID: {app_or_infra_id}") - - - stk_id = { - "appId": app_or_infra_id, - } if stk_yaml_type != "infra" else { - "infraId": app_or_infra_id, - } - - request_data = { - **stk_id, - "envId": get_env_id(ENVIRONMENT), - "tag": VERSION_TAG, - "config": { - "tfstate": { - "bucket": TF_STATE_BUCKET_NAME, - "region": TF_STATE_REGION - }, - "iac": { - "bucket": IAC_BUCKET_NAME, - "region": IAC_REGION - } - }, - "pipelineUrl": "http://stackspot.com" - } - - if VERBOSE is not None: - print("- ROLLBACK RUN REQUEST DATA:", request_data) + type = get_type(stk_yaml) + url = f"{STK_RUNTIME_MANAGER_DOMAIN}/v1/run/self-hosted/rollback/{type}" + print("> Calling runtime manager to rollback tasks...") + response = post_with_authorization(url=url, body=request_data, headers=HEADERS, timeout=TIMEOUT) + + if response.ok: + if(response.status_code == 201): + response_data = response.json() + print(f"> Rollback successfully started:\n{json.dumps(response_data, indent=4)}") + else: + print(f"> Rollback successfully but no modifications detected for tag {version_tag}") + response_data = {} + + save_output(response_data) + + else: + print(f"> Error: Failed to start self-hosted rollback run. Status: {response.status_code}") + print(f"> Response: {response.text}") + exit(1) - print("Deploying Self-Hosted Rollback..") - rollback_request = post_with_authorization(url=f"https://runtime-manager.v1.stackspot.com/v1/run/self-hosted/rollback/{stk_yaml_type}", - body=request_data, - headers={'Content-Type': 'application/json' }, - timeout=20) +def get_environment_id(environment_slug): + url = f"{STK_WORKSPACE_DOMAIN}/v1/environments" - if rollback_request.status_code == 201: - d2 = rollback_request.json() - runId = d2["runId"] - runType = d2["runType"] - tasks = d2["tasks"] + print("> Calling workspace to load environments...") + response = get_with_authorization(url=url,headers=HEADERS,timeout=TIMEOUT) + + if response.ok: + env_list = response.json() + for env in env_list: + if env["name"] == environment_slug: + return env["id"] - save_output('tasks', tasks) - save_output('run_id', runId) + else: + print(f"> Error: Failed to load environments. Status: {response.status_code}") + print(f"> Response: {response.text}") + exit(1) + - print(f"- Rollback RUN {runType} successfully started with ID: {runId}") - print(f"- Rollback RUN TASKS LIST: {tasks}") +def run(metadata): + # Load the manifest file + stk_yaml = get_stk_yaml() - else: - print("- Error starting self hosted rollback run") - print("- Status:", rollback_request.status_code) - print("- Error:", rollback_request.reason) - print("- Response:", rollback_request.text) - exit(1) \ No newline at end of file + # Load environment Id + env_id = get_environment_id(metadata.inputs.get("environment")) + + # Build the request data + request = build_request(metadata.inputs, env_id, stk_yaml) + + # Execute the rollback request + runtime_manager_run_self_hosted_rollback(request, stk_yaml, metadata.inputs['version_tag']) \ No newline at end of file diff --git a/script.py b/script.py index 8144d9d..5b6e13b 100644 --- a/script.py +++ b/script.py @@ -25,24 +25,32 @@ def deploy_workflow(run_action: RunAction): run_action("runtime-create-manifest-action") run_action("runtime-manager-action") - with open('manager-output.log', 'r') as file: + run_tasks("manager-output.log", run_action) + +def cancel_deploy_run(run_action: RunAction): + run_action("runtime-cancel-run-action") + +def rollback_deploy_run(run_action: RunAction): + run_action("runtime-rollback-action") + run_tasks("rollback-output.log", run_action) + + +def run_tasks(file_tasks: str, run_action: RunAction): + with open(file_tasks, 'r') as file: data = json.loads(file.read().replace("\'", "\"")) task_runners = dict( IAC_SELF_HOSTED=lambda **i: run_action("runtime-iac-action", **i), DEPLOY_SELF_HOSTED=lambda **i: run_action("runtime-deploy-action", **i), + DESTROY_SELF_HOSTED=lambda **i: run_action("runtime-destroy-action", **i), ) - for t in data['tasks']: + + for t in data.get('tasks') or []: runner = task_runners.get(t["taskType"]) runner and runner(run_task_id=t["runTaskId"]) - -def cancel_deploy_run(run_action: RunAction): - run_action("runtime-cancel-run-action") - - def run(metadata): - workflows = dict(deploy=deploy_workflow, cancel_deploy=cancel_deploy_run) + workflows = dict(deploy=deploy_workflow, cancel_deploy=cancel_deploy_run, rollback_deploy=rollback_deploy_run) run_action = RunAction(metadata) workflow_runner = workflows.get(metadata.inputs["workflow_type"]) workflow_runner and workflow_runner(run_action=run_action)