House conventions port and valid listener names for wildcard certificates #1
@@ -7,7 +7,7 @@ Plan: `docs/plans/2026-10-08-2028-house-conventions.md`
|
||||
- [x] Step 2 — `.claude/` and docs scaffolding
|
||||
- [x] Step 3 — Makefile, lint, formatting
|
||||
- [x] Step 4 — Module path and version identity
|
||||
- [ ] Step 5 — Baseline tests
|
||||
- [x] Step 5 — Baseline tests
|
||||
- [ ] Step 6 — CI build workflow
|
||||
- [ ] Step 7 — Close out
|
||||
|
||||
@@ -113,3 +113,20 @@ flag wiring is verified by `--help` output and the build only. `GOARCH` defaults
|
||||
`amd64` when `TARGETARCH` is empty (legacy builder, as on the CI runner) and follows
|
||||
the host under BuildKit, so a local build on an Apple Silicon machine is arm64.
|
||||
`docs/flows.md` still cites pre-change `main.go` line numbers in §2.
|
||||
|
||||
## Step 5 — Baseline tests
|
||||
|
||||
`internal/gateway/patch_test.go`: five table-driven tests over the pure functions.
|
||||
|
||||
```
|
||||
$ go test -race -cover ./...
|
||||
ok .../internal/gateway 1.870s coverage: 96.9% of statements
|
||||
ok .../internal/version 2.196s coverage: 94.7% of statements
|
||||
```
|
||||
|
||||
Worth noting: `internal/controller` and `main` are at 0%; reconciler tests with the
|
||||
fake client come with the first reconcile fix. Wildcard names, name collisions and
|
||||
truncation are deliberately not asserted, so the suite does not pin behaviour that
|
||||
the bug-fix phase will change. `TestListenersEqual_managedFields` only asserts the
|
||||
four fields the comparison covers today; it says nothing about protocol, allowed
|
||||
routes or TLS mode.
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
cmv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
)
|
||||
|
||||
const testPrefix = "auto-"
|
||||
|
||||
func cert(namespace, name, secret string, dnsNames ...string) cmv1.Certificate {
|
||||
return cmv1.Certificate{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name},
|
||||
Spec: cmv1.CertificateSpec{SecretName: secret, DNSNames: dnsNames},
|
||||
}
|
||||
}
|
||||
|
||||
func listener(name, hostname, secretNamespace, secret string, port gwapiv1.PortNumber) gwapiv1.Listener {
|
||||
h := gwapiv1.Hostname(hostname)
|
||||
ns := gwapiv1.Namespace(secretNamespace)
|
||||
return gwapiv1.Listener{
|
||||
Name: gwapiv1.SectionName(name),
|
||||
Hostname: &h,
|
||||
Port: port,
|
||||
Protocol: gwapiv1.HTTPSProtocolType,
|
||||
TLS: &gwapiv1.ListenerTLSConfig{
|
||||
CertificateRefs: []gwapiv1.SecretObjectReference{
|
||||
{Name: gwapiv1.ObjectName(secret), Namespace: &ns},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func names(listeners []gwapiv1.Listener) []string {
|
||||
out := make([]string, 0, len(listeners))
|
||||
for _, l := range listeners {
|
||||
out = append(out, string(l.Name))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func equalStrings(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestBuildListenersFromCertificates_names(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
deleted := cert("default", "gone", "gone-tls", "gone.example.com")
|
||||
now := metav1.NewTime(time.Unix(0, 0))
|
||||
deleted.DeletionTimestamp = &now
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
certs []cmv1.Certificate
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "no certificates",
|
||||
},
|
||||
{
|
||||
name: "one listener per DNS name",
|
||||
certs: []cmv1.Certificate{cert("default", "web", "web-tls", "a.example.com", "b.example.com")},
|
||||
want: []string{"auto-web-a-example-com", "auto-web-b-example-com"},
|
||||
},
|
||||
{
|
||||
name: "sorted by listener name across certificates",
|
||||
certs: []cmv1.Certificate{
|
||||
cert("default", "zeta", "zeta-tls", "z.example.com"),
|
||||
cert("default", "alpha", "alpha-tls", "a.example.com"),
|
||||
},
|
||||
want: []string{"auto-alpha-a-example-com", "auto-zeta-z-example-com"},
|
||||
},
|
||||
{
|
||||
name: "certificate being deleted is skipped",
|
||||
certs: []cmv1.Certificate{deleted, cert("default", "web", "web-tls", "a.example.com")},
|
||||
want: []string{"auto-web-a-example-com"},
|
||||
},
|
||||
{
|
||||
name: "certificate without secret name is skipped",
|
||||
certs: []cmv1.Certificate{cert("default", "web", "", "a.example.com")},
|
||||
},
|
||||
{
|
||||
name: "certificate without DNS names yields nothing",
|
||||
certs: []cmv1.Certificate{cert("default", "web", "web-tls")},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := names(BuildListenersFromCertificates(tc.certs, testPrefix))
|
||||
if !equalStrings(got, tc.want) {
|
||||
t.Errorf("listener names = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildListenersFromCertificates_listenerFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := BuildListenersFromCertificates(
|
||||
[]cmv1.Certificate{cert("apps", "web", "web-tls", "app.example.com")}, testPrefix)
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("got %d listeners, want 1", len(got))
|
||||
}
|
||||
l := got[0]
|
||||
|
||||
if l.Hostname == nil || *l.Hostname != "app.example.com" {
|
||||
t.Errorf("hostname = %v, want app.example.com", l.Hostname)
|
||||
}
|
||||
if l.Port != 443 {
|
||||
t.Errorf("port = %d, want 443", l.Port)
|
||||
}
|
||||
if l.Protocol != gwapiv1.HTTPSProtocolType {
|
||||
t.Errorf("protocol = %q, want HTTPS", l.Protocol)
|
||||
}
|
||||
if l.TLS == nil || l.TLS.Mode == nil || *l.TLS.Mode != gwapiv1.TLSModeTerminate {
|
||||
t.Fatalf("TLS mode is not Terminate: %+v", l.TLS)
|
||||
}
|
||||
if len(l.TLS.CertificateRefs) != 1 {
|
||||
t.Fatalf("got %d certificate refs, want 1", len(l.TLS.CertificateRefs))
|
||||
}
|
||||
ref := l.TLS.CertificateRefs[0]
|
||||
if ref.Name != "web-tls" {
|
||||
t.Errorf("secret name = %q, want web-tls", ref.Name)
|
||||
}
|
||||
if ref.Namespace == nil || *ref.Namespace != "apps" {
|
||||
t.Errorf("secret namespace = %v, want the certificate's namespace apps", ref.Namespace)
|
||||
}
|
||||
if ref.Kind == nil || *ref.Kind != "Secret" {
|
||||
t.Errorf("ref kind = %v, want Secret", ref.Kind)
|
||||
}
|
||||
if l.AllowedRoutes == nil || l.AllowedRoutes.Namespaces == nil ||
|
||||
l.AllowedRoutes.Namespaces.From == nil ||
|
||||
*l.AllowedRoutes.Namespaces.From != gwapiv1.NamespacesFromAll {
|
||||
t.Errorf("allowed routes = %+v, want namespaces from All", l.AllowedRoutes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeListeners_ownership(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
manual := listener("https", "manual.example.com", "default", "manual-tls", 443)
|
||||
stale := listener("auto-old-old-example-com", "old.example.com", "default", "old-tls", 443)
|
||||
desired := listener("auto-web-a-example-com", "a.example.com", "default", "web-tls", 443)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
existing []gwapiv1.Listener
|
||||
desired []gwapiv1.Listener
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "both empty",
|
||||
},
|
||||
{
|
||||
name: "manual listeners are kept",
|
||||
existing: []gwapiv1.Listener{manual},
|
||||
desired: []gwapiv1.Listener{desired},
|
||||
want: []string{"https", "auto-web-a-example-com"},
|
||||
},
|
||||
{
|
||||
name: "stale managed listeners are dropped",
|
||||
existing: []gwapiv1.Listener{manual, stale},
|
||||
desired: []gwapiv1.Listener{desired},
|
||||
want: []string{"https", "auto-web-a-example-com"},
|
||||
},
|
||||
{
|
||||
name: "no desired listeners removes every managed one",
|
||||
existing: []gwapiv1.Listener{stale, manual},
|
||||
want: []string{"https"},
|
||||
},
|
||||
{
|
||||
name: "existing managed listener is not duplicated",
|
||||
existing: []gwapiv1.Listener{desired},
|
||||
desired: []gwapiv1.Listener{desired},
|
||||
want: []string{"auto-web-a-example-com"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := names(MergeListeners(tc.existing, tc.desired, testPrefix))
|
||||
if !equalStrings(got, tc.want) {
|
||||
t.Errorf("merged names = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenersEqual_managedFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
a := listener("auto-web-a-example-com", "a.example.com", "default", "web-tls", 443)
|
||||
b := listener("auto-web-b-example-com", "b.example.com", "default", "web-tls", 443)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
x, y []gwapiv1.Listener
|
||||
want bool
|
||||
}{
|
||||
{name: "both empty", want: true},
|
||||
{name: "identical", x: []gwapiv1.Listener{a, b}, y: []gwapiv1.Listener{a, b}, want: true},
|
||||
{name: "order is ignored", x: []gwapiv1.Listener{a, b}, y: []gwapiv1.Listener{b, a}, want: true},
|
||||
{name: "different length", x: []gwapiv1.Listener{a, b}, y: []gwapiv1.Listener{a}, want: false},
|
||||
{name: "different name", x: []gwapiv1.Listener{a}, y: []gwapiv1.Listener{b}, want: false},
|
||||
{
|
||||
name: "different hostname",
|
||||
x: []gwapiv1.Listener{a},
|
||||
y: []gwapiv1.Listener{listener("auto-web-a-example-com", "other.example.com", "default", "web-tls", 443)},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "different port",
|
||||
x: []gwapiv1.Listener{a},
|
||||
y: []gwapiv1.Listener{listener("auto-web-a-example-com", "a.example.com", "default", "web-tls", 8443)},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "different secret name",
|
||||
x: []gwapiv1.Listener{a},
|
||||
y: []gwapiv1.Listener{listener("auto-web-a-example-com", "a.example.com", "default", "other-tls", 443)},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "different secret namespace",
|
||||
x: []gwapiv1.Listener{a},
|
||||
y: []gwapiv1.Listener{listener("auto-web-a-example-com", "a.example.com", "other", "web-tls", 443)},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := ListenersEqual(tc.x, tc.y); got != tc.want {
|
||||
t.Errorf("ListenersEqual() = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListenerNameFor_sanitizesDots(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
certName string
|
||||
dnsName string
|
||||
want string
|
||||
}{
|
||||
{name: "dots become dashes", certName: "web", dnsName: "app.example.com", want: "auto-web-app-example-com"},
|
||||
{name: "single label", certName: "web", dnsName: "localhost", want: "auto-web-localhost"},
|
||||
{name: "existing dashes are kept", certName: "my-cert", dnsName: "my-app.example.com", want: "auto-my-cert-my-app-example-com"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := listenerNameFor(testPrefix, tc.certName, tc.dnsName); got != tc.want {
|
||||
t.Errorf("listenerNameFor() = %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user