Skip to content

Commit a71ac1f

Browse files
authored
Fix policy attachment when ancestors slice is full (#3698)
Problem: Multiple bugs when ancestor slice was full: BackendTLSPolicy became invalid NGF Policies would not partially attach policies to services when multiple ancestor refs were created from a single service but attach to the whole service No condition was added to ancestor No logs on controller Solution: Added conditions to ancestor and logs to controller. I fixed BackendTLSPolicy to add attachments until ancestors became full and fixed NGF policies as well
1 parent 9da888b commit a71ac1f

14 files changed

+1195
-144
lines changed

internal/controller/state/change_processor.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ func (c *ChangeProcessorImpl) Process() *graph.Graph {
268268
c.cfg.GatewayClassName,
269269
c.cfg.PlusSecrets,
270270
c.cfg.Validators,
271+
c.cfg.Logger,
271272
)
272273

273274
return c.latestGraph

internal/controller/state/conditions/conditions.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,15 @@ const (
138138
// GatewayReasonParamsRefInvalid is used with the "GatewayResolvedRefs" condition when the
139139
// parametersRef resource is invalid.
140140
GatewayReasonParamsRefInvalid v1.GatewayConditionReason = "ParametersRefInvalid"
141+
142+
// PolicyReasonAncestorLimitReached is used with the "PolicyAccepted" condition when a policy
143+
// cannot be applied because the ancestor status list has reached the maximum size of 16.
144+
PolicyReasonAncestorLimitReached v1alpha2.PolicyConditionReason = "AncestorLimitReached"
145+
146+
// PolicyMessageAncestorLimitReached is a message used with PolicyReasonAncestorLimitReached
147+
// when a policy cannot be applied due to the ancestor limit being reached.
148+
PolicyMessageAncestorLimitReached = "Policies cannot be applied because the ancestor status list " +
149+
"has reached the maximum size. The following policies have been ignored:"
141150
)
142151

143152
// Condition defines a condition to be reported in the status of resources.
@@ -968,6 +977,17 @@ func NewPolicyTargetNotFound(msg string) Condition {
968977
}
969978
}
970979

980+
// NewPolicyAncestorLimitReached returns a Condition that indicates that the Policy is not accepted because
981+
// the ancestor status list has reached the maximum size of 16.
982+
func NewPolicyAncestorLimitReached(policyType string, policyName string) Condition {
983+
return Condition{
984+
Type: string(v1alpha2.PolicyConditionAccepted),
985+
Status: metav1.ConditionFalse,
986+
Reason: string(PolicyReasonAncestorLimitReached),
987+
Message: fmt.Sprintf("%s %s %s", PolicyMessageAncestorLimitReached, policyType, policyName),
988+
}
989+
}
990+
971991
// NewPolicyNotAcceptedTargetConflict returns a Condition that indicates that the Policy is not accepted
972992
// because the target resource has a conflict with another resource when attempting to apply this policy.
973993
func NewPolicyNotAcceptedTargetConflict(msg string) Condition {

internal/controller/state/dataplane/configuration.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ func newBackendGroup(
390390
UpstreamName: ref.ServicePortReference(),
391391
Weight: ref.Weight,
392392
Valid: valid,
393-
VerifyTLS: convertBackendTLS(ref.BackendTLSPolicy),
393+
VerifyTLS: convertBackendTLS(ref.BackendTLSPolicy, gatewayName),
394394
})
395395
}
396396

@@ -401,10 +401,15 @@ func newBackendGroup(
401401
}
402402
}
403403

404-
func convertBackendTLS(btp *graph.BackendTLSPolicy) *VerifyTLS {
404+
func convertBackendTLS(btp *graph.BackendTLSPolicy, gwNsName types.NamespacedName) *VerifyTLS {
405405
if btp == nil || !btp.Valid {
406406
return nil
407407
}
408+
409+
if !slices.Contains(btp.Gateways, gwNsName) {
410+
return nil
411+
}
412+
408413
verify := &VerifyTLS{}
409414
if btp.CaCertRef.Name != "" {
410415
verify.CertBundleID = generateCertBundleID(btp.CaCertRef)

internal/controller/state/dataplane/configuration_test.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,7 @@ func TestBuildConfiguration(t *testing.T) {
607607
},
608608
CaCertRef: types.NamespacedName{Namespace: "test", Name: "configmap-1"},
609609
Valid: true,
610+
Gateways: []types.NamespacedName{gatewayNsName},
610611
}
611612

612613
expHTTPSHR8Groups[0].Backends[0].VerifyTLS = &VerifyTLS{
@@ -661,6 +662,7 @@ func TestBuildConfiguration(t *testing.T) {
661662
},
662663
CaCertRef: types.NamespacedName{Namespace: "test", Name: "configmap-2"},
663664
Valid: true,
665+
Gateways: []types.NamespacedName{gatewayNsName},
664666
}
665667

666668
expHTTPSHR9Groups[0].Backends[0].VerifyTLS = &VerifyTLS{
@@ -3569,6 +3571,9 @@ func TestHostnameMoreSpecific(t *testing.T) {
35693571

35703572
func TestConvertBackendTLS(t *testing.T) {
35713573
t.Parallel()
3574+
3575+
testGateway := types.NamespacedName{Namespace: "test", Name: "gateway"}
3576+
35723577
btpCaCertRefs := &graph.BackendTLSPolicy{
35733578
Source: &v1alpha3.BackendTLSPolicy{
35743579
ObjectMeta: metav1.ObjectMeta{
@@ -3588,6 +3593,7 @@ func TestConvertBackendTLS(t *testing.T) {
35883593
},
35893594
Valid: true,
35903595
CaCertRef: types.NamespacedName{Namespace: "test", Name: "ca-cert"},
3596+
Gateways: []types.NamespacedName{testGateway},
35913597
}
35923598

35933599
btpWellKnownCerts := &graph.BackendTLSPolicy{
@@ -3598,7 +3604,8 @@ func TestConvertBackendTLS(t *testing.T) {
35983604
},
35993605
},
36003606
},
3601-
Valid: true,
3607+
Valid: true,
3608+
Gateways: []types.NamespacedName{testGateway},
36023609
}
36033610

36043611
expectedWithCertPath := &VerifyTLS{
@@ -3615,31 +3622,41 @@ func TestConvertBackendTLS(t *testing.T) {
36153622

36163623
tests := []struct {
36173624
btp *graph.BackendTLSPolicy
3625+
gwNsName types.NamespacedName
36183626
expected *VerifyTLS
36193627
msg string
36203628
}{
36213629
{
36223630
btp: nil,
3631+
gwNsName: testGateway,
36233632
expected: nil,
36243633
msg: "nil backend tls policy",
36253634
},
36263635
{
36273636
btp: btpCaCertRefs,
3637+
gwNsName: testGateway,
36283638
expected: expectedWithCertPath,
36293639
msg: "normal case with cert path",
36303640
},
36313641
{
36323642
btp: btpWellKnownCerts,
3643+
gwNsName: testGateway,
36333644
expected: expectedWithWellKnownCerts,
36343645
msg: "normal case no cert path",
36353646
},
3647+
{
3648+
btp: btpCaCertRefs,
3649+
gwNsName: types.NamespacedName{Namespace: "test", Name: "unsupported-gateway"},
3650+
expected: nil,
3651+
msg: "gateway not supported by policy",
3652+
},
36363653
}
36373654

36383655
for _, tc := range tests {
36393656
t.Run(tc.msg, func(t *testing.T) {
36403657
t.Parallel()
36413658
g := NewWithT(t)
3642-
g.Expect(convertBackendTLS(tc.btp)).To(Equal(tc.expected))
3659+
g.Expect(convertBackendTLS(tc.btp, tc.gwNsName)).To(Equal(tc.expected))
36433660
})
36443661
}
36453662
}

internal/controller/state/graph/backend_tls_policy.go

Lines changed: 136 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package graph
33
import (
44
"fmt"
55
"slices"
6+
"strings"
67

8+
"github.com/go-logr/logr"
79
"k8s.io/apimachinery/pkg/types"
810
"k8s.io/apimachinery/pkg/util/validation/field"
911
v1 "sigs.k8s.io/gateway-api/apis/v1"
@@ -18,7 +20,8 @@ type BackendTLSPolicy struct {
1820
Source *v1alpha3.BackendTLSPolicy
1921
// CaCertRef is the name of the ConfigMap that contains the CA certificate.
2022
CaCertRef types.NamespacedName
21-
// Gateways are the names of the Gateways that are being checked for this BackendTLSPolicy.
23+
// Gateways are the names of the Gateways for which this BackendTLSPolicy is effectively applied.
24+
// Only contains gateways where the policy can be applied (not limited by ancestor status).
2225
Gateways []types.NamespacedName
2326
// Conditions include Conditions for the BackendTLSPolicy.
2427
Conditions []conditions.Condition
@@ -34,7 +37,6 @@ func processBackendTLSPolicies(
3437
backendTLSPolicies map[types.NamespacedName]*v1alpha3.BackendTLSPolicy,
3538
configMapResolver *configMapResolver,
3639
secretResolver *secretResolver,
37-
ctlrName string,
3840
gateways map[types.NamespacedName]*Gateway,
3941
) map[types.NamespacedName]*BackendTLSPolicy {
4042
if len(backendTLSPolicies) == 0 || len(gateways) == 0 {
@@ -45,7 +47,7 @@ func processBackendTLSPolicies(
4547
for nsname, backendTLSPolicy := range backendTLSPolicies {
4648
var caCertRef types.NamespacedName
4749

48-
valid, ignored, conds := validateBackendTLSPolicy(backendTLSPolicy, configMapResolver, secretResolver, ctlrName)
50+
valid, ignored, conds := validateBackendTLSPolicy(backendTLSPolicy, configMapResolver, secretResolver)
4951

5052
if valid && !ignored && backendTLSPolicy.Spec.Validation.CACertificateRefs != nil {
5153
caCertRef = types.NamespacedName{
@@ -68,17 +70,10 @@ func validateBackendTLSPolicy(
6870
backendTLSPolicy *v1alpha3.BackendTLSPolicy,
6971
configMapResolver *configMapResolver,
7072
secretResolver *secretResolver,
71-
ctlrName string,
7273
) (valid, ignored bool, conds []conditions.Condition) {
7374
valid = true
7475
ignored = false
7576

76-
// FIXME (kate-osborn): https://github.com/nginx/nginx-gateway-fabric/issues/1987
77-
if backendTLSPolicyAncestorsFull(backendTLSPolicy.Status.Ancestors, ctlrName) {
78-
valid = false
79-
ignored = true
80-
}
81-
8277
if err := validateBackendTLSHostname(backendTLSPolicy); err != nil {
8378
valid = false
8479
conds = append(conds, conditions.NewPolicyInvalid(fmt.Sprintf("invalid hostname: %s", err.Error())))
@@ -183,31 +178,148 @@ func validateBackendTLSWellKnownCACerts(btp *v1alpha3.BackendTLSPolicy) error {
183178
return nil
184179
}
185180

181+
// countNonNGFAncestors counts the number of non-NGF ancestors in policy status.
182+
func countNonNGFAncestors(policy *v1alpha3.BackendTLSPolicy, ctlrName string) int {
183+
nonNGFCount := 0
184+
for _, ancestor := range policy.Status.Ancestors {
185+
if string(ancestor.ControllerName) != ctlrName {
186+
nonNGFCount++
187+
}
188+
}
189+
return nonNGFCount
190+
}
191+
192+
// addPolicyAncestorLimitCondition adds or updates a PolicyAncestorLimitReached condition.
193+
func addPolicyAncestorLimitCondition(
194+
conds []conditions.Condition,
195+
policyName string,
196+
policyType string,
197+
) []conditions.Condition {
198+
for i, condition := range conds {
199+
if condition.Reason == string(conditions.PolicyReasonAncestorLimitReached) {
200+
if !strings.Contains(condition.Message, policyName) {
201+
conds[i].Message = fmt.Sprintf("%s, %s %s", condition.Message, policyType, policyName)
202+
}
203+
return conds
204+
}
205+
}
206+
207+
newCondition := conditions.NewPolicyAncestorLimitReached(policyType, policyName)
208+
return append(conds, newCondition)
209+
}
210+
211+
// collectOrderedGateways collects gateways in spec order (services) then creation time order (gateways within service).
212+
func collectOrderedGateways(
213+
policy *v1alpha3.BackendTLSPolicy,
214+
services map[types.NamespacedName]*ReferencedService,
215+
gateways map[types.NamespacedName]*Gateway,
216+
existingNGFGatewayAncestors map[types.NamespacedName]struct{},
217+
) []types.NamespacedName {
218+
seenGateways := make(map[types.NamespacedName]struct{})
219+
existingGateways := make([]types.NamespacedName, 0)
220+
newGateways := make([]types.NamespacedName, 0)
221+
222+
// Process services in spec order to maintain deterministic gateway ordering
223+
for _, refs := range policy.Spec.TargetRefs {
224+
if refs.Kind != kinds.Service {
225+
continue
226+
}
227+
228+
svcNsName := types.NamespacedName{
229+
Namespace: policy.Namespace,
230+
Name: string(refs.Name),
231+
}
232+
233+
referencedService, exists := services[svcNsName]
234+
if !exists {
235+
continue
236+
}
237+
238+
// Add to ordered lists, categorizing existing vs new, skipping duplicates
239+
for gateway := range referencedService.GatewayNsNames {
240+
if _, seen := seenGateways[gateway]; seen {
241+
continue
242+
}
243+
seenGateways[gateway] = struct{}{}
244+
if _, exists := existingNGFGatewayAncestors[gateway]; exists {
245+
existingGateways = append(existingGateways, gateway)
246+
} else {
247+
newGateways = append(newGateways, gateway)
248+
}
249+
}
250+
}
251+
252+
sortGatewaysByCreationTime(existingGateways, gateways)
253+
sortGatewaysByCreationTime(newGateways, gateways)
254+
255+
return append(existingGateways, newGateways...)
256+
}
257+
258+
func extractExistingNGFGatewayAncestors(
259+
backendTLSPolicy *v1alpha3.BackendTLSPolicy,
260+
ctlrName string,
261+
) map[types.NamespacedName]struct{} {
262+
existingNGFGatewayAncestors := make(map[types.NamespacedName]struct{})
263+
264+
for _, ancestor := range backendTLSPolicy.Status.Ancestors {
265+
if string(ancestor.ControllerName) != ctlrName {
266+
continue
267+
}
268+
269+
if ancestor.AncestorRef.Kind != nil && *ancestor.AncestorRef.Kind == v1.Kind(kinds.Gateway) &&
270+
ancestor.AncestorRef.Namespace != nil {
271+
gatewayNsName := types.NamespacedName{
272+
Namespace: string(*ancestor.AncestorRef.Namespace),
273+
Name: string(ancestor.AncestorRef.Name),
274+
}
275+
existingNGFGatewayAncestors[gatewayNsName] = struct{}{}
276+
}
277+
}
278+
279+
return existingNGFGatewayAncestors
280+
}
281+
186282
func addGatewaysForBackendTLSPolicies(
187283
backendTLSPolicies map[types.NamespacedName]*BackendTLSPolicy,
188284
services map[types.NamespacedName]*ReferencedService,
285+
ctlrName string,
286+
gateways map[types.NamespacedName]*Gateway,
287+
logger logr.Logger,
189288
) {
190289
for _, backendTLSPolicy := range backendTLSPolicies {
191-
gateways := make(map[types.NamespacedName]struct{})
290+
existingNGFGatewayAncestors := extractExistingNGFGatewayAncestors(backendTLSPolicy.Source, ctlrName)
291+
orderedGateways := collectOrderedGateways(
292+
backendTLSPolicy.Source,
293+
services,
294+
gateways,
295+
existingNGFGatewayAncestors,
296+
)
192297

193-
for _, refs := range backendTLSPolicy.Source.Spec.TargetRefs {
194-
if refs.Kind != kinds.Service {
195-
continue
196-
}
298+
ancestorCount := countNonNGFAncestors(backendTLSPolicy.Source, ctlrName)
197299

198-
for svcNsName, referencedServices := range services {
199-
if svcNsName.Name != string(refs.Name) {
200-
continue
201-
}
300+
// Process each gateway, respecting ancestor limits
301+
for _, gatewayNsName := range orderedGateways {
302+
// Check if adding this gateway would exceed the ancestor limit
303+
if ancestorCount >= maxAncestors {
304+
policyName := backendTLSPolicy.Source.Namespace + "/" + backendTLSPolicy.Source.Name
305+
proposedAncestor := createParentReference(v1.GroupName, kinds.Gateway, gatewayNsName)
306+
gatewayName := getAncestorName(proposedAncestor)
202307

203-
for gateway := range referencedServices.GatewayNsNames {
204-
gateways[gateway] = struct{}{}
308+
if gateway, ok := gateways[gatewayNsName]; ok {
309+
gateway.Conditions = addPolicyAncestorLimitCondition(gateway.Conditions, policyName, kinds.BackendTLSPolicy)
310+
} else {
311+
// This should never happen, but we'll log it if it does
312+
logger.Error(fmt.Errorf("gateway not found in the graph"),
313+
"Gateway not found in the graph", "policy", policyName, "ancestor", gatewayName)
205314
}
315+
316+
logAncestorLimitReached(logger, policyName, "BackendTLSPolicy", gatewayName)
317+
continue
206318
}
207-
}
208319

209-
for gateway := range gateways {
210-
backendTLSPolicy.Gateways = append(backendTLSPolicy.Gateways, gateway)
320+
ancestorCount++
321+
322+
backendTLSPolicy.Gateways = append(backendTLSPolicy.Gateways, gatewayNsName)
211323
}
212324
}
213325
}

0 commit comments

Comments
 (0)