Files
nchain/internal/manifests/cloud_api_ingress_tls_test.go
zeekayandHanzo Dev b9c82eb058 ingress: group TLS by registered domain everywhere, not just the Cloud API
The Cloud API builder was fixed to emit one certificate per brand. The generic
BuildIngress — used by Indexer, Explorer, Bridge and Gateway — still put every
host in spec.Hosts on ONE certificate, so it carries the same latent failure:
ACME issues a SAN cert only when every name validates, and one CDN-fronted host
that 404s its challenge blocks issuance and renewal for all the others.

No live CR spans two domains today, so this is prevention rather than a fix. But
nothing guarantees that — Hosts is a free-form list and the convention is only a
convention, which is exactly how web3-platform-api-tls ended up bundling ten
hosts across five domains and stalling past its renewal.

tlsPerRegisteredDomain groups hosts by their last two labels and emits one entry
per domain. Single-domain Ingresses — the common case — are byte-identical to
before, including the "<name>-tls" secret, so nothing re-issues; the qualified
"<name>-<domain>-tls" form appears only when a second domain is present.

Tests: grouping across two domains, unchanged routing, and unchanged secret name
for the single-domain case.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-30 18:38:32 -07:00

94 lines
3.2 KiB
Go

package manifests
import (
"testing"
v1alpha1 "github.com/hanzoai/nchain/api/v1alpha1"
)
// A brand that cannot answer its HTTP-01 challenge must not be able to block
// certificate issuance for every other brand. That happens when all hosts share
// one SAN certificate, so the API Ingress must emit one TLS entry per brand.
func TestBuildCloudAPIIngress_TLSIsPerBrand(t *testing.T) {
c := &v1alpha1.Cloud{}
c.Name = "web3-platform"
c.Namespace = "bootnode"
c.Spec.Ingress = &v1alpha1.IngressSpec{Enabled: true, TLS: true}
c.Spec.Brands = []v1alpha1.BrandConfig{
{Name: "hanzo", Domain: "web3.hanzo.ai"},
{Name: "lux", Domain: "web3.lux.network"},
{Name: "bootnode", Domain: "bootno.de"},
}
ing := BuildCloudAPIIngress(c)
if got := len(ing.Spec.TLS); got != 3 {
t.Fatalf("want one TLS entry per brand (3), got %d", got)
}
for _, e := range ing.Spec.TLS {
if len(e.Hosts) != 2 {
t.Errorf("%s: want exactly api+ws (2 hosts), got %v", e.SecretName, e.Hosts)
}
}
if got, want := ing.Spec.TLS[0].SecretName, "web3-platform-api-hanzo-tls"; got != want {
t.Errorf("secret name = %q, want %q", got, want)
}
if got, want := ing.Spec.TLS[2].Hosts[0], "api.bootno.de"; got != want {
t.Errorf("bootnode host = %q, want %q", got, want)
}
// routing must be untouched: api+ws per brand
if got := len(ing.Spec.Rules); got != 6 {
t.Errorf("rules changed: want 6, got %d", got)
}
}
func TestBuildCloudAPIIngress_NoTLSWhenDisabled(t *testing.T) {
c := &v1alpha1.Cloud{}
c.Name = "web3-platform"
c.Spec.Ingress = &v1alpha1.IngressSpec{Enabled: true, TLS: false}
c.Spec.Brands = []v1alpha1.BrandConfig{{Name: "hanzo", Domain: "web3.hanzo.ai"}}
if got := len(BuildCloudAPIIngress(c).Spec.TLS); got != 0 {
t.Errorf("TLS disabled must emit no entries, got %d", got)
}
}
// A host that cannot answer its ACME challenge must not block certificates for
// hosts on other domains, so BuildIngress groups TLS by registered domain.
func TestBuildIngress_TLSGroupedByDomain(t *testing.T) {
spec := &v1alpha1.IngressSpec{
Enabled: true,
TLS: true,
Hosts: []string{"api.web3.hanzo.ai", "ws.web3.hanzo.ai", "api.bootno.de", "ws.bootno.de"},
}
ing := BuildIngress("gw", "ns", spec, "gw", 80, nil)
if got := len(ing.Spec.TLS); got != 2 {
t.Fatalf("want 2 TLS entries (hanzo.ai + bootno.de), got %d", got)
}
if got, want := ing.Spec.TLS[0].SecretName, "gw-hanzo-ai-tls"; got != want {
t.Errorf("secret = %q, want %q", got, want)
}
if got, want := len(ing.Spec.TLS[1].Hosts), 2; got != want {
t.Errorf("bootno.de hosts = %d, want %d", got, want)
}
if got := len(ing.Spec.Rules); got != 4 {
t.Errorf("routing changed: %d rules, want 4", got)
}
}
// The overwhelmingly common case is one domain; that must keep its existing
// secret name so live Ingresses are not forced to re-issue.
func TestBuildIngress_SingleDomainKeepsSecretName(t *testing.T) {
spec := &v1alpha1.IngressSpec{
Enabled: true, TLS: true,
Hosts: []string{"api.lux.network", "ws.lux.network"},
}
ing := BuildIngress("gw", "ns", spec, "gw", 80, nil)
if len(ing.Spec.TLS) != 1 {
t.Fatalf("want 1 entry, got %d", len(ing.Spec.TLS))
}
if got, want := ing.Spec.TLS[0].SecretName, "gw-tls"; got != want {
t.Errorf("secret = %q, want unchanged %q", got, want)
}
}