diff --git a/.github/workflows/lint-quality-check.yml b/.github/workflows/lint-quality-check.yml new file mode 100644 index 0000000..90e9052 --- /dev/null +++ b/.github/workflows/lint-quality-check.yml @@ -0,0 +1,30 @@ +name: Quality Check +on: + workflow_call: + secrets: + git-org-token: + required: true + pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + static_tests: + runs-on: ubuntu-latest + steps: + + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install linters + run: | + pip install black + + - name: Perform lint + run: | + black . --check diff --git a/.github/workflows/stk-quality-check.yaml b/.github/workflows/stk-quality-check.yaml new file mode 100644 index 0000000..6b9190f --- /dev/null +++ b/.github/workflows/stk-quality-check.yaml @@ -0,0 +1,88 @@ +name: STK actions quality-check + +on: + pull_request: + +jobs: + validate-actions: + name: StackSpot Runtime (v2) + runs-on: ubuntu-latest + env: + LANG: C.UTF-8 + LANGUAGE: C.UTF-8 + LC_ALL: C.UTF-8 + PYTHONIOENCODING: utf-8 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - run: sudo apt update && sudo apt install -y curl unzip git jq + name: install dependencies + + - name: Setup STK CLI + run: | + curl \ + --fail \ + --http2-prior-knowledge \ + --location \ + --output /tmp/stk.deb \ + --silent \ + --show-error \ + --tlsv1.3 \ + https://stk.stackspot.com/installer/linux/stk.deb + sudo dpkg --install /tmp/stk.deb || echo installed + rm --force /tmp/stk.deb + + - name: Show STK CLI version + run: $HOME/.stk/bin/stk --version + + - name: Login StackSpot + run: | + $HOME/.stk/bin/stk login -id ${{ secrets.STK_CLIENT_ID }} -key ${{ secrets.STK_CLIENT_SECRET }} -r ${{ secrets.STK_REALM }} + + - name: Validate main action + run: | + $HOME/.stk/bin/stk validate action + + - name: Validate runtime-cancel-run-action action + run: | + cd runtime-cancel-run-action + $HOME/.stk/bin/stk validate action + + - name: Validate runtime-create-manifest-action action + run: | + cd runtime-create-manifest-action + $HOME/.stk/bin/stk validate action + + - name: Validate runtime-deploy-action action + run: | + cd runtime-deploy-action + $HOME/.stk/bin/stk validate action + + - name: Validate runtime-destroy-action action + run: | + cd runtime-destroy-action + $HOME/.stk/bin/stk validate action + + - name: Validate runtime-iac-action action + run: | + cd runtime-iac-action + $HOME/.stk/bin/stk validate action + + - name: Validate runtime-manager-action action + run: | + cd runtime-manager-action + $HOME/.stk/bin/stk validate action + + - name: Validate runtime-rollback-action action + run: | + cd runtime-rollback-action + $HOME/.stk/bin/stk validate action + + - name: Validate runtime-matrix action + run: | + cd runtime-matrix + $HOME/.stk/bin/stk validate action + + \ No newline at end of file diff --git a/README.md b/README.md index c934a72..6d62b44 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,43 @@ -# workflow-stackspot-actions-runtime-selfhosted -workflow-stackspot-actions-runtime-selfhosted +# StackSpot actions runtimes repository + +This repository contains various StackSpot Actions that can be used to automate processes in CI/CD pipelines. Additionally, there is a main action that orchestrates the execution of the other actions, simplifying the integration and management of multiple tasks in a single workflow. + +## Repository Structure + + +## Requirements + +Before using the actions in this repository, ensure that you have the following requirements: + +* StackSpot CLI installed and configured. + +## How to Use + +### Deploy + +```bash +stk run action \ + --workflow_type deploy \ + --environment "" \ + --version_tag "" \ + --repository_name "" \ + --client_id "" \ + --client_key "" \ + --client_realm "" \ + --aws_access_key_id "" \ + --aws_secret_access_key "" \ + --aws_session_token "" \ + --tf_state_bucket_name "" \ + --iac_bucket_name "" \ + --tf_state_region "" \ + --iac_region "" \ + --aws_region "" +``` + +### Cancel + +```bash +stk run action \ + --workflow_type cancel \ + --run_id "" +``` \ No newline at end of file diff --git a/action.yaml b/action.yaml index eb93a87..878e20b 100644 --- a/action.yaml +++ b/action.yaml @@ -13,7 +13,10 @@ spec: inputs: - label: Workflow type name: workflow_type - type: text + type: select + items: + - deploy + - cancel required: true python: workdir: . diff --git a/docs/en-us/docs.md b/docs/en-us/docs.md index e69de29..4287ca8 100644 --- a/docs/en-us/docs.md +++ b/docs/en-us/docs.md @@ -0,0 +1 @@ +# \ No newline at end of file diff --git a/runtime-cancel-run-action/script.py b/runtime-cancel-run-action/script.py index 24d5c80..ba53925 100644 --- a/runtime-cancel-run-action/script.py +++ b/runtime-cancel-run-action/script.py @@ -2,7 +2,9 @@ from oscli.core.http import post_with_authorization -STK_RUNTIME_MANAGER_DOMAIN = os.getenv("STK_RUNTIME_MANAGER_DOMAIN", "https://runtime-manager.v1.stackspot.com") +STK_RUNTIME_MANAGER_DOMAIN = os.getenv( + "STK_RUNTIME_MANAGER_DOMAIN", "https://runtime-manager.v1.stackspot.com" +) def run(metadata): @@ -40,17 +42,17 @@ def run(metadata): print the error details and exit the program. """ - + # Extract the RUN_ID from the metadata inputs - RUN_ID = metadata.inputs['run_id'] + RUN_ID = metadata.inputs["run_id"] print(f"> Requesting Run {RUN_ID} to be cancelled") - + # Send a POST request to cancel the run cancel_request = post_with_authorization( - url=f"{STK_RUNTIME_MANAGER_DOMAIN}/v1/run/cancel/{RUN_ID}?force=true", - headers={'Content-Type': 'application/json'}, + url=f"{STK_RUNTIME_MANAGER_DOMAIN}/v1/run/cancel/{RUN_ID}?force=true", + headers={"Content-Type": "application/json"}, body=None, - timeout=20 + timeout=20, ) # Handle the response based on the status code @@ -66,4 +68,4 @@ def run(metadata): print("- Status:", cancel_request.status_code) print("- Error:", cancel_request.reason) print("- Response:", cancel_request.text) - exit(1) \ No newline at end of file + exit(1) diff --git a/runtime-create-manifest-action/script.py b/runtime-create-manifest-action/script.py index 26cc479..984f176 100644 --- a/runtime-create-manifest-action/script.py +++ b/runtime-create-manifest-action/script.py @@ -4,12 +4,14 @@ from typing import List, Optional # Configure logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) def check(result: subprocess.Popen) -> None: """ - Checks the result of a subprocess execution. If the return code is non-zero, + Checks the result of a subprocess execution. If the return code is non-zero, it logs an error message and exits the program. Args: @@ -35,12 +37,14 @@ def run_command(command: List[str]) -> subprocess.Popen: try: logging.info(f"Running command: {' '.join(command)}") # Start the process - process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - + 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 @@ -49,7 +53,7 @@ def run_command(command: List[str]) -> subprocess.Popen: logging.error(str(e)) sys.exit(1) - + def run(metadata) -> None: """ Executes a cmd of StackSpot CLI to deploy a plan. @@ -58,10 +62,10 @@ def run(metadata) -> None: metadata (object): An object containing the inputs required for the execution. """ stk = sys.argv[0] - environment = metadata.inputs['environment'] - version_tag = metadata.inputs['version_tag'] - open_api_path: Optional[str] = metadata.inputs.get('open_api_path') - dynamic_inputs: Optional[str] = metadata.inputs.get('dynamic_inputs') + environment = metadata.inputs["environment"] + version_tag = metadata.inputs["version_tag"] + open_api_path: Optional[str] = metadata.inputs.get("open_api_path") + dynamic_inputs: Optional[str] = metadata.inputs.get("dynamic_inputs") # Prepare optional parameters for the deploy command optional_params = [] @@ -71,7 +75,15 @@ def run(metadata) -> None: optional_params += dynamic_inputs.split() # Prepare the StackSpot CLI commands - stk_deploy_plan = [stk, "deploy", "plan", "--env", environment, "--version", version_tag] + optional_params + stk_deploy_plan = [ + stk, + "deploy", + "plan", + "--env", + environment, + "--version", + version_tag, + ] + optional_params # Execute the commands - run_command(stk_deploy_plan) \ No newline at end of file + run_command(stk_deploy_plan) diff --git a/runtime-deploy-action/docs/en-us/docs.md b/runtime-deploy-action/docs/en-us/docs.md index e69de29..792d600 100644 --- a/runtime-deploy-action/docs/en-us/docs.md +++ b/runtime-deploy-action/docs/en-us/docs.md @@ -0,0 +1 @@ +# diff --git a/runtime-deploy-action/script.py b/runtime-deploy-action/script.py index 98fe743..1288421 100644 --- a/runtime-deploy-action/script.py +++ b/runtime-deploy-action/script.py @@ -5,12 +5,18 @@ from typing import List # Configure logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +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_DEPLOY_URL = os.getenv("CONTAINER_DEPLOY_URL", "stackspot/runtime-job-deploy:latest") +STK_RUNTIME_MANAGER_DOMAIN = os.getenv( + "STK_RUNTIME_MANAGER_DOMAIN", "https://runtime-manager.v1.stackspot.com" +) +CONTAINER_DEPLOY_URL = os.getenv( + "CONTAINER_DEPLOY_URL", "stackspot/runtime-job-deploy:latest" +) FEATURES_BASEPATH_TMP = "/tmp/runtime/deploys" FEATURES_BASEPATH_EBS = "/opt/runtime" @@ -20,7 +26,7 @@ def check(result: subprocess.Popen) -> None: """ - Checks the result of a subprocess execution. If the return code is non-zero, + Checks the result of a subprocess execution. If the return code is non-zero, it logs an error message and exits the program. Args: @@ -46,12 +52,14 @@ def run_command(command: List[str]) -> subprocess.Popen: try: logging.info(f"Running command: {' '.join(command)}") # Start the process - process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - + 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 @@ -62,33 +70,31 @@ def run_command(command: List[str]) -> subprocess.Popen: 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'], + 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 + FEATURES_BASEPATH_TERRAFORM=FEATURES_BASEPATH_TERRAFORM, ) flags = [] for k, v in docker_flags.items(): flags += ["-e", f"{k}={v}"] - + return flags @@ -97,15 +103,19 @@ def run(metadata): run_task_id: str = inputs["run_task_id"] output_file: str = inputs.get("output_file") or "deploy-output.json" path_to_mount: str = inputs.get("path_to_mount") or "." - path_to_mount = f"{path_to_mount}:/app-volume" + 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-deploy", - CONTAINER_DEPLOY_URL, - "start", - f"--run-task-id={run_task_id}", - f"--output-file={output_file}", - ] + cmd = ( + ["docker", "run", "--rm", "-v", path_to_mount] + + flags + + [ + "--entrypoint=/app/stackspot-runtime-job-deploy", + CONTAINER_DEPLOY_URL, + "start", + f"--run-task-id={run_task_id}", + f"--output-file={output_file}", + ] + ) run_command(cmd) diff --git a/runtime-destroy-action/script.py b/runtime-destroy-action/script.py index 6f9d631..e82696c 100644 --- a/runtime-destroy-action/script.py +++ b/runtime-destroy-action/script.py @@ -5,12 +5,18 @@ from typing import List # Configure logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +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") +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" @@ -20,7 +26,7 @@ def check(result: subprocess.Popen) -> None: """ - Checks the result of a subprocess execution. If the return code is non-zero, + Checks the result of a subprocess execution. If the return code is non-zero, it logs an error message and exits the program. Args: @@ -46,12 +52,14 @@ def run_command(command: List[str]) -> subprocess.Popen: try: logging.info(f"Running command: {' '.join(command)}") # Start the process - process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - + 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 @@ -62,33 +70,31 @@ def run_command(command: List[str]) -> subprocess.Popen: 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'], + 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 + FEATURES_BASEPATH_TERRAFORM=FEATURES_BASEPATH_TERRAFORM, ) flags = [] for k, v in docker_flags.items(): flags += ["-e", f"{k}={v}"] - + return flags @@ -96,14 +102,18 @@ 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" + 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 + 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) diff --git a/runtime-iac-action/docs/en-us/docs.md b/runtime-iac-action/docs/en-us/docs.md index e69de29..792d600 100644 --- a/runtime-iac-action/docs/en-us/docs.md +++ b/runtime-iac-action/docs/en-us/docs.md @@ -0,0 +1 @@ +# diff --git a/runtime-iac-action/script.py b/runtime-iac-action/script.py index 5f29708..80a524c 100644 --- a/runtime-iac-action/script.py +++ b/runtime-iac-action/script.py @@ -5,17 +5,21 @@ from typing import List # Configure logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +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") +STK_RUNTIME_MANAGER_DOMAIN = os.getenv( + "STK_RUNTIME_MANAGER_DOMAIN", "https://runtime-manager.v1.stackspot.com" +) CONTAINER_IAC_URL = os.getenv("CONTAINER_IAC_URL", "stackspot/runtime-job-iac:latest") def check(result: subprocess.Popen) -> None: """ - Checks the result of a subprocess execution. If the return code is non-zero, + Checks the result of a subprocess execution. If the return code is non-zero, it logs an error message and exits the program. Args: @@ -41,12 +45,14 @@ def run_command(command: List[str]) -> subprocess.Popen: try: logging.info(f"Running command: {' '.join(command)}") # Start the process - process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - + 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 @@ -59,23 +65,21 @@ def run_command(command: List[str]) -> subprocess.Popen: def build_flags(inputs: dict) -> list: docker_flags: dict = dict( FEATURES_LEVEL_LOG=inputs.get("features_level_log") or "info", - REPOSITORY_NAME=inputs["repository_name"], AUTHENTICATE_CLIENT_ID=inputs["client_id"], AUTHENTICATE_CLIENT_SECRET=inputs["client_key"], AUTHENTICATE_CLIENT_REALMS=inputs["client_realm"], - 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'], + 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"], AWS_REGION=inputs["aws_region"], - AUTHENTICATE_URL=STK_IAM_DOMAIN, FEATURES_API_MANAGER=STK_RUNTIME_MANAGER_DOMAIN, ) flags = [] for k, v in docker_flags.items(): flags += ["-e", f"{k}={v}"] - + return flags @@ -84,15 +88,19 @@ def run(metadata): run_task_id: str = inputs["run_task_id"] base_path_output: str = inputs.get("base_path_output") or "." path_to_mount: str = inputs.get("path_to_mount") or "." - path_to_mount = f"{path_to_mount}:/app-volume" + 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-iac", - CONTAINER_IAC_URL, - "start", - f"--run-task-id={run_task_id}", - f"--base-path-output={base_path_output}", - ] + cmd = ( + ["docker", "run", "--rm", "-v", path_to_mount] + + flags + + [ + "--entrypoint=/app/stackspot-runtime-job-iac", + CONTAINER_IAC_URL, + "start", + f"--run-task-id={run_task_id}", + f"--base-path-output={base_path_output}", + ] + ) run_command(cmd) diff --git a/runtime-manager-action/script.py b/runtime-manager-action/script.py index f0f02e5..dd8df93 100644 --- a/runtime-manager-action/script.py +++ b/runtime-manager-action/script.py @@ -7,18 +7,20 @@ from oscli.core.http import post_with_authorization # Constants -STK_RUNTIME_MANAGER_DOMAIN = os.getenv("STK_RUNTIME_MANAGER_DOMAIN", "https://runtime-manager.v1.stackspot.com") +STK_RUNTIME_MANAGER_DOMAIN = os.getenv( + "STK_RUNTIME_MANAGER_DOMAIN", "https://runtime-manager.v1.stackspot.com" +) TYPE_PARSER = {"application": "app", "shared-infrastructure": "infra"} -MANIFEST_FILE = 'manifest.yaml' -OUTPUT_FILE = 'manager-output.log' -HEADERS = {'Content-Type': 'application/json'} +MANIFEST_FILE = "manifest.yaml" +OUTPUT_FILE = "manager-output.log" +HEADERS = {"Content-Type": "application/json"} TIMEOUT = 20 def yaml() -> YAML: """ Initializes and returns a YAML parser with specific configurations. - + Returns: YAML: A configured YAML parser. """ @@ -33,10 +35,10 @@ def yaml() -> YAML: def safe_load(content: str) -> dict: """ Safely loads a YAML string into a Python dictionary. - + Args: content (str): The YAML content as a string. - + Returns: dict: The parsed YAML content as a dictionary. """ @@ -47,15 +49,15 @@ def safe_load(content: str) -> dict: def get_manifest() -> dict: """ Reads and parses the 'manifest.yaml' file into a dictionary. - + Returns: dict: The parsed manifest content. - + Raises: FileNotFoundError: If the 'manifest.yaml' file is not found. """ try: - with open(Path(MANIFEST_FILE), 'r') as file: + with open(Path(MANIFEST_FILE), "r") as file: manifesto_yaml = file.read() manifest = safe_load(manifesto_yaml) return manifest @@ -70,23 +72,23 @@ def get_manifest() -> dict: def save_output(value: dict): """ Saves the provided value to the 'manager-output.log' file. - + Args: value (dict): The content to be saved in the log file. """ - with open(OUTPUT_FILE, 'w') as output_file: + with open(OUTPUT_FILE, "w") as output_file: json.dump(value, output_file, indent=4) print(f"> Output saved to {OUTPUT_FILE}") - + def build_request(action_inputs: dict, app_infra_manifest: dict) -> dict: """ Builds the request data for the self-hosted deployment by merging the action inputs and manifest data. - + Args: action_inputs (dict): The inputs provided for the action (e.g., bucket names, regions). app_infra_manifest (dict): The parsed manifest data. - + Returns: dict: The complete request data for the deployment. """ @@ -95,18 +97,20 @@ def build_request(action_inputs: dict, app_infra_manifest: dict) -> dict: "config": { "tfstate": { "bucket": action_inputs["tf_state_bucket_name"], - "region": action_inputs["tf_state_region"] + "region": action_inputs["tf_state_region"], }, "iac": { "bucket": action_inputs["iac_bucket_name"], - "region": action_inputs["iac_region"] - } + "region": action_inputs["iac_region"], + }, }, "pipelineUrl": "http://stackspot.com", } # Merge the manifest data with the configuration data request_data = {**app_infra_manifest, **config_data} - print(f"> Runtime manager run self hosted deploy request data:\n{json.dumps(request_data, indent=4)}") + print( + f"> Runtime manager run self hosted deploy request data:\n{json.dumps(request_data, indent=4)}" + ) return request_data except KeyError as e: print(f"> Error: Missing required input {e}") @@ -116,29 +120,33 @@ def build_request(action_inputs: dict, app_infra_manifest: dict) -> dict: def runtime_manager_run_self_hosted_deploy(request_data: dict, manifest: dict): """ Sends a request to the StackSpot Runtime Manager to start a self-hosted deployment. - + Args: request_data (dict): The request data for the deployment. manifest (dict): The parsed manifest data. - + Raises: SystemExit: If the deployment request fails. """ manifest_type = manifest["manifesto"]["kind"] url = f"{STK_RUNTIME_MANAGER_DOMAIN}/v1/run/self-hosted/deploy/{TYPE_PARSER[manifest_type]}" - + print("> Calling runtime manager to define tasks...") - response = post_with_authorization(url=url, body=request_data, headers=HEADERS, timeout=TIMEOUT) + response = post_with_authorization( + url=url, body=request_data, headers=HEADERS, timeout=TIMEOUT + ) if response.ok: # Parse the response and extract relevant data response_data = response.json() print(f"> Deploy successfully started:\n{json.dumps(response_data, indent=4)}") - + # Save the response to the output log save_output(response_data) else: - print(f"> Error: Failed to start self-hosted deploy run. Status: {response.status_code}") + print( + f"> Error: Failed to start self-hosted deploy run. Status: {response.status_code}" + ) print(f"> Response: {response.text}") exit(1) @@ -146,9 +154,9 @@ def runtime_manager_run_self_hosted_deploy(request_data: dict, manifest: dict): def run(metadata): # Load the manifest file manifest = get_manifest() - + # Build the request data request = build_request(metadata.inputs, manifest) - + # Execute the deployment request - runtime_manager_run_self_hosted_deploy(request, manifest) \ No newline at end of file + runtime_manager_run_self_hosted_deploy(request, manifest) diff --git a/runtime-matrix/action.yaml b/runtime-matrix/action.yaml new file mode 100644 index 0000000..5842355 --- /dev/null +++ b/runtime-matrix/action.yaml @@ -0,0 +1,98 @@ +schema-version: v3 +kind: action +metadata: + name: runtime-iac-action + display-name: runtime-iac-action + description: runtime-iac-action + version: 1.0.0 +spec: + type: python + docs: + en-us: docs/en-us/docs.md + repository: https://github.com/stack-spot/workflow-stackspot-actions-runtime-selfhosted.git + inputs: + + - label: "CLIENT ID" + name: client_id + type: text + required: true + + - label: "CLIENT KEY" + name: client_key + type: text + required: true + + - label: "CLIENT REALM" + name: client_realm + type: text + required: true + + - label: "Git Repository Name" + name: repository_name + type: text + required: true + + - label: "AWS ACCESS KEY ID from console" + name: aws_access_key_id + type: text + required: true + + - label: "AWS SECRET ACCESS KEY from console" + name: aws_secret_access_key + type: text + required: true + + - label: "AWS SESSION TOKEN from console" + name: aws_session_token + type: text + required: true + + - label: "AWS REGION" + name: aws_region + type: text + required: true + + - label: "Runtime Run Task Id" + name: run_task_id + type: text + required: true + + - label: "Path to mount inside the docker" + name: path_to_mount + type: text + required: false + + - label: "Base Path Output" + name: base_path_output + type: text + required: false + + - label: "Features Log Level" + name: features_level_log + type: text + required: false + + - label: "Features Terraform Modules" + name: features_terraform_modules + type: text + required: false + + - label: "File name to save outputs" + name: output_file + type: text + required: false + + - label: "If Runtimes will allow execution of the local-exec command within terraform" + name: localexec_enabled + type: bool + required: false + + - label: "Level tf log provider - info, debug, warn or trace" + name: tf_log_provider + type: text + required: false + + python: + workdir: . + script: script.py + \ No newline at end of file diff --git a/runtime-matrix/docs/en-us/docs.md b/runtime-matrix/docs/en-us/docs.md new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/runtime-matrix/docs/en-us/docs.md @@ -0,0 +1 @@ +# diff --git a/runtime-matrix/script.py b/runtime-matrix/script.py new file mode 100644 index 0000000..eeca6e4 --- /dev/null +++ b/runtime-matrix/script.py @@ -0,0 +1,175 @@ +import os +import sys +import subprocess +import logging +import json +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_IAC_URL = os.getenv("CONTAINER_IAC_URL", "stackspot/runtime-job-iac:latest") +CONTAINER_DEPLOY_URL = os.getenv( + "CONTAINER_DEPLOY_URL", "stackspot/runtime-job-deploy: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_iac_flags(inputs: dict) -> list: + docker_flags: dict = dict( + FEATURES_LEVEL_LOG=inputs.get("features_level_log") or "info", + REPOSITORY_NAME=inputs["repository_name"], + AUTHENTICATE_CLIENT_ID=inputs["client_id"], + AUTHENTICATE_CLIENT_SECRET=inputs["client_key"], + AUTHENTICATE_CLIENT_REALMS=inputs["client_realm"], + 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"], + AWS_REGION=inputs["aws_region"], + AUTHENTICATE_URL=STK_IAM_DOMAIN, + FEATURES_API_MANAGER=STK_RUNTIME_MANAGER_DOMAIN, + ) + flags = [] + for k, v in docker_flags.items(): + flags += ["-e", f"{k}={v}"] + + return flags + + +def run_iac(inputs: dict): + run_task_id: str = inputs["run_task_id"] + base_path_output: str = inputs.get("base_path_output") or "." + path_to_mount: str = inputs.get("path_to_mount") or "." + path_to_mount = f"{path_to_mount}:/app-volume" + + flags = build_iac_flags(inputs) + cmd = ( + ["docker", "run", "--rm", "-v", path_to_mount] + + flags + + [ + "--entrypoint=/app/stackspot-runtime-job-iac", + CONTAINER_IAC_URL, + "start", + f"--run-task-id={run_task_id}", + f"--base-path-output={base_path_output}", + ] + ) + + run_command(cmd) + + +def build_deploy_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_deploy(inputs: dict): + run_task_id: str = inputs["run_task_id"] + output_file: str = inputs.get("output_file") or "deploy-output.json" + path_to_mount: str = inputs.get("path_to_mount") or "." + path_to_mount = f"{path_to_mount}:/app-volume" + + flags = build_deploy_flags(inputs) + cmd = ( + ["docker", "run", "--rm", "-v", path_to_mount] + + flags + + [ + "--entrypoint=/app/stackspot-runtime-job-deploy", + CONTAINER_DEPLOY_URL, + "start", + f"--run-task-id={run_task_id}", + f"--output-file={output_file}", + ] + ) + + run_command(cmd) + + +def run(metadata): + with open("manager-output.log", "r") as file: + data = json.loads(file.read().replace("'", '"')) + + task_runners = dict(IAC_SELF_HOSTED=run_iac, DEPLOY_SELF_HOSTED=run_deploy) + for t in data["tasks"]: + runner = task_runners.get(t["taskType"]) + runner and runner(run_task_id=t["runTaskId"], **metadata.inputs) diff --git a/runtime-rollback-action/action.yaml b/runtime-rollback-action/action.yaml index 98aa4b9..eb8bc83 100644 --- a/runtime-rollback-action/action.yaml +++ b/runtime-rollback-action/action.yaml @@ -8,7 +8,6 @@ metadata: spec: 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: diff --git a/runtime-rollback-action/script.py b/runtime-rollback-action/script.py index 19a6534..b92ef0e 100644 --- a/runtime-rollback-action/script.py +++ b/runtime-rollback-action/script.py @@ -9,11 +9,15 @@ 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'} +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 @@ -25,6 +29,7 @@ def yaml() -> YAML: yml.preserve_quotes = True return yml + def safe_load(content: str) -> dict: yml = yaml() return yml.load(StringIO(content)) @@ -32,7 +37,7 @@ def safe_load(content: str) -> dict: def get_stk_yaml() -> dict: try: - with open(Path(STK_FILE), 'r') as file: + with open(Path(STK_FILE), "r") as file: stk_yaml = file.read() stk_yaml = safe_load(stk_yaml) return stk_yaml @@ -46,82 +51,96 @@ def get_stk_yaml() -> dict: def save_output(value: dict): - with open(OUTPUT_FILE, 'w') as output_file: + with open(OUTPUT_FILE, "w") as output_file: json.dump(value, output_file, indent=4) print(f"> Output saved to {OUTPUT_FILE}") - + def build_request(action_inputs: dict, env_id: str, stk_yaml: dict) -> dict: 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")} - } - + "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'], + 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"] + "region": action_inputs["tf_state_region"], }, "iac": { "bucket": action_inputs["iac_bucket_name"], - "region": action_inputs["iac_region"] - } + "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)}") + + 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) + def get_type(stk_yaml: dict) -> str: return stk_yaml["spec"]["type"] -def runtime_manager_run_self_hosted_rollback(request_data: dict, stk_yaml: dict, version_tag: str): + +def runtime_manager_run_self_hosted_rollback( + request_data: dict, stk_yaml: dict, version_tag: str +): 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) + response = post_with_authorization( + url=url, body=request_data, headers=HEADERS, timeout=TIMEOUT + ) if response.ok: - if(response.status_code == 201): + if response.status_code == 201: response_data = response.json() - print(f"> Rollback successfully started:\n{json.dumps(response_data, indent=4)}") + 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}") + 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"> Error: Failed to start self-hosted rollback run. Status: {response.status_code}" + ) print(f"> Response: {response.text}") exit(1) def get_environment_id(environment_slug): - + url = f"{STK_WORKSPACE_DOMAIN}/v1/environments" print("> Calling workspace to load environments...") - response = get_with_authorization(url=url,headers=HEADERS,timeout=TIMEOUT) - - if response.ok: + 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: @@ -131,7 +150,7 @@ def get_environment_id(environment_slug): print(f"> Error: Failed to load environments. Status: {response.status_code}") print(f"> Response: {response.text}") exit(1) - + def run(metadata): # Load the manifest file @@ -139,9 +158,11 @@ def run(metadata): # 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 + runtime_manager_run_self_hosted_rollback( + request, stk_yaml, metadata.inputs["version_tag"] + ) diff --git a/script.py b/script.py index 5b6e13b..fa70af6 100644 --- a/script.py +++ b/script.py @@ -13,11 +13,19 @@ def __init__(self, metadata): self.inputs = {k: v for k, v in metadata.inputs.items() if v is not None} def __call__(self, action_name: str, **inputs): - action_path = f'{self.action_base_path}/{action_name}' - cmd = [self.stk, 'run', 'action', action_path, '--inputs-json', json.dumps({**self.inputs, **inputs}),'--non-interactive'] + action_path = f"{self.action_base_path}/{action_name}" + cmd = [ + self.stk, + "run", + "action", + action_path, + "--inputs-json", + json.dumps({**self.inputs, **inputs}), + "--non-interactive", + ] result = subprocess.run(cmd) - if (result.returncode == 1): - print(f"Fail to execute {result.args}...") + if result.returncode == 1: + print(f"Fail to execute {result.args}...") exit(1) @@ -27,31 +35,37 @@ def deploy_workflow(run_action: RunAction): 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("\'", "\"")) - + 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.get('tasks') or []: + + for t in data.get("tasks") or []: runner = task_runners.get(t["taskType"]) runner and runner(run_task_id=t["runTaskId"]) + def run(metadata): - workflows = dict(deploy=deploy_workflow, cancel_deploy=cancel_deploy_run, rollback_deploy=rollback_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) - \ No newline at end of file