House conventions port and valid listener names for wildcard certificates #1

Merged
kacerr merged 11 commits from feat/house-conventions into main 2026-10-08 20:48:32 +02:00
2 changed files with 297 additions and 1 deletions
Showing only changes of commit ab408403bc - Show all commits
@@ -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.
+279
View File
@@ -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)
}
})
}
}