Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Changelog
Unreleased
----------

* Fixed the grand-central hostname being corrupted when the cluster name recurs
later in its external DNS name.

2.63.0 (2026-08-12)
-------------------

Expand Down
16 changes: 15 additions & 1 deletion crate/operator/grand_central.py
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,20 @@ def grand_central_uses_traefik(spec: kopf.Spec) -> bool:
return get_grand_central_exposure(spec) == "traefik"


def _grand_central_hostname(cluster_name: str, external_dns: str) -> str:
"""
Derive the grand-central hostname from a cluster's external DNS name.

``external_dns`` is built as ``<cluster_name>.<region>.<domain>.`` so the
cluster name is always its leading label. The grand-central hostname is the
same name with a ``.gc`` label inserted after that leading label.

:param cluster_name: The CrateDB cluster name (the leading DNS label).
:param external_dns: The cluster's external DNS name (may have a trailing dot).
"""
return external_dns.replace(cluster_name, f"{cluster_name}.gc", 1).rstrip(".")


async def create_grand_central_exposure(
namespace: str,
name: str,
Expand Down Expand Up @@ -794,7 +808,7 @@ async def create_grand_central_exposure(
owner_references = get_owner_references(name, meta)
cluster_name = spec["cluster"]["name"]
external_dns = spec["cluster"]["externalDNS"]
hostname = external_dns.replace(cluster_name, f"{cluster_name}.gc").rstrip(".")
hostname = _grand_central_hostname(cluster_name, external_dns)
labels = get_grand_central_labels(name, meta)
cidrs = spec["cluster"].get("allowedCIDRs", None)

Expand Down
36 changes: 36 additions & 0 deletions tests/test_grand_central_exposure.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import pytest

from crate.operator.grand_central import (
_grand_central_hostname,
get_grand_central_exposure,
grand_central_uses_traefik,
)
Expand Down Expand Up @@ -70,3 +71,38 @@ def test_explicit_grand_central_exposure_wins_over_cluster():
def test_grand_central_exposure_falls_back_to_cluster():
spec = {"cluster": {"exposure": "traefik"}}
assert get_grand_central_exposure(spec) == "traefik"


@pytest.mark.parametrize(
"cluster_name, external_dns, expected",
[
# Non-colliding: cluster name appears only as the leading label.
(
"mycluster",
"mycluster.eks1.us-west-2.aws.cratedb.net.",
"mycluster.gc.eks1.us-west-2.aws.cratedb.net",
),
# Collision with the region: 'us-west' also occurs in 'us-west-2'.
(
"us-west",
"us-west.eks1.us-west-2.aws.cratedb.net.",
"us-west.gc.eks1.us-west-2.aws.cratedb.net",
),
# Collision with the cloud suffix: 'aws' also occurs in '.aws.'.
(
"aws",
"aws.eks1.us-west-2.aws.cratedb.net.",
"aws.gc.eks1.us-west-2.aws.cratedb.net",
),
# Cluster literally named 'gc'.
(
"gc",
"gc.eks1.eu-central-1.azure.cratedb.net.",
"gc.gc.eks1.eu-central-1.azure.cratedb.net",
),
],
)
def test_grand_central_hostname_only_replaces_leading_label(
cluster_name, external_dns, expected
):
assert _grand_central_hostname(cluster_name, external_dns) == expected
Loading