package gateway import ( "fmt" "sort" "strings" cmv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" ) var ( defaultPort gwapiv1.PortNumber = 443 tlsModeTerminate = gwapiv1.TLSModeTerminate namespacesFromAll gwapiv1.FromNamespaces = gwapiv1.NamespacesFromAll gatewayGroup gwapiv1.Group = gwapiv1.GroupName gatewayKind gwapiv1.Kind = "Gateway" ) // BuildListenersFromCertificates creates the desired set of managed listeners // from all annotated Certificate resources targeting a single Gateway. func BuildListenersFromCertificates(certs []cmv1.Certificate, prefix string) []gwapiv1.Listener { var listeners []gwapiv1.Listener for _, cert := range certs { if cert.DeletionTimestamp != nil { continue } secretName := cert.Spec.SecretName if secretName == "" { continue } for _, dns := range cert.Spec.DNSNames { hostname := gwapiv1.Hostname(dns) listenerName := listenerNameFor(prefix, cert.Name, dns) // Determine the namespace of the secret (same as cert namespace). ns := gwapiv1.Namespace(cert.Namespace) listeners = append(listeners, gwapiv1.Listener{ Name: gwapiv1.SectionName(listenerName), Hostname: &hostname, Port: defaultPort, Protocol: gwapiv1.HTTPSProtocolType, AllowedRoutes: &gwapiv1.AllowedRoutes{ Namespaces: &gwapiv1.RouteNamespaces{ From: &namespacesFromAll, }, }, TLS: &gwapiv1.ListenerTLSConfig{ Mode: &tlsModeTerminate, CertificateRefs: []gwapiv1.SecretObjectReference{ { Group: ptrTo(gwapiv1.Group("")), Kind: ptrTo(gwapiv1.Kind("Secret")), Name: gwapiv1.ObjectName(secretName), Namespace: &ns, }, }, }, }) } } // Sort for deterministic output. sort.Slice(listeners, func(i, j int) bool { return string(listeners[i].Name) < string(listeners[j].Name) }) return listeners } // MergeListeners takes existing Gateway listeners and replaces all managed // (prefixed) listeners with the desired set, preserving user-defined listeners. func MergeListeners(existing, desired []gwapiv1.Listener, prefix string) []gwapiv1.Listener { var merged []gwapiv1.Listener // Keep all non-managed listeners. for _, l := range existing { if !strings.HasPrefix(string(l.Name), prefix) { merged = append(merged, l) } } // Append desired managed listeners. merged = append(merged, desired...) return merged } // ListenersEqual does a quick comparison of two listener slices by name+hostname+secretRef. // Not a deep equal — we only check the fields we manage. func ListenersEqual(a, b []gwapiv1.Listener) bool { if len(a) != len(b) { return false } aMap := listenerMap(a) bMap := listenerMap(b) if len(aMap) != len(bMap) { return false } for k, av := range aMap { bv, ok := bMap[k] if !ok || av != bv { return false } } return true } type listenerKey struct { name string hostname string secret string port gwapiv1.PortNumber } func listenerMap(listeners []gwapiv1.Listener) map[string]listenerKey { m := make(map[string]listenerKey, len(listeners)) for _, l := range listeners { key := listenerKey{ name: string(l.Name), port: l.Port, } if l.Hostname != nil { key.hostname = string(*l.Hostname) } if l.TLS != nil && len(l.TLS.CertificateRefs) > 0 { ref := l.TLS.CertificateRefs[0] ns := "" if ref.Namespace != nil { ns = string(*ref.Namespace) } key.secret = fmt.Sprintf("%s/%s", ns, ref.Name) } m[key.name] = key } return m } // listenerNameFor generates a deterministic listener name from cert name and DNS name. // Truncates to stay within Gateway API's 253-char limit for section names. func listenerNameFor(prefix, certName, dnsName string) string { // Replace dots with dashes for DNS names, combine with cert name. sanitized := strings.ReplaceAll(dnsName, ".", "-") name := fmt.Sprintf("%s%s-%s", prefix, certName, sanitized) if len(name) > 253 { name = name[:253] } return name } func ptrTo[T any](v T) *T { return &v }