|
39 | 39 | import shutil |
40 | 40 | import subprocess |
41 | 41 | import sys |
| 42 | +import urllib.error |
42 | 43 | import urllib.parse |
43 | 44 | import urllib.request |
44 | 45 |
|
|
50 | 51 | # under the "sdp" path, e.g. quay.io/stackable/sdp/hbase-operator. |
51 | 52 | QUAY_REPO = "stackable/sdp" |
52 | 53 |
|
| 54 | +HUB_API = "https://hub.stackable.tech/api/v1" |
| 55 | + |
53 | 56 |
|
54 | 57 | class ManifestException(Exception): |
55 | 58 | pass |
@@ -384,12 +387,21 @@ def generate_csv( |
384 | 387 | "spark-operator" if args.op_name == "spark-k8s-operator" else args.op_name |
385 | 388 | ) |
386 | 389 |
|
| 390 | + chart = load_chart(args) |
| 391 | + listing = CSV_LISTING[args.product] |
| 392 | + |
387 | 393 | result["spec"]["version"] = args.release |
388 | | - result["spec"]["keywords"] = [args.product] |
389 | | - result["spec"]["displayName"] = CSV_DISPLAY_NAME[args.product] |
| 394 | + result["spec"]["displayName"] = listing["display_name"] |
| 395 | + result["spec"]["description"] = chart["description"] |
| 396 | + # Charts from before SDP 26.11 carry no keywords. |
| 397 | + result["spec"]["keywords"] = chart.get("keywords", [args.product]) |
| 398 | + result["spec"]["links"] = csv_links(chart) |
| 399 | + result["spec"]["minKubeVersion"] = min_kube_version(args.release) |
390 | 400 | result["metadata"]["name"] = f"{csv_name}.v{args.release}" |
391 | 401 | result["metadata"]["annotations"]["containerImage"] = related_images[0]["image"] |
392 | | - result["metadata"]["annotations"]["description"] = CSV_DISPLAY_NAME[args.product] |
| 402 | + result["metadata"]["annotations"]["description"] = chart["description"] |
| 403 | + result["metadata"]["annotations"]["capabilities"] = listing["capabilities"] |
| 404 | + result["metadata"]["annotations"]["categories"] = listing["categories"] |
393 | 405 | result["metadata"]["annotations"]["repository"] = ( |
394 | 406 | f"https://github.com/stackabletech/{args.op_name}" |
395 | 407 | ) |
@@ -419,6 +431,80 @@ def generate_csv( |
419 | 431 | return result |
420 | 432 |
|
421 | 433 |
|
| 434 | +def load_chart(args: argparse.Namespace) -> dict: |
| 435 | + chart_path = args.repo_operator / "deploy" / "helm" / args.op_name / "Chart.yaml" |
| 436 | + return yaml.load(chart_path.read_text(), Loader=yaml.SafeLoader) |
| 437 | + |
| 438 | + |
| 439 | +def csv_links(chart: dict) -> list[dict[str, str]]: |
| 440 | + """Turn the chart's Artifact Hub links into CSV links. |
| 441 | +
|
| 442 | + >>> links = "[{name: Documentation, url: https://docs.stackable.tech}, {name: support, url: https://github.com}]" |
| 443 | + >>> csv_links({"annotations": {"artifacthub.io/links": links}}) |
| 444 | + [{'name': 'Documentation', 'url': 'https://docs.stackable.tech'}, {'name': 'Support', 'url': 'https://github.com'}] |
| 445 | +
|
| 446 | + Charts without the annotation get no links: |
| 447 | +
|
| 448 | + >>> csv_links({"description": "An older chart"}) |
| 449 | + [] |
| 450 | + """ |
| 451 | + links = yaml.load( |
| 452 | + chart.get("annotations", {}).get("artifacthub.io/links", "[]"), |
| 453 | + Loader=yaml.SafeLoader, |
| 454 | + ) |
| 455 | + # Artifact Hub only highlights a link named exactly "support"; the console shows names as-is. |
| 456 | + return [ |
| 457 | + { |
| 458 | + "name": "Support" if link["name"] == "support" else link["name"], |
| 459 | + "url": link["url"], |
| 460 | + } |
| 461 | + for link in links |
| 462 | + ] |
| 463 | + |
| 464 | + |
| 465 | +def hub_release_name(release: str) -> str: |
| 466 | + """Return the name the Stackable Hub uses for a release. |
| 467 | +
|
| 468 | + >>> hub_release_name("26.11.0") |
| 469 | + '26.11' |
| 470 | + >>> hub_release_name("26.7.1") |
| 471 | + '26.7' |
| 472 | + >>> hub_release_name("0.0.0-dev") |
| 473 | + 'latest' |
| 474 | + """ |
| 475 | + if release == "0.0.0-dev": |
| 476 | + return "latest" |
| 477 | + return ".".join(release.split(".")[:2]) |
| 478 | + |
| 479 | + |
| 480 | +def lowest_kube_version(platforms: list[dict]) -> str: |
| 481 | + """Return the lowest Kubernetes version in the Hub's platform list, as a full version. |
| 482 | +
|
| 483 | + Versions compare numerically, so 1.9 is lower than 1.10: |
| 484 | +
|
| 485 | + >>> lowest_kube_version([ |
| 486 | + ... {"name": "OpenShift", "versions": ["4.20", "4.21"]}, |
| 487 | + ... {"name": "Kubernetes", "versions": ["1.10", "1.9", "1.11"]}, |
| 488 | + ... ]) |
| 489 | + '1.9.0' |
| 490 | + """ |
| 491 | + versions = next(p["versions"] for p in platforms if p["name"] == "Kubernetes") |
| 492 | + lowest = min(versions, key=lambda v: tuple(int(x) for x in v.split("."))) |
| 493 | + return f"{lowest}.0" |
| 494 | + |
| 495 | + |
| 496 | +def min_kube_version(release: str) -> str: |
| 497 | + """Return the lowest Kubernetes version the release supports, according to the Stackable Hub.""" |
| 498 | + hub_release = hub_release_name(release) |
| 499 | + url = f"{HUB_API}/releases/{hub_release}" |
| 500 | + try: |
| 501 | + with urllib.request.urlopen(url) as response: |
| 502 | + platforms = json.load(response)["platforms"] |
| 503 | + except urllib.error.HTTPError as e: |
| 504 | + raise ManifestException(f"Could not read release {hub_release} from {url}: {e}") |
| 505 | + return lowest_kube_version(platforms) |
| 506 | + |
| 507 | + |
422 | 508 | def generate_helm_templates(args: argparse.Namespace) -> list[dict]: |
423 | 509 | logging.debug(f"start generate_helm_templates for {args.repo_operator}") |
424 | 510 | template_path = args.repo_operator / "deploy" / "helm" / args.repo_operator.name |
@@ -592,21 +678,81 @@ def main(argv) -> int: |
592 | 678 | return 0 |
593 | 679 |
|
594 | 680 |
|
595 | | -CSV_DISPLAY_NAME = { |
596 | | - "airflow": "Stackable Operator for Apache Airflow", |
597 | | - "commons": "Stackable Commons Operator", |
598 | | - "druid": "Stackable Operator for Apache Druid", |
599 | | - "hbase": "Stackable Operator for Apache HBase", |
600 | | - "hdfs": "Stackable Operator for Apache Hadoop HDFS", |
601 | | - "hive": "Stackable Operator for Apache Hive", |
602 | | - "kafka": "Stackable Operator for Apache Kafka", |
603 | | - "nifi": "Stackable Operator for Apache NiFi", |
604 | | - "opa": "Stackable Operator for the Open Policy Agent", |
605 | | - "opensearch": "Stackable Operator for OpenSearch", |
606 | | - "spark-k8s": "Stackable Operator for Apache Spark", |
607 | | - "superset": "Stackable Operator for Apache Superset", |
608 | | - "trino": "Stackable Operator for Trino", |
609 | | - "zookeeper": "Stackable Operator for Apache ZooKeeper", |
| 681 | +# OLM-only listing values. |
| 682 | +# Everything else the listing shows comes from the operator's Chart.yaml. |
| 683 | +# * capabilities: https://sdk.operatorframework.io/docs/overview/operator-capabilities/ |
| 684 | +# * categories: comma-separated, from the list in https://github.com/operator-framework/api/blob/3d2cbf67cb0990b1b28956bd6eeb21054ca33d0a/pkg/validation/internal/standardcategories.go |
| 685 | +CSV_LISTING = { |
| 686 | + "airflow": { |
| 687 | + "display_name": "Stackable Operator for Apache Airflow", |
| 688 | + "capabilities": "Seamless Upgrades", |
| 689 | + "categories": "Big Data, Integration & Delivery", |
| 690 | + }, |
| 691 | + "commons": { |
| 692 | + "display_name": "Stackable Commons Operator", |
| 693 | + "capabilities": "Basic Install", |
| 694 | + "categories": "Big Data", |
| 695 | + }, |
| 696 | + "druid": { |
| 697 | + "display_name": "Stackable Operator for Apache Druid", |
| 698 | + "capabilities": "Seamless Upgrades", |
| 699 | + "categories": "Big Data, Database", |
| 700 | + }, |
| 701 | + "hbase": { |
| 702 | + "display_name": "Stackable Operator for Apache HBase", |
| 703 | + "capabilities": "Seamless Upgrades", |
| 704 | + "categories": "Big Data, Database", |
| 705 | + }, |
| 706 | + "hdfs": { |
| 707 | + "display_name": "Stackable Operator for Apache Hadoop HDFS", |
| 708 | + "capabilities": "Seamless Upgrades", |
| 709 | + "categories": "Big Data", |
| 710 | + }, |
| 711 | + "hive": { |
| 712 | + "display_name": "Stackable Operator for Apache Hive", |
| 713 | + "capabilities": "Seamless Upgrades", |
| 714 | + "categories": "Big Data, Database", |
| 715 | + }, |
| 716 | + "kafka": { |
| 717 | + "display_name": "Stackable Operator for Apache Kafka", |
| 718 | + "capabilities": "Seamless Upgrades", |
| 719 | + "categories": "Big Data, Streaming & Messaging", |
| 720 | + }, |
| 721 | + "nifi": { |
| 722 | + "display_name": "Stackable Operator for Apache NiFi", |
| 723 | + "capabilities": "Seamless Upgrades", |
| 724 | + "categories": "Big Data, Integration & Delivery", |
| 725 | + }, |
| 726 | + "opa": { |
| 727 | + "display_name": "Stackable Operator for the Open Policy Agent", |
| 728 | + "capabilities": "Seamless Upgrades", |
| 729 | + "categories": "Big Data, Security", |
| 730 | + }, |
| 731 | + "opensearch": { |
| 732 | + "display_name": "Stackable Operator for OpenSearch", |
| 733 | + "capabilities": "Seamless Upgrades", |
| 734 | + "categories": "Big Data", |
| 735 | + }, |
| 736 | + "spark-k8s": { |
| 737 | + "display_name": "Stackable Operator for Apache Spark", |
| 738 | + "capabilities": "Seamless Upgrades", |
| 739 | + "categories": "Big Data, Integration & Delivery", |
| 740 | + }, |
| 741 | + "superset": { |
| 742 | + "display_name": "Stackable Operator for Apache Superset", |
| 743 | + "capabilities": "Seamless Upgrades", |
| 744 | + "categories": "Big Data", |
| 745 | + }, |
| 746 | + "trino": { |
| 747 | + "display_name": "Stackable Operator for Trino", |
| 748 | + "capabilities": "Seamless Upgrades", |
| 749 | + "categories": "Big Data, Database", |
| 750 | + }, |
| 751 | + "zookeeper": { |
| 752 | + "display_name": "Stackable Operator for Apache ZooKeeper", |
| 753 | + "capabilities": "Seamless Upgrades", |
| 754 | + "categories": "Big Data", |
| 755 | + }, |
610 | 756 | } |
611 | 757 |
|
612 | 758 | if __name__ == "__main__": |
|
0 commit comments