Skip to content

Commit 39c1a4c

Browse files
authored
Merge branch 'main' into feat/envoy-billing-pipeline
2 parents b78b968 + fbf7acc commit 39c1a4c

15 files changed

Lines changed: 1187 additions & 4 deletions

File tree

.claude/settings.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"extraKnownMarketplaces": {
3+
"datum-claude-code-plugins": {
4+
"source": {
5+
"source": "github",
6+
"repo": "datum-cloud/claude-code-plugins"
7+
}
8+
}
9+
},
10+
"enabledPlugins": {
11+
"datum-platform@datum-claude-code-plugins": true,
12+
"datum-gtm@datum-claude-code-plugins": true
13+
}
14+
}

api/v1alpha/trafficprotectionpolicy_types.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ type OWASPCRS struct {
9191
// ParanoiaLevels specifies the OWASP ModSecurity Core Rule Set (CRS)
9292
// paranoia levels to use.
9393
//
94-
// +kubebuilder:default={}
94+
// +kubebuilder:default={blocking:1,detection:1}
9595
ParanoiaLevels ParanoiaLevels `json:"paranoiaLevels,omitempty"`
9696

9797
// ScoreThresholds specifies the OWASP ModSecurity Core Rule Set (CRS)
@@ -110,6 +110,7 @@ type OWASPCRS struct {
110110
RuleExclusions *OWASPRuleExclusions `json:"ruleExclusions,omitempty"`
111111
}
112112

113+
// +kubebuilder:validation:XValidation:message="detection paranoia level must be greater than or equal to blocking paranoia level",rule="self.detection >= self.blocking"
113114
type ParanoiaLevels struct {
114115
// Blocking specifies the paranoia level for blocking requests or responses.
115116
//

config/crd/bases/networking.datumapis.com_trafficprotectionpolicies.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ spec:
6767
Core Rule Set (CRS).
6868
properties:
6969
paranoiaLevels:
70-
default: {}
70+
default:
71+
blocking: 1
72+
detection: 1
7173
description: |-
7274
ParanoiaLevels specifies the OWASP ModSecurity Core Rule Set (CRS)
7375
paranoia levels to use.
@@ -89,6 +91,10 @@ spec:
8991
minimum: 1
9092
type: integer
9193
type: object
94+
x-kubernetes-validations:
95+
- message: detection paranoia level must be greater than
96+
or equal to blocking paranoia level
97+
rule: self.detection >= self.blocking
9298
ruleExclusions:
9399
description: |-
94100
RuleExclusions can be used to disable specific OWASP ModSecurity Rules.

docs/api/trafficprotectionpolicies.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ Core Rule Set (CRS).
191191
ParanoiaLevels specifies the OWASP ModSecurity Core Rule Set (CRS)
192192
paranoia levels to use.<br/>
193193
<br/>
194+
<i>Validations</i>:<li>self.detection >= self.blocking: detection paranoia level must be greater than or equal to blocking paranoia level</li>
194195
<i>Default</i>: map[]<br/>
195196
</td>
196197
<td>false</td>

internal/config/config.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,35 @@ type GatewayConfig struct {
710710
// When false: NSO emits ZERO EPPs and does NOT delete EPPs it did not create.
711711
// When true (default): EPP emission proceeds as today.
712712
EPPEmissionEnabled *bool `json:"eppEmissionEnabled,omitempty" yaml:"eppEmissionEnabled,omitempty"`
713+
714+
// CertificateReissuance controls how the gateway controller handles
715+
// failed certificate issuance for custom hostnames. When a Certificate
716+
// is stuck in a failed state, the controller deletes and recreates it
717+
// to bypass cert-manager's exponential backoff. Kubernetes GC cascades
718+
// the deletion through the entire chain (CertificateRequest, Order,
719+
// Challenge, solver resources).
720+
CertificateReissuance CertificateReissuanceConfig `json:"certificateReissuance,omitempty"`
721+
}
722+
723+
// +k8s:deepcopy-gen=true
724+
725+
// CertificateReissuanceConfig controls automatic recovery of failed certificate
726+
// issuance by deleting and recreating stuck Certificates.
727+
type CertificateReissuanceConfig struct {
728+
// RetryInterval is the minimum time to wait after a Certificate failure
729+
// before deleting it and recreating a fresh one. This prevents excessive
730+
// requests to the ACME provider when the underlying issue persists (e.g.
731+
// DNS not pointed to the Gateway).
732+
//
733+
// Defaults to 5m via GetRetryInterval().
734+
RetryInterval metav1.Duration `json:"retryInterval,omitempty"`
735+
736+
// MaxRetries is the maximum number of times the gateway controller will
737+
// fast-track re-issuance of a failed Certificate before falling back to
738+
// cert-manager's built-in exponential backoff.
739+
//
740+
// +default=3
741+
MaxRetries int `json:"maxRetries,omitempty"`
713742
}
714743

715744
// HasDefaultListenerTLSSecret returns true when a shared TLS certificate
@@ -738,6 +767,24 @@ func (c *GatewayConfig) IsEPPEmissionEnabled() bool {
738767
return *c.EPPEmissionEnabled
739768
}
740769

770+
// GetRetryInterval returns the configured retry interval for certificate
771+
// re-issuance, defaulting to 5 minutes.
772+
func (c *CertificateReissuanceConfig) GetRetryInterval() time.Duration {
773+
if c.RetryInterval.Duration > 0 {
774+
return c.RetryInterval.Duration
775+
}
776+
return 5 * time.Minute
777+
}
778+
779+
// GetMaxRetries returns the configured maximum number of fast-track
780+
// re-issuance attempts, defaulting to 3.
781+
func (c *CertificateReissuanceConfig) GetMaxRetries() int {
782+
if c.MaxRetries > 0 {
783+
return c.MaxRetries
784+
}
785+
return 3
786+
}
787+
741788
func (c *GatewayConfig) GatewayDNSAddress(gateway *gatewayv1.Gateway) string {
742789
seed := string(gateway.UID)
743790
suffix := fmt.Sprintf(".%s", c.TargetDomain)

internal/config/zz_generated.deepcopy.go

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/config/zz_generated.defaults.go

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/controller/gateway_controller.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
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.
6162
const autoIssuerSentinel = "auto"
63+
const annotationReissuanceCount = "networking.datumapis.com/reissuance-count"
6264
const KindGateway = "Gateway"
6365
const KindHTTPRoute = "HTTPRoute"
6466
const 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+
9711109
func (r *GatewayReconciler) reconcileGatewayStatus(
9721110
ctx context.Context,
9731111
upstreamClient client.Client,

0 commit comments

Comments
 (0)