77 "crypto/tls"
88 "fmt"
99 "slices"
10+ "strconv"
1011 "strings"
1112 "time"
1213
@@ -59,6 +60,7 @@ const certificateIssuerTLSOption = "gateway.networking.datumapis.com/certificate
5960// listenerTLSOptions. It means "use whichever real issuer the user's other
6061// TLS listener on this gateway specified" — see resolveAutoIssuer.
6162const autoIssuerSentinel = "auto"
63+ const annotationReissuanceCount = "networking.datumapis.com/reissuance-count"
6264const KindGateway = "Gateway"
6365const KindHTTPRoute = "HTTPRoute"
6466const KindService = "Service"
@@ -806,6 +808,7 @@ func (r *GatewayReconciler) ensureListenerCertificates(
806808 wildcardSuffix := "." + r .Config .Gateway .TargetDomain
807809
808810 desiredCerts := make (map [string ]bool )
811+ gatewayNeedsUpdate := false
809812
810813 // Wildcard-covered listeners are covered by the shared TLS secret only when
811814 // one is configured (DefaultListenerTLSSecretName). Otherwise they need a
@@ -913,6 +916,13 @@ func (r *GatewayReconciler) ensureListenerCertificates(
913916 var opResult string
914917 var err error
915918 if isNew {
919+ reissuanceCount := getReissuanceCount (downstreamGateway , certName )
920+ if reissuanceCount > 0 {
921+ if cert .Annotations == nil {
922+ cert .Annotations = make (map [string ]string )
923+ }
924+ cert .Annotations [annotationReissuanceCount ] = fmt .Sprintf ("%d" , reissuanceCount )
925+ }
916926 cert .Spec = desiredSpec
917927 err = downstreamClient .Create (ctx , cert )
918928 opResult = "created"
@@ -928,6 +938,27 @@ func (r *GatewayReconciler) ensureListenerCertificates(
928938 if opResult != "" {
929939 logger .Info ("Certificate reconciled" , "certificate" , certName , "operation" , opResult )
930940 }
941+
942+ // Check if an existing Certificate is stuck in a failed state and
943+ // should be deleted so we can recreate it fresh on the next reconcile.
944+ if ! isNew {
945+ requeueAfter , gwChanged := r .reissueFailedCertificate (ctx , cert , certName , downstreamGateway , downstreamClient )
946+ if gwChanged {
947+ gatewayNeedsUpdate = true
948+ }
949+ if requeueAfter > 0 && (result .RequeueAfter == 0 || requeueAfter < result .RequeueAfter ) {
950+ result .RequeueAfter = requeueAfter
951+ }
952+ }
953+ }
954+
955+ // Persist reissuance tracking annotations on the downstream Gateway if
956+ // any Certificates were deleted for re-issuance.
957+ if gatewayNeedsUpdate {
958+ if err := downstreamClient .Update (ctx , downstreamGateway ); err != nil {
959+ result .Err = fmt .Errorf ("failed to update downstream gateway reissuance annotations: %w" , err )
960+ return result
961+ }
931962 }
932963
933964 // Clean up Certificate resources for listeners that no longer need them.
@@ -968,6 +999,113 @@ func (r *GatewayReconciler) ensureListenerCertificates(
968999 return result
9691000}
9701001
1002+ // reissueFailedCertificate checks whether a Certificate is stuck in a failed
1003+ // state and should be deleted so the gateway controller recreates it fresh on
1004+ // the next reconcile (bypassing cert-manager's exponential backoff).
1005+ //
1006+ // The reissuance count is tracked as an annotation on the downstream Gateway
1007+ // (keyed by certificate name) so it survives Certificate deletion.
1008+ //
1009+ // Returns:
1010+ // - requeueAfter: non-zero if the caller should requeue after this duration
1011+ // - gatewayChanged: true if the downstream gateway annotations were modified
1012+ // and the caller must persist the change
1013+ func (r * GatewayReconciler ) reissueFailedCertificate (
1014+ ctx context.Context ,
1015+ cert * cmv1.Certificate ,
1016+ certName string ,
1017+ downstreamGateway * gatewayv1.Gateway ,
1018+ downstreamClient client.Client ,
1019+ ) (requeueAfter time.Duration , gatewayChanged bool ) {
1020+ logger := log .FromContext (ctx )
1021+ reissuanceCfg := & r .Config .Gateway .CertificateReissuance
1022+
1023+ if cert .Status .LastFailureTime == nil {
1024+ if clearReissuanceCount (downstreamGateway , certName ) {
1025+ logger .V (1 ).Info ("cleared reissuance count for healthy Certificate" , "certificate" , certName )
1026+ gatewayChanged = true
1027+ }
1028+ return 0 , gatewayChanged
1029+ }
1030+
1031+ retryCount := getReissuanceCount (downstreamGateway , certName )
1032+ maxRetries := reissuanceCfg .GetMaxRetries ()
1033+ if retryCount >= maxRetries {
1034+ logger .V (1 ).Info ("Certificate has exhausted fast-track re-issuance attempts, deferring to cert-manager backoff" ,
1035+ "certificate" , certName ,
1036+ "reissuanceCount" , retryCount ,
1037+ "maxRetries" , maxRetries ,
1038+ )
1039+ return 0 , false
1040+ }
1041+
1042+ retryInterval := reissuanceCfg .GetRetryInterval ()
1043+ sinceFailure := time .Since (cert .Status .LastFailureTime .Time )
1044+ if sinceFailure < retryInterval {
1045+ remaining := retryInterval - sinceFailure
1046+ logger .V (1 ).Info ("Certificate failed but retry interval has not elapsed" ,
1047+ "certificate" , certName ,
1048+ "sinceFailure" , sinceFailure ,
1049+ "retryInterval" , retryInterval ,
1050+ "requeueAfter" , remaining ,
1051+ )
1052+ return remaining , false
1053+ }
1054+
1055+ setReissuanceCount (downstreamGateway , certName , retryCount + 1 )
1056+
1057+ logger .Info ("deleting failed Certificate for re-issuance" ,
1058+ "certificate" , certName ,
1059+ "lastFailureTime" , cert .Status .LastFailureTime .Time ,
1060+ "reissuanceCount" , retryCount + 1 ,
1061+ "maxRetries" , maxRetries ,
1062+ )
1063+
1064+ if err := downstreamClient .Delete (ctx , cert ); err != nil {
1065+ if ! apierrors .IsNotFound (err ) {
1066+ logger .Error (err , "failed to delete Certificate for re-issuance" , "certificate" , certName )
1067+ }
1068+ return 0 , true
1069+ }
1070+
1071+ return 0 , true
1072+ }
1073+
1074+ // reissuanceAnnotationKey returns the gateway annotation key used to track
1075+ // reissuance count for a given certificate name.
1076+ func reissuanceAnnotationKey (certName string ) string {
1077+ return annotationReissuanceCount + "/" + certName
1078+ }
1079+
1080+ func getReissuanceCount (gw * gatewayv1.Gateway , certName string ) int {
1081+ if gw .Annotations == nil {
1082+ return 0
1083+ }
1084+ count , err := strconv .Atoi (gw .Annotations [reissuanceAnnotationKey (certName )])
1085+ if err != nil {
1086+ return 0
1087+ }
1088+ return count
1089+ }
1090+
1091+ func setReissuanceCount (gw * gatewayv1.Gateway , certName string , count int ) {
1092+ if gw .Annotations == nil {
1093+ gw .Annotations = make (map [string ]string )
1094+ }
1095+ gw .Annotations [reissuanceAnnotationKey (certName )] = fmt .Sprintf ("%d" , count )
1096+ }
1097+
1098+ // clearReissuanceCount removes the reissuance tracking annotation for a
1099+ // certificate that has recovered. Returns true if the annotation was present.
1100+ func clearReissuanceCount (gw * gatewayv1.Gateway , certName string ) bool {
1101+ key := reissuanceAnnotationKey (certName )
1102+ if _ , ok := gw .Annotations [key ]; ok {
1103+ delete (gw .Annotations , key )
1104+ return true
1105+ }
1106+ return false
1107+ }
1108+
9711109func (r * GatewayReconciler ) reconcileGatewayStatus (
9721110 ctx context.Context ,
9731111 upstreamClient client.Client ,
0 commit comments