From f9e4c90762525d96be5457d439d97609660fa5ed Mon Sep 17 00:00:00 2001 From: Lars Francke Date: Wed, 21 Oct 2026 17:04:24 +0200 Subject: [PATCH 1/3] fix(olm): Resolve images for the chart layout since 26.7 --- scripts/generate-olm.py | 75 ++++++++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 28 deletions(-) diff --git a/scripts/generate-olm.py b/scripts/generate-olm.py index 6b112711..60b4c406 100755 --- a/scripts/generate-olm.py +++ b/scripts/generate-olm.py @@ -37,13 +37,10 @@ HELM_CHART = REPO_ROOT / "deploy" / "helm" / "secret-operator" OLM_OUTPUT = REPO_ROOT / "deploy" / "olm" -# Mapping from OCI registry image repos to quay.io image repos. -# The script replaces oci.stackable.tech images with quay.io images (using digest). -OCI_TO_QUAY: dict[str, str] = { - "oci.stackable.tech/sdp/secret-operator": "quay.io/stackable/secret-operator", - "oci.stackable.tech/sdp/sig-storage/csi-provisioner": "quay.io/stackable/sig-storage/csi-provisioner", - "oci.stackable.tech/sdp/sig-storage/csi-node-driver-registrar": "quay.io/stackable/sig-storage/csi-node-driver-registrar", -} +# The chart is rendered with its own quay.io overlay, so the operator image comes from that +# registry. The CSI sidecars are only mirrored to quay.io/stackable/sig-storage. +QUAY_OVERLAY = HELM_CHART / "values" / "quay.io.yaml" +SIDECAR_QUAY_REPO = "quay.io/stackable/sig-storage" # Stackable logo icon (PNG, base64-encoded). Sourced from the 25.11.0 OLM bundle. # typos:ignore-block-start @@ -1052,7 +1049,7 @@ def get_quay_digest(quay_image: str, tag: str) -> str: ) from exc -def render_helm(version: str) -> list[dict]: +def render_helm(version: str, extra_values: list[str]) -> list[dict]: """Render the Helm chart with OpenShift APIs enabled and return parsed YAML docs.""" result = subprocess.run( [ @@ -1063,6 +1060,9 @@ def render_helm(version: str) -> list[dict]: # Enable OpenShift API so that the SCC manifest is included. "--api-versions", "security.openshift.io/v1", + "--values", + str(QUAY_OVERLAY), + *(arg for path in extra_values for arg in ("--values", path)), "--set", f"image.tag={version}", str(HELM_CHART), @@ -1080,21 +1080,29 @@ def load_values() -> dict: return yaml.safe_load(fh) -def sidecar_image_info(values: dict) -> dict[str, dict[str, str]]: - """Return a dict of sidecar name -> {repo, tag} from values.yaml.""" +def load_registry() -> str: + with open(QUAY_OVERLAY) as fh: + return yaml.safe_load(fh)["image"]["repository"] + + +def sidecar_image_info(values: dict, registry: str) -> dict[str, dict[str, str]]: + """Return a dict of sidecar name -> {repo, tag} as the chart renders them. + + Like the chart, a sidecar repository that values.yaml leaves unset defaults to + /sig-storage. + """ + images = { + "csi-provisioner": values["csiNodeDriver"]["externalProvisioner"]["image"], + "csi-node-driver-registrar": values["csiNodeDriver"]["nodeDriverRegistrar"][ + "image" + ], + } return { - "csi-provisioner": { - "repo": values["csiNodeDriver"]["externalProvisioner"]["image"][ - "repository" - ], - "tag": values["csiNodeDriver"]["externalProvisioner"]["image"]["tag"], - }, - "csi-node-driver-registrar": { - "repo": values["csiNodeDriver"]["nodeDriverRegistrar"]["image"][ - "repository" - ], - "tag": values["csiNodeDriver"]["nodeDriverRegistrar"]["image"]["tag"], - }, + name: { + "repo": f"{image.get('repository') or f'{registry}/sig-storage'}/{name}", + "tag": image["tag"], + } + for name, image in images.items() } @@ -1445,6 +1453,17 @@ def main() -> None: f" (default: {OLM_OUTPUT.relative_to(REPO_ROOT)})" ), ) + parser.add_argument( + "--values", + "-f", + metavar="FILE", + action="append", + default=[], + help=( + "Extra Helm values file, can be repeated. The OLM bundles of a release take the" + " memory overrides in stackable-utils/olm/resources/values/secret-operator/values.yaml." + ), + ) args = parser.parse_args() version: str = args.version @@ -1464,9 +1483,9 @@ def main() -> None: # ------------------------------------------------------------------ print("[1/4] Reading Helm values.yaml ...") values = load_values() - sidecars = sidecar_image_info(values) - main_oci_repo: str = values["image"]["repository"] - main_quay_repo = OCI_TO_QUAY[main_oci_repo] + registry = load_registry() + sidecars = sidecar_image_info(values, registry) + main_quay_repo = f"{registry}/secret-operator" # ------------------------------------------------------------------ # 2. Look up quay.io image digests @@ -1482,11 +1501,11 @@ def main() -> None: } # image_map: OCI repo:tag -> quay.io@digest (for textual substitution) image_map: dict[str, str] = { - f"{main_oci_repo}:{version}": final_images["secret-operator"], + f"{main_quay_repo}:{version}": final_images["secret-operator"], } for sidecar_name, info in sidecars.items(): - quay_repo = OCI_TO_QUAY[info["repo"]] + quay_repo = f"{SIDECAR_QUAY_REPO}/{sidecar_name}" tag = info["tag"] print(f" {quay_repo}:{tag}") digest = get_quay_digest(quay_repo, tag) @@ -1499,7 +1518,7 @@ def main() -> None: # 3. Render Helm chart # ------------------------------------------------------------------ print("[3/4] Rendering Helm chart ...") - helm_docs = render_helm(version) + helm_docs = render_helm(version, [str(Path(f).resolve()) for f in args.values]) print(f" -> {len(helm_docs)} Kubernetes resources") # ------------------------------------------------------------------ From e09c7bea4f80b7a4b67722eaee3c3e80b4e140b3 Mon Sep 17 00:00:00 2001 From: Lars Francke Date: Wed, 21 Oct 2026 17:04:28 +0200 Subject: [PATCH 2/3] feat(olm): Take the CSV listing metadata from the chart --- scripts/generate-olm.py | 99 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 87 insertions(+), 12 deletions(-) diff --git a/scripts/generate-olm.py b/scripts/generate-olm.py index 60b4c406..a43c3c10 100755 --- a/scripts/generate-olm.py +++ b/scripts/generate-olm.py @@ -25,6 +25,7 @@ import argparse import subprocess +import json import urllib.request import urllib.error from pathlib import Path @@ -42,6 +43,8 @@ QUAY_OVERLAY = HELM_CHART / "values" / "quay.io.yaml" SIDECAR_QUAY_REPO = "quay.io/stackable/sig-storage" +HUB_API = "https://hub.stackable.tech/api/v1" + # Stackable logo icon (PNG, base64-encoded). Sourced from the 25.11.0 OLM bundle. # typos:ignore-block-start _STACKABLE_ICON_B64 = ( @@ -1225,6 +1228,81 @@ def _operator_cluster_rules(helm_docs: list[dict]) -> list[dict]: return [] +def load_chart() -> dict: + with open(HELM_CHART / "Chart.yaml") as fh: + return yaml.safe_load(fh) + + +def csv_links(chart: dict) -> list[dict[str, str]]: + """Turn the chart's Artifact Hub links into CSV links. + + >>> links = "[{name: Documentation, url: https://docs.stackable.tech}, {name: support, url: https://github.com}]" + >>> csv_links({"annotations": {"artifacthub.io/links": links}}) + [{'name': 'Documentation', 'url': 'https://docs.stackable.tech'}, {'name': 'Support', 'url': 'https://github.com'}] + + Charts without the annotation get no links: + + >>> csv_links({"description": "An older chart"}) + [] + """ + links = yaml.safe_load( + chart.get("annotations", {}).get("artifacthub.io/links", "[]") + ) + # Artifact Hub only highlights a link named exactly "support"; the console shows names as-is. + return [ + { + "name": "Support" if link["name"] == "support" else link["name"], + "url": link["url"], + } + for link in links + ] + + +def hub_release_name(release: str) -> str: + """Return the name the Stackable Hub uses for a release. + + >>> hub_release_name("26.11.0") + '26.11' + >>> hub_release_name("26.7.1") + '26.7' + >>> hub_release_name("0.0.0-dev") + 'latest' + """ + if release == "0.0.0-dev": + return "latest" + return ".".join(release.split(".")[:2]) + + +def lowest_kube_version(platforms: list[dict]) -> str: + """Return the lowest Kubernetes version in the Hub's platform list, as a full version. + + Versions compare numerically, so 1.9 is lower than 1.10: + + >>> lowest_kube_version([ + ... {"name": "OpenShift", "versions": ["4.20", "4.21"]}, + ... {"name": "Kubernetes", "versions": ["1.10", "1.9", "1.11"]}, + ... ]) + '1.9.0' + """ + versions = next(p["versions"] for p in platforms if p["name"] == "Kubernetes") + lowest = min(versions, key=lambda v: tuple(int(x) for x in v.split("."))) + return f"{lowest}.0" + + +def min_kube_version(release: str) -> str: + """Return the lowest Kubernetes version the release supports, according to the Stackable Hub.""" + hub_release = hub_release_name(release) + url = f"{HUB_API}/releases/{hub_release}" + try: + with urllib.request.urlopen(url) as resp: + platforms = json.load(resp)["platforms"] + except urllib.error.HTTPError as exc: + raise RuntimeError( + f"Failed to read release {hub_release} from {url}: HTTP {exc.code}" + ) from exc + return lowest_kube_version(platforms) + + def build_csv( version: str, helm_docs: list[dict], @@ -1233,6 +1311,7 @@ def build_csv( openshift_versions: str = "v4.18-v4.21", ) -> dict: """Build and return the ClusterServiceVersion manifest as a dict.""" + chart = load_chart() deployer_deployment = { "name": f"secret-operator-deployer-{version}", @@ -1323,9 +1402,9 @@ def build_csv( "features.operators.openshift.io/token-auth-azure": "false", "features.operators.openshift.io/token-auth-gcp": "false", "support": "Stackable GmbH", - "categories": "Storage", - "capabilities": "Full Lifecycle", - "description": "Stackable Secret Operator", + "categories": "Big Data, Security", + "capabilities": "Basic Install", + "description": chart["description"], "repository": "https://github.com/stackabletech/secret-operator", "com.redhat.openshift.versions": openshift_versions, }, @@ -1333,24 +1412,20 @@ def build_csv( "spec": { "displayName": "Stackable Secret Operator", "description": ( - "This is a Kubernetes operator to provide secret functionality to other" - " Stackable operators. The Stackable Secret Operator is part of the Stackable" - " Data Platform, a curated selection of the best open source data apps like" - " Kafka, Druid, Trino or Spark, all working together seamlessly. Based on" - " Kubernetes, it runs everywhere - on prem or in the cloud.\n\n" - "You can install the operator using" - " [stackablectl or helm](https://docs.stackable.tech/home/stable/secret-operator/installation.html).\n\n" + f"{chart['description']}\n\n" "NOTE:\n" 'Make sure you install this operator in a namespace called "stackable-operators".' " Failing to do so will result in a broken installation." ), "icon": [{"base64data": icon_b64, "mediatype": "image/png"}], - "keywords": ["secret"], + # Charts from before SDP 26.11 carry no keywords. + "keywords": chart.get("keywords", ["secret"]), + "links": csv_links(chart), "maintainers": [{"email": "info@stackable.tech", "name": "Stackable GmbH"}], "maturity": "stable", "provider": {"name": "Stackable GmbH", "url": "https://stackable.tech"}, "version": version, - "minKubeVersion": "1.23.0", + "minKubeVersion": min_kube_version(version), "installModes": [ {"supported": True, "type": "OwnNamespace"}, {"supported": True, "type": "SingleNamespace"}, From 9c23530f68f1129ff913797c8a784cc90d9c2211 Mon Sep 17 00:00:00 2001 From: Lars Francke Date: Thu, 22 Oct 2026 12:13:02 +0200 Subject: [PATCH 3/3] refactor(olm): Remove the --values option --- scripts/generate-olm.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/scripts/generate-olm.py b/scripts/generate-olm.py index a43c3c10..93c17066 100755 --- a/scripts/generate-olm.py +++ b/scripts/generate-olm.py @@ -1052,7 +1052,7 @@ def get_quay_digest(quay_image: str, tag: str) -> str: ) from exc -def render_helm(version: str, extra_values: list[str]) -> list[dict]: +def render_helm(version: str) -> list[dict]: """Render the Helm chart with OpenShift APIs enabled and return parsed YAML docs.""" result = subprocess.run( [ @@ -1065,7 +1065,6 @@ def render_helm(version: str, extra_values: list[str]) -> list[dict]: "security.openshift.io/v1", "--values", str(QUAY_OVERLAY), - *(arg for path in extra_values for arg in ("--values", path)), "--set", f"image.tag={version}", str(HELM_CHART), @@ -1528,17 +1527,6 @@ def main() -> None: f" (default: {OLM_OUTPUT.relative_to(REPO_ROOT)})" ), ) - parser.add_argument( - "--values", - "-f", - metavar="FILE", - action="append", - default=[], - help=( - "Extra Helm values file, can be repeated. The OLM bundles of a release take the" - " memory overrides in stackable-utils/olm/resources/values/secret-operator/values.yaml." - ), - ) args = parser.parse_args() version: str = args.version @@ -1593,7 +1581,7 @@ def main() -> None: # 3. Render Helm chart # ------------------------------------------------------------------ print("[3/4] Rendering Helm chart ...") - helm_docs = render_helm(version, [str(Path(f).resolve()) for f in args.values]) + helm_docs = render_helm(version) print(f" -> {len(helm_docs)} Kubernetes resources") # ------------------------------------------------------------------