diff --git a/modules/ske/ske-starterkit/e2e/main.tf b/modules/ske/ske-starterkit/e2e/main.tf index 60db081f..bf1dda04 100644 --- a/modules/ske/ske-starterkit/e2e/main.tf +++ b/modules/ske/ske-starterkit/e2e/main.tf @@ -5,6 +5,7 @@ variable "test_context" { name_suffix = string forgejo_base_url = string forgejo_organization = string + dns_zone_name = string }) nullable = false } @@ -109,6 +110,19 @@ module "forgejo_connector" { forgejo_repo_definition_uuid = module.stackit_git_repository.building_block_definition.uuid harbor_username = var.harbor_push_username harbor_password = var.harbor_push_password + + # Smoke tests don't exercise real inference — the app only needs the `stackit-ai` + # secret to exist so its pods can start (the app chart mounts it via `envFrom`, so a + # missing secret leaves pods in CreateContainerConfigError and `helm --wait --atomic` + # rolls the deploy back). Static foundations (e.g. trial) inject a real STACKIT + # model-serving token here via their own `ai.tf`; the smoke test uses dummy values. + additional_kubernetes_secrets = { + "stackit-ai" = { + STACKIT_AI_BASE_URL = "https://ai.invalid/v1" + STACKIT_AI_API_KEY = "dummy-smoke-test" + STACKIT_AI_MODEL = "dummy-model" + } + } } module "ske_starterkit" { @@ -128,7 +142,7 @@ module "ske_starterkit" { prod = module.meshstack_kubernetes_platform.landing_zone_identifiers.prod } repo_clone_addr = "https://github.com/likvid-bank/starterkit-template-stackit-ai-summarizer.git" - dns_zone_name = "meshcloud-dev-ske-starterkit" + dns_zone_name = var.test_context.dns_zone_name add_random_name_suffix = false building_block_definitions = { @@ -166,3 +180,16 @@ resource "meshstack_building_block_v2" "this" { depends_on = [module.meshstack_kubernetes_platform] } + +# Probe the deployed dev + prod app endpoints: reaching SUCCEEDED means the app was deployed, but +# not that the ingress actually serves traffic with a valid, cert-manager-issued certificate. The +# script GETs the URL over TLS (verified against the system trust store) and retries while +# cert-manager issues the cert; the test asserts each returns 200. Referencing the BB outputs makes +# these data sources read after the building block completes. +data "external" "app_probe" { + for_each = toset(["dev", "prod"]) + program = ["python3", "${path.module}/probe_endpoint.py"] + query = { + url = meshstack_building_block_v2.this.status.outputs["app_link_${each.key}"].value_string + } +} diff --git a/modules/ske/ske-starterkit/e2e/probe_endpoint.py b/modules/ske/ske-starterkit/e2e/probe_endpoint.py new file mode 100644 index 00000000..669bdfea --- /dev/null +++ b/modules/ske/ske-starterkit/e2e/probe_endpoint.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Probe a single app endpoint for HTTP 200 over valid TLS. + +Terraform `external` data source protocol: reads {"url": ...} as JSON on stdin and prints +{"status": ...} as JSON on stdout. The status is the HTTP status code as a string (e.g. "200") +for a TLS-verified response, or "error: " if the endpoint stays unreachable / the +certificate stays invalid until the deadline. The caller loops over endpoints with `for_each`. + +Why probe here: the building block reaching SUCCEEDED means the app was deployed, but it verifies +nothing about the ingress actually serving traffic with a valid, cert-manager-issued certificate. +TLS is verified against the system trust store (the point of the check — a self-signed or +untrusted cert fails verification). cert-manager issues the certificate asynchronously, so the +URL is retried for a bounded time before giving up. +""" + +import json +import ssl +import sys +import time +import urllib.error +import urllib.request + +DEADLINE_SECONDS = 180 +POLL_INTERVAL_SECONDS = 10 +REQUEST_TIMEOUT_SECONDS = 15 + + +def probe(url): + """Return "200" once the URL serves 200 over verified TLS, else the last status/error.""" + ctx = ssl.create_default_context() # verifies hostname + chain against the system trust store + deadline = time.monotonic() + DEADLINE_SECONDS + last = "error: no attempt made" + while True: + try: + with urllib.request.urlopen( + urllib.request.Request(url, method="GET"), + timeout=REQUEST_TIMEOUT_SECONDS, + context=ctx, + ) as resp: + if resp.status == 200: + return "200" + last = str(resp.status) # TLS ok, but not ready yet — keep retrying + except urllib.error.HTTPError as e: + last = str(e.code) # TLS ok, non-2xx (app not ready yet) — keep retrying + except ssl.SSLCertVerificationError as e: + last = f"error: tls verification failed: {e.reason}" # cert not (yet) valid + except (urllib.error.URLError, OSError) as e: + last = f"error: {e}" + if time.monotonic() >= deadline: + return last + time.sleep(POLL_INTERVAL_SECONDS) + + +def main(): + query = json.load(sys.stdin) + print(json.dumps({"status": probe(query["url"])})) + + +if __name__ == "__main__": + main() diff --git a/modules/ske/ske-starterkit/e2e/terraform.tf b/modules/ske/ske-starterkit/e2e/terraform.tf index 6b9b271a..46994e51 100644 --- a/modules/ske/ske-starterkit/e2e/terraform.tf +++ b/modules/ske/ske-starterkit/e2e/terraform.tf @@ -11,5 +11,8 @@ terraform { random = { source = "hashicorp/random" } + external = { + source = "hashicorp/external" + } } } diff --git a/modules/ske/ske-starterkit/e2e/tests/building_block_ske_starterkit_hub.tftest.hcl b/modules/ske/ske-starterkit/e2e/tests/building_block_ske_starterkit_hub.tftest.hcl index 073cd733..5036f86f 100644 --- a/modules/ske/ske-starterkit/e2e/tests/building_block_ske_starterkit_hub.tftest.hcl +++ b/modules/ske/ske-starterkit/e2e/tests/building_block_ske_starterkit_hub.tftest.hcl @@ -13,4 +13,15 @@ run "building_block_ske_starterkit_hub" { condition = can(regex("^https://", meshstack_building_block_v2.this.status.outputs["app_link_prod"].value_string)) error_message = "ske-starterkit hub building block expected app_link_prod to be a URL, got ${meshstack_building_block_v2.this.status.outputs["app_link_prod"].value_string}" } + + # The app must actually serve traffic over a valid (cert-manager-issued) TLS certificate. + assert { + condition = data.external.app_probe["dev"].result.status == "200" + error_message = "dev app endpoint expected HTTP 200 over verified TLS, got ${data.external.app_probe["dev"].result.status}" + } + + assert { + condition = data.external.app_probe["prod"].result.status == "200" + error_message = "prod app endpoint expected HTTP 200 over verified TLS, got ${data.external.app_probe["prod"].result.status}" + } }