- ci: reject a manual dispatch with tag "latest"; concurrency group per tag - Dockerfile: declare GIT_COMMIT after the module download layer - Makefile: docker-build targets linux/amd64 by default - patch.go: no shared package-level pointers; main.go: no init() - tests: assert that merge replaces managed listener content - docs: flows.md startup section matches main.go; review saved to docs/reviews Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
298 lines
8.8 KiB
Go
298 lines
8.8 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|