package gateway import ( "crypto/sha256" "encoding/hex" "fmt" "sort" "strings" cmv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" ) const defaultPort gwapiv1.PortNumber = 443 // 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: ptrTo(gwapiv1.NamespacesFromAll), }, }, TLS: &gwapiv1.ListenerTLSConfig{ Mode: ptrTo(gwapiv1.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 } // maxSectionNameLen is the Gateway API limit for listener names. const maxSectionNameLen = 253 // listenerNameFor generates a deterministic listener name from cert name and DNS name. // The result is a valid Gateway API SectionName: names that would exceed the limit // are cut and get a hash of the full name appended, so distinct long names stay distinct. func listenerNameFor(prefix, certName, dnsName string) string { name := strings.TrimRight(fmt.Sprintf("%s%s-%s", prefix, certName, sanitizeDNSName(dnsName)), "-.") if len(name) <= maxSectionNameLen { return name } sum := sha256.Sum256([]byte(name)) suffix := "-" + hex.EncodeToString(sum[:4]) return strings.TrimRight(name[:maxSectionNameLen-len(suffix)], "-.") + suffix } // sanitizeDNSName maps a certificate DNS name onto the characters allowed in a // listener name. A wildcard label is spelled out so "*.example.com" and // "example.com" stay distinct. func sanitizeDNSName(dnsName string) string { var b strings.Builder for _, r := range strings.ToLower(dnsName) { switch { case r >= 'a' && r <= 'z', r >= '0' && r <= '9': b.WriteRune(r) case r == '*': b.WriteString("wildcard") default: b.WriteByte('-') } } return strings.Trim(b.String(), "-") } func ptrTo[T any](v T) *T { return &v }