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 ref.Group == nil || *ref.Group != "" { t.Errorf("ref group = %v, want the core group", ref.Group) } 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 TestMergeListeners_replacesManagedContent(t *testing.T) { t.Parallel() old := listener("auto-web-a-example-com", "a.example.com", "default", "old-tls", 443) desired := listener("auto-web-a-example-com", "a.example.com", "default", "web-tls", 443) got := MergeListeners([]gwapiv1.Listener{old}, []gwapiv1.Listener{desired}, testPrefix) if len(got) != 1 { t.Fatalf("got %d listeners, want 1", len(got)) } if name := got[0].TLS.CertificateRefs[0].Name; name != "web-tls" { t.Errorf("secret name = %q, want the desired web-tls", name) } } 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) } }) } }