Skip to content

Commit c2be813

Browse files
authored
feat: added validation for the openshift-ai cluster addon (#884)
1 parent 928f001 commit c2be813

File tree

6 files changed

+205
-5
lines changed

6 files changed

+205
-5
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ Optionally, you need the following permissions to attach Access Management tags
291291
| Name | Version |
292292
|------|---------|
293293
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.9.0 |
294+
| <a name="requirement_external"></a> [external](#requirement\_external) | >=2.3.5, <3.0.0 |
294295
| <a name="requirement_ibm"></a> [ibm](#requirement\_ibm) | >= 1.78.2, < 2.0.0 |
295296
| <a name="requirement_kubernetes"></a> [kubernetes](#requirement\_kubernetes) | >= 3.0.0, < 4.0.0 |
296297
| <a name="requirement_null"></a> [null](#requirement\_null) | >= 3.2.1, < 4.0.0 |
@@ -328,9 +329,11 @@ Optionally, you need the following permissions to attach Access Management tags
328329
| [null_resource.install_required_binaries](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource |
329330
| [null_resource.ocp_console_management](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource |
330331
| [time_sleep.wait_for_auth_policy](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/sleep) | resource |
332+
| [external_external.ocp_addon_versions](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external) | data source |
331333
| [ibm_container_addons.existing_addons](https://registry.terraform.io/providers/ibm-cloud/ibm/latest/docs/data-sources/container_addons) | data source |
332334
| [ibm_container_cluster_config.cluster_config](https://registry.terraform.io/providers/ibm-cloud/ibm/latest/docs/data-sources/container_cluster_config) | data source |
333335
| [ibm_container_cluster_versions.cluster_versions](https://registry.terraform.io/providers/ibm-cloud/ibm/latest/docs/data-sources/container_cluster_versions) | data source |
336+
| [ibm_iam_auth_token.tokendata](https://registry.terraform.io/providers/ibm-cloud/ibm/latest/docs/data-sources/iam_auth_token) | data source |
334337
| [ibm_is_lbs.all_lbs](https://registry.terraform.io/providers/ibm-cloud/ibm/latest/docs/data-sources/is_lbs) | data source |
335338
| [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 |
336339
| [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 |

main.tf

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,20 @@ locals {
5353
binaries_path = "/tmp"
5454
}
5555

56+
########################################################################################################################
57+
# Get OCP AI Add-on Versions
58+
########################################################################################################################
59+
60+
data "ibm_iam_auth_token" "tokendata" {}
61+
62+
data "external" "ocp_addon_versions" {
63+
program = ["python3", "${path.module}/scripts/get_ocp_addon_versions.py"]
64+
query = {
65+
IAM_TOKEN = sensitive(data.ibm_iam_auth_token.tokendata.iam_access_token)
66+
REGION = var.region
67+
}
68+
}
69+
5670
# Local block to verify validations for OCP AI Addon.
5771
locals {
5872

@@ -65,6 +79,7 @@ locals {
6579
is_gpu = contains(["gx2", "gx3", "gx4"], split(".", pool.machine_type)[0])
6680
}
6781
}
82+
ocp_ai_addon_supported_versions = jsondecode(data.external.ocp_addon_versions.result["openshift-ai"])
6883
}
6984

7085
# Separate local block to handle os validations

scripts/get_ocp_addon_versions.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
#!/usr/bin/env python3
2+
import http.client
3+
import json
4+
import os
5+
import sys
6+
from urllib.parse import urlparse
7+
8+
9+
def parse_input():
10+
"""
11+
Reads JSON input from stdin and parses it into a dictionary.
12+
Returns:
13+
dict: Parsed input data.
14+
"""
15+
try:
16+
data = json.loads(sys.stdin.read())
17+
except json.JSONDecodeError as e:
18+
raise ValueError("Invalid JSON input") from e
19+
return data
20+
21+
22+
def validate_inputs(data):
23+
"""
24+
Validates required inputs 'IAM_TOKEN' and 'REGION' from the parsed input.
25+
Args:
26+
data (dict): Input data parsed from JSON.
27+
Returns:
28+
tuple: A tuple containing (IAM_TOKEN, REGION).
29+
"""
30+
token = data.get("IAM_TOKEN")
31+
if not token:
32+
raise ValueError("IAM_TOKEN is required")
33+
34+
region = data.get("REGION")
35+
if not region:
36+
raise ValueError("REGION is required")
37+
38+
return token, region
39+
40+
41+
def get_env_variable():
42+
"""
43+
Retrieves the value of an environment variable.
44+
Returns:
45+
str: The value of the environment variable.
46+
"""
47+
api_endpoint = os.getenv("IBMCLOUD_CS_API_ENDPOINT")
48+
if not api_endpoint:
49+
api_endpoint = "https://containers.test.cloud.ibm.com/global"
50+
return api_endpoint
51+
52+
53+
def fetch_addon_versions(iam_token, region, api_endpoint):
54+
"""
55+
Fetches openshift add-on versions using HTTP connection.
56+
Args:
57+
iam_token (str): IBM Cloud IAM token for authentication.
58+
region (str): Region to query for add-ons.
59+
api_endpoint (str): Base API endpoint URL.
60+
Returns:
61+
list: Parsed JSON response containing add-on information.
62+
"""
63+
# Add https if user passed just a hostname
64+
if not api_endpoint.startswith("https://"):
65+
api_endpoint = f"https://{api_endpoint}"
66+
67+
parsed = urlparse(api_endpoint)
68+
69+
# Default path to /global if none supplied
70+
base_path = parsed.path.rstrip("/") if parsed.path else "/global"
71+
72+
host = parsed.hostname
73+
headers = {
74+
"Authorization": f"Bearer {iam_token}",
75+
"Accept": "application/json",
76+
"X-Region": region,
77+
}
78+
79+
conn = http.client.HTTPSConnection(host)
80+
try:
81+
# Final API path
82+
url = f"{base_path}/v1/addons"
83+
conn.request("GET", url, headers=headers)
84+
response = conn.getresponse()
85+
data = response.read().decode()
86+
87+
if response.status != 200:
88+
raise RuntimeError(
89+
f"API request failed: {response.status} {response.reason} - {data}"
90+
)
91+
92+
return json.loads(data)
93+
except http.client.HTTPException as e:
94+
raise RuntimeError("HTTP request failed") from e
95+
finally:
96+
conn.close()
97+
98+
99+
def transform_cluster_addons_data(addons_data):
100+
"""
101+
Transforms cluster add-on raw data into a nested dictionary structured by add-on name and version.
102+
Args:
103+
addons_data: Raw data returned by the add-on API.
104+
Returns:
105+
dict: Transformed add-on data suitable for Terraform consumption.
106+
"""
107+
result = {}
108+
109+
for addon in addons_data:
110+
name = addon.get("name")
111+
version = addon.get("version")
112+
113+
supported_ocp = addon.get("supportedOCPRange", "unsupported")
114+
supported_kube = addon.get("supportedKubeRange", "unsupported")
115+
116+
if name not in result:
117+
result[name] = {}
118+
119+
result[name][version] = {
120+
"supported_openshift_range": supported_ocp,
121+
"supported_kubernetes_range": supported_kube,
122+
}
123+
124+
if not result:
125+
raise RuntimeError("No add-on data found.")
126+
127+
return result
128+
129+
130+
def format_for_terraform(result):
131+
"""
132+
Converts the transformed add-on data into JSON strings for Terraform external data source consumption.
133+
Args:
134+
result (dict): Transformed add-on data.
135+
Returns:
136+
dict: A dictionary mapping add-on names to JSON strings of their version info.
137+
"""
138+
return {name: json.dumps(versions) for name, versions in result.items()}
139+
140+
141+
def main():
142+
"""
143+
Main execution function: reads input, validates, fetches API data, transforms it,
144+
formats it for Terraform and prints the JSON output.
145+
"""
146+
data = parse_input()
147+
iam_token, region = validate_inputs(data)
148+
api_endpoint = get_env_variable()
149+
addons_data = fetch_addon_versions(iam_token, region, api_endpoint)
150+
transformed = transform_cluster_addons_data(addons_data)
151+
output = format_for_terraform(transformed)
152+
153+
print(json.dumps(output))
154+
155+
156+
if __name__ == "__main__":
157+
main()

tests/pr_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ func setupQuickstartOptions(t *testing.T, prefix string) *testschematic.TestSche
101101
Region: region,
102102
TarIncludePatterns: []string{
103103
"*.tf",
104-
quickStartTerraformDir + "/*.tf", "scripts/*.sh", "kubeconfig/README.md",
104+
quickStartTerraformDir + "/*.tf", "scripts/*.*", "kubeconfig/README.md",
105105
"modules/worker-pool/*.tf",
106106
"modules/kube-audit/scripts/*.sh",
107107
},
@@ -162,7 +162,7 @@ func TestRunFullyConfigurableInSchematics(t *testing.T) {
162162
options := testschematic.TestSchematicOptionsDefault(&testschematic.TestSchematicOptions{
163163
Testing: t,
164164
Prefix: "ocp-fc",
165-
TarIncludePatterns: []string{"*.tf", fullyConfigurableTerraformDir + "/*.*", fullyConfigurableTerraformDir + "/scripts/*.*", "scripts/*.sh", "kubeconfig/README.md", "modules/kube-audit/*.*", "modules/worker-pool/*.tf", "modules/kube-audit/kubeconfig/README.md", "modules/kube-audit/scripts/*.sh", fullyConfigurableTerraformDir + "/kubeconfig/README.md", "modules/kube-audit/helm-charts/kube-audit/*.*", "modules/kube-audit/helm-charts/kube-audit/templates/*.*"},
165+
TarIncludePatterns: []string{"*.tf", fullyConfigurableTerraformDir + "/*.*", fullyConfigurableTerraformDir + "/scripts/*.*", "scripts/*.*", "kubeconfig/README.md", "modules/kube-audit/*.*", "modules/worker-pool/*.tf", "modules/kube-audit/kubeconfig/README.md", "modules/kube-audit/scripts/*.sh", fullyConfigurableTerraformDir + "/kubeconfig/README.md", "modules/kube-audit/helm-charts/kube-audit/*.*", "modules/kube-audit/helm-charts/kube-audit/templates/*.*"},
166166
TemplateFolder: fullyConfigurableTerraformDir,
167167
Tags: []string{"test-schematic"},
168168
DeleteWorkspaceOnFail: false,
@@ -203,7 +203,7 @@ func TestRunUpgradeFullyConfigurable(t *testing.T) {
203203
options := testschematic.TestSchematicOptionsDefault(&testschematic.TestSchematicOptions{
204204
Testing: t,
205205
Prefix: "fc-upg",
206-
TarIncludePatterns: []string{"*.tf", fullyConfigurableTerraformDir + "/*.*", fullyConfigurableTerraformDir + "/scripts/*.*", "scripts/*.sh", "kubeconfig/README.md", "modules/kube-audit/*.*", "modules/kube-audit/kubeconfig/README.md", "modules/kube-audit/scripts/*.sh", fullyConfigurableTerraformDir + "/kubeconfig/README.md", "modules/kube-audit/helm-charts/kube-audit/*.*", "modules/kube-audit/helm-charts/kube-audit/templates/*.*", "modules/worker-pool/*.tf"},
206+
TarIncludePatterns: []string{"*.tf", fullyConfigurableTerraformDir + "/*.*", fullyConfigurableTerraformDir + "/scripts/*.*", "scripts/*.*", "kubeconfig/README.md", "modules/kube-audit/*.*", "modules/kube-audit/kubeconfig/README.md", "modules/kube-audit/scripts/*.sh", fullyConfigurableTerraformDir + "/kubeconfig/README.md", "modules/kube-audit/helm-charts/kube-audit/*.*", "modules/kube-audit/helm-charts/kube-audit/templates/*.*", "modules/worker-pool/*.tf"},
207207
TemplateFolder: fullyConfigurableTerraformDir,
208208
Tags: []string{"test-schematic"},
209209
DeleteWorkspaceOnFail: false,

variables.tf

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,9 +364,30 @@ variable "addons" {
364364
nullable = false
365365
default = {}
366366

367+
########################################################################################################################
368+
# OCP AI Addon version validation
369+
########################################################################################################################
370+
367371
validation {
368-
condition = (lookup(var.addons, "openshift-ai", null) != null ? lookup(var.addons["openshift-ai"], "version", null) == null : true) || (tonumber(local.ocp_version_num) >= 4.16)
369-
error_message = "OCP AI add-on requires OCP version >= 4.16.0"
372+
condition = (
373+
try(var.addons["openshift-ai"].version, null) == null ||
374+
(
375+
contains(keys(local.ocp_ai_addon_supported_versions), try(var.addons["openshift-ai"].version, "") != null ? try(var.addons["openshift-ai"].version, "") : "") &&
376+
(
377+
local.ocp_version_num >= tonumber(regexall("\\d+\\.\\d+", split(" ", lookup(local.ocp_ai_addon_supported_versions, try(var.addons["openshift-ai"].version, "") != null ? try(var.addons["openshift-ai"].version, "") : "", { supported_openshift_range = "0.0 0.0" }).supported_openshift_range)[0])[0])
378+
) &&
379+
(
380+
local.ocp_version_num < tonumber(regexall("\\d+\\.\\d+", split(" ", lookup(local.ocp_ai_addon_supported_versions, try(var.addons["openshift-ai"].version, "") != null ? try(var.addons["openshift-ai"].version, "") : "", { supported_openshift_range = "0.0 0.0" }).supported_openshift_range)[1])[0])
381+
)
382+
)
383+
)
384+
385+
error_message = (
386+
try(var.addons["openshift-ai"].version, null) != null ?
387+
(contains(keys(local.ocp_ai_addon_supported_versions), try(var.addons["openshift-ai"].version, "")) ?
388+
format("OCP AI add-on version %s requires OCP version %s", try(var.addons["openshift-ai"].version, ""), lookup(local.ocp_ai_addon_supported_versions, try(var.addons["openshift-ai"].version, ""), { supported_openshift_range = "" }).supported_openshift_range) :
389+
format("OCP AI add-on version %s is not supported.", try(var.addons["openshift-ai"].version, ""))) : "Invalid OCP AI configuration."
390+
)
370391
}
371392

372393
validation {

version.tf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,9 @@ terraform {
1818
source = "hashicorp/time"
1919
version = ">= 0.9.1, < 1.0.0"
2020
}
21+
external = {
22+
source = "hashicorp/external"
23+
version = ">=2.3.5, <3.0.0"
24+
}
2125
}
2226
}

0 commit comments

Comments
 (0)