diff --git a/README.md b/README.md index 7cd4f0d2..f4e6cd93 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,7 @@ Optionally, you need the following permissions to attach Access Management tags | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.9.0 | +| [external](#requirement\_external) | >=2.3.5, <3.0.0 | | [ibm](#requirement\_ibm) | >= 1.78.2, < 2.0.0 | | [kubernetes](#requirement\_kubernetes) | >= 2.16.1, < 3.0.0 | | [null](#requirement\_null) | >= 3.2.1, < 4.0.0 | @@ -327,9 +328,11 @@ Optionally, you need the following permissions to attach Access Management tags | [null_resource.install_required_binaries](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | | [null_resource.ocp_console_management](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | | [time_sleep.wait_for_auth_policy](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/sleep) | resource | +| [external_external.ocp_addon_versions](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external) | data source | | [ibm_container_addons.existing_addons](https://registry.terraform.io/providers/ibm-cloud/ibm/latest/docs/data-sources/container_addons) | data source | | [ibm_container_cluster_config.cluster_config](https://registry.terraform.io/providers/ibm-cloud/ibm/latest/docs/data-sources/container_cluster_config) | data source | | [ibm_container_cluster_versions.cluster_versions](https://registry.terraform.io/providers/ibm-cloud/ibm/latest/docs/data-sources/container_cluster_versions) | data source | +| [ibm_iam_auth_token.tokendata](https://registry.terraform.io/providers/ibm-cloud/ibm/latest/docs/data-sources/iam_auth_token) | data source | | [ibm_is_lbs.all_lbs](https://registry.terraform.io/providers/ibm-cloud/ibm/latest/docs/data-sources/is_lbs) | data source | | [ibm_is_virtual_endpoint_gateway.api_vpe](https://registry.terraform.io/providers/ibm-cloud/ibm/latest/docs/data-sources/is_virtual_endpoint_gateway) | data source | | [ibm_is_virtual_endpoint_gateway.master_vpe](https://registry.terraform.io/providers/ibm-cloud/ibm/latest/docs/data-sources/is_virtual_endpoint_gateway) | data source | diff --git a/main.tf b/main.tf index 7d8ee7a7..6b82ac37 100644 --- a/main.tf +++ b/main.tf @@ -53,6 +53,20 @@ locals { binaries_path = "/tmp" } +######################################################################################################################## +# Get OCP AI Add-on Versions +######################################################################################################################## + +data "ibm_iam_auth_token" "tokendata" {} + +data "external" "ocp_addon_versions" { + program = ["python3", "${path.module}/scripts/get_ocp_addon_versions.py"] + query = { + IAM_TOKEN = sensitive(data.ibm_iam_auth_token.tokendata.iam_access_token) + REGION = var.region + } +} + # Local block to verify validations for OCP AI Addon. locals { @@ -65,6 +79,7 @@ locals { is_gpu = contains(["gx2", "gx3", "gx4"], split(".", pool.machine_type)[0]) } } + ocp_ai_addon_supported_versions = jsondecode(data.external.ocp_addon_versions.result["openshift-ai"]) } # Separate local block to handle os validations diff --git a/scripts/get_ocp_addon_versions.py b/scripts/get_ocp_addon_versions.py new file mode 100644 index 00000000..88e1d9f1 --- /dev/null +++ b/scripts/get_ocp_addon_versions.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +import http.client +import json +import os +import sys +from urllib.parse import urlparse + + +def parse_input(): + """ + Reads JSON input from stdin and parses it into a dictionary. + Returns: + dict: Parsed input data. + """ + try: + data = json.loads(sys.stdin.read()) + except json.JSONDecodeError as e: + raise ValueError("Invalid JSON input") from e + return data + + +def validate_inputs(data): + """ + Validates required inputs 'IAM_TOKEN' and 'REGION' from the parsed input. + Args: + data (dict): Input data parsed from JSON. + Returns: + tuple: A tuple containing (IAM_TOKEN, REGION). + """ + token = data.get("IAM_TOKEN") + if not token: + raise ValueError("IAM_TOKEN is required") + + region = data.get("REGION") + if not region: + raise ValueError("REGION is required") + + return token, region + + +def get_env_variable(): + """ + Retrieves the value of an environment variable. + Returns: + str: The value of the environment variable. + """ + api_endpoint = os.getenv( + "IBMCLOUD_CS_API_ENDPOINT", "https://containers.test.cloud.ibm.com/global" + ) + return api_endpoint + + +def fetch_addon_versions(iam_token, region, api_endpoint): + """ + Fetches openshift add-on versions using HTTP connection. + Args: + iam_token (str): IBM Cloud IAM token for authentication. + region (str): Region to query for add-ons. + api_endpoint (str): Base API endpoint URL. + Returns: + list: Parsed JSON response containing add-on information. + """ + # Add https if user passed just a hostname + if not api_endpoint.startswith("https://"): + api_endpoint = f"https://{api_endpoint}" + + parsed = urlparse(api_endpoint) + + # Default path to /global if none supplied + base_path = parsed.path.rstrip("/") if parsed.path else "/global" + + # Final API path + url = f"{base_path}/v1/addons" + host = parsed.hostname + headers = { + "Authorization": f"Bearer {iam_token}", + "Accept": "application/json", + "X-Region": region, + } + + conn = http.client.HTTPSConnection(host) + try: + conn.request("GET", url, headers=headers) + response = conn.getresponse() + data = response.read().decode() + + if response.status != 200: + raise RuntimeError( + f"API request failed: {response.status} {response.reason} - {data}" + ) + + return json.loads(data) + except http.client.HTTPException as e: + raise RuntimeError("HTTP request failed") from e + finally: + conn.close() + + +def transform_addons(addons_data): + """ + Transforms the raw add-on data into a nested dictionary structured by add-on name and version. + Args: + addons_data: Raw data returned by the add-on API. + Returns: + dict: Transformed add-on data suitable for Terraform consumption. + """ + result = {} + + for addon in addons_data: + name = addon.get("name") + version = addon.get("version") + + supported_ocp = addon.get("supportedOCPRange", "unsupported") + supported_kube = addon.get("supportedKubeRange", "unsupported") + + if name not in result: + result[name] = {} + + result[name][version] = { + "supported_openshift_range": supported_ocp, + "supported_kubernetes_range": supported_kube, + } + + if not result: + raise RuntimeError("No add-on data found.") + + return result + + +def format_for_terraform(result): + """ + Converts the transformed add-on data into JSON strings for Terraform external data source consumption. + Args: + result (dict): Transformed add-on data. + Returns: + dict: A dictionary mapping add-on names to JSON strings of their version info. + """ + return {name: json.dumps(versions) for name, versions in result.items()} + + +def main(): + """ + Main execution function: reads input, validates, fetches API data, transforms it, + formats it for Terraform and prints the JSON output. + """ + data = parse_input() + iam_token, region = validate_inputs(data) + api_endpoint = get_env_variable() + api_endpoint = get_env_variable() + addons_data = fetch_addon_versions(iam_token, region, api_endpoint) + transformed = transform_addons(addons_data) + output = format_for_terraform(transformed) + + print(json.dumps(output)) + + +if __name__ == "__main__": + main() diff --git a/variables.tf b/variables.tf index 6aaea3c6..27813ddc 100644 --- a/variables.tf +++ b/variables.tf @@ -364,9 +364,30 @@ variable "addons" { nullable = false default = {} + ######################################################################################################################## + # OCP AI Addon version validation + ######################################################################################################################## + validation { - condition = (lookup(var.addons, "openshift-ai", null) != null ? lookup(var.addons["openshift-ai"], "version", null) == null : true) || (tonumber(local.ocp_version_num) >= 4.16) - error_message = "OCP AI add-on requires OCP version >= 4.16.0" + condition = ( + lookup(var.addons, "openshift-ai", null) == null || + lookup(var.addons["openshift-ai"], "version", null) == null || + ( + contains(keys(local.ocp_ai_addon_supported_versions), var.addons["openshift-ai"].version) && + ( + local.ocp_version_num >= tonumber(regexall("\\d+\\.\\d+", split(" ", local.ocp_ai_addon_supported_versions[var.addons["openshift-ai"].version].supported_openshift_range)[0])[0])) && + ( + local.ocp_version_num < tonumber(regexall("\\d+\\.\\d+", split(" ", local.ocp_ai_addon_supported_versions[var.addons["openshift-ai"].version].supported_openshift_range)[1])[0]) + ) + ) + ) + + error_message = ( + var.addons["openshift-ai"] != null && var.addons["openshift-ai"].version != null ? + (contains(keys(local.ocp_ai_addon_supported_versions), var.addons["openshift-ai"].version) ? + format("OCP AI add-on version %s requires OCP version %s", var.addons["openshift-ai"].version, local.ocp_ai_addon_supported_versions[var.addons["openshift-ai"].version].supported_openshift_range) : + format("OCP AI add-on version %s is not supported.", var.addons["openshift-ai"].version)) : null + ) } validation { diff --git a/version.tf b/version.tf index fac8de8b..345a8aae 100644 --- a/version.tf +++ b/version.tf @@ -18,5 +18,9 @@ terraform { source = "hashicorp/time" version = ">= 0.9.1, < 1.0.0" } + external = { + source = "hashicorp/external" + version = ">=2.3.5, <3.0.0" + } } }