Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion modules/ske/ske-starterkit/e2e/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ variable "test_context" {
name_suffix = string
forgejo_base_url = string
forgejo_organization = string
dns_zone_name = string
})
nullable = false
}
Expand Down Expand Up @@ -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" {
Expand All @@ -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 = {
Expand Down Expand Up @@ -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
}
}
60 changes: 60 additions & 0 deletions modules/ske/ske-starterkit/e2e/probe_endpoint.py
Original file line number Diff line number Diff line change
@@ -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: <reason>" 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()
3 changes: 3 additions & 0 deletions modules/ske/ske-starterkit/e2e/terraform.tf
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@ terraform {
random = {
source = "hashicorp/random"
}
external = {
source = "hashicorp/external"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
}
}
Loading