diff --git a/internal/controller/state/conditions/conditions.go b/internal/controller/state/conditions/conditions.go index b9ec5f10cf..6630e69a28 100644 --- a/internal/controller/state/conditions/conditions.go +++ b/internal/controller/state/conditions/conditions.go @@ -107,6 +107,18 @@ const ( // has an overlapping hostname:port/path combination with another Route. PolicyReasonTargetConflict v1alpha2.PolicyConditionReason = "TargetConflict" + // ClientSettingsPolicyAffected is used with the "PolicyAffected" condition when a + // ClientSettingsPolicy is applied to a Gateway, HTTPRoute, or GRPCRoute. + ClientSettingsPolicyAffected v1alpha2.PolicyConditionType = "ClientSettingsPolicyAffected" + + // ObservabilityPolicyAffected is used with the "PolicyAffected" condition when an + // ObservabilityPolicy is applied to a HTTPRoute, or GRPCRoute. + ObservabilityPolicyAffected v1alpha2.PolicyConditionType = "ObservabilityPolicyAffected" + + // PolicyAffectedReason is used with the "PolicyAffected" condition when a + // ObservabilityPolicy or ClientSettingsPolicy is applied to Gateways or Routes. + PolicyAffectedReason v1alpha2.PolicyConditionReason = "PolicyAffected" + // GatewayResolvedRefs condition indicates whether the controller was able to resolve the // parametersRef on the Gateway. GatewayResolvedRefs v1.GatewayConditionType = "ResolvedRefs" @@ -185,6 +197,19 @@ func ConvertConditions( return apiConds } +// HasMatchingCondition checks if the given condition matches any of the existing conditions. +func HasMatchingCondition(existingConditions []Condition, cond Condition) bool { + for _, existing := range existingConditions { + if existing.Type == cond.Type && + existing.Status == cond.Status && + existing.Reason == cond.Reason && + existing.Message == cond.Message { + return true + } + } + return false +} + // NewDefaultGatewayClassConditions returns Conditions that indicate that the GatewayClass is accepted and that the // Gateway API CRD versions are supported. func NewDefaultGatewayClassConditions() []Condition { @@ -940,3 +965,25 @@ func NewSnippetsFilterAccepted() Condition { Message: "SnippetsFilter is accepted", } } + +// NewObservabilityPolicyAffected returns a Condition that indicates that an ObservabilityPolicy +// is applied to the resource. +func NewObservabilityPolicyAffected() Condition { + return Condition{ + Type: string(ObservabilityPolicyAffected), + Status: metav1.ConditionTrue, + Reason: string(PolicyAffectedReason), + Message: "ObservabilityPolicy is applied to the resource", + } +} + +// NewClientSettingsPolicyAffected returns a Condition that indicates that a ClientSettingsPolicy +// is applied to the resource. +func NewClientSettingsPolicyAffected() Condition { + return Condition{ + Type: string(ClientSettingsPolicyAffected), + Status: metav1.ConditionTrue, + Reason: string(PolicyAffectedReason), + Message: "ClientSettingsPolicy is applied to the resource", + } +} diff --git a/internal/controller/state/conditions/conditions_test.go b/internal/controller/state/conditions/conditions_test.go index b446a6ecaf..45ebcc5808 100644 --- a/internal/controller/state/conditions/conditions_test.go +++ b/internal/controller/state/conditions/conditions_test.go @@ -105,3 +105,43 @@ func TestConvertConditions(t *testing.T) { result := ConvertConditions(conds, generation, time) g.Expect(result).Should(Equal(expected)) } + +func TestHasMatchingCondition(t *testing.T) { + t.Parallel() + + tests := []struct { + condition Condition + name string + conds []Condition + expected bool + }{ + { + name: "no conditions in the list", + conds: nil, + condition: NewClientSettingsPolicyAffected(), + expected: false, + }, + { + name: "condition matches existing condition", + conds: []Condition{NewClientSettingsPolicyAffected()}, + condition: NewClientSettingsPolicyAffected(), + expected: true, + }, + { + name: "condition does not match existing condition", + conds: []Condition{NewClientSettingsPolicyAffected()}, + condition: NewObservabilityPolicyAffected(), + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + result := HasMatchingCondition(test.conds, test.condition) + g.Expect(result).To(Equal(test.expected)) + }) + } +} diff --git a/internal/controller/state/graph/graph.go b/internal/controller/state/graph/graph.go index 12017112e0..a20615259e 100644 --- a/internal/controller/state/graph/graph.go +++ b/internal/controller/state/graph/graph.go @@ -280,6 +280,9 @@ func BuildGraph( gws, ) + // add status conditions to each targetRef based on the policies that affect them. + addPolicyAffectedStatusToTargetRefs(processedPolicies, routes, gws) + setPlusSecretContent(state.Secrets, plusSecrets) g := &Graph{ diff --git a/internal/controller/state/graph/graph_test.go b/internal/controller/state/graph/graph_test.go index 748a3aa20c..e871b3c4ae 100644 --- a/internal/controller/state/graph/graph_test.go +++ b/internal/controller/state/graph/graph_test.go @@ -765,6 +765,9 @@ func TestBuildGraph(t *testing.T) { Rules: []RouteRule{createValidRuleWithBackendRefsAndFilters(routeMatches, RouteTypeHTTP)}, }, Policies: []*Policy{processedRoutePolicy}, + Conditions: []conditions.Condition{ + conditions.NewClientSettingsPolicyAffected(), + }, } routeTR := &L4Route{ @@ -1080,7 +1083,10 @@ func TestBuildGraph(t *testing.T) { ErrorLevel: helpers.GetPointer(ngfAPIv1alpha2.NginxLogLevelError), }, }, - Conditions: []conditions.Condition{conditions.NewGatewayResolvedRefs()}, + Conditions: []conditions.Condition{ + conditions.NewGatewayResolvedRefs(), + conditions.NewClientSettingsPolicyAffected(), + }, DeploymentName: types.NamespacedName{ Namespace: "test", Name: "gateway-1-my-class", diff --git a/internal/controller/state/graph/policies.go b/internal/controller/state/graph/policies.go index b5a1251aee..509e41f2a1 100644 --- a/internal/controller/state/graph/policies.go +++ b/internal/controller/state/graph/policies.go @@ -448,3 +448,60 @@ func refGroupKind(group v1.Group, kind v1.Kind) string { return fmt.Sprintf("%s/%s", group, kind) } + +// addPolicyAffectedStatusToTargetRefs adds the policyAffected status to the target references +// of ClientSettingsPolicies and ObservabilityPolicies. +func addPolicyAffectedStatusToTargetRefs( + processedPolicies map[PolicyKey]*Policy, + routes map[RouteKey]*L7Route, + gws map[types.NamespacedName]*Gateway, +) { + for policyKey, policy := range processedPolicies { + for _, ref := range policy.TargetRefs { + switch ref.Kind { + case kinds.Gateway: + if !gatewayExists(ref.Nsname, gws) { + continue + } + gw := gws[ref.Nsname] + if gw == nil { + continue + } + + // set the policy status on the Gateway. + policyKind := policyKey.GVK.Kind + addStatusToTargetRefs(policyKind, &gw.Conditions) + case kinds.HTTPRoute, kinds.GRPCRoute: + routeKey := routeKeyForKind(ref.Kind, ref.Nsname) + l7route, exists := routes[routeKey] + if !exists { + continue + } + + // set the policy status on L7 routes. + policyKind := policyKey.GVK.Kind + addStatusToTargetRefs(policyKind, &l7route.Conditions) + default: + continue + } + } + } +} + +func addStatusToTargetRefs(policyKind string, conditionsList *[]conditions.Condition) { + if conditionsList == nil { + return + } + switch policyKind { + case kinds.ObservabilityPolicy: + if conditions.HasMatchingCondition(*conditionsList, conditions.NewObservabilityPolicyAffected()) { + return + } + *conditionsList = append(*conditionsList, conditions.NewObservabilityPolicyAffected()) + case kinds.ClientSettingsPolicy: + if conditions.HasMatchingCondition(*conditionsList, conditions.NewClientSettingsPolicyAffected()) { + return + } + *conditionsList = append(*conditionsList, conditions.NewClientSettingsPolicyAffected()) + } +} diff --git a/internal/controller/state/graph/policies_test.go b/internal/controller/state/graph/policies_test.go index 7df4a9d432..18be2262e8 100644 --- a/internal/controller/state/graph/policies_test.go +++ b/internal/controller/state/graph/policies_test.go @@ -1544,6 +1544,14 @@ func createTestRef(kind v1.Kind, group v1.Group, name string) v1alpha2.LocalPoli } } +func createTestPolicyTargetRef(kind v1.Kind, nsname types.NamespacedName) PolicyTargetRef { + return PolicyTargetRef{ + Kind: kind, + Group: v1.GroupName, + Nsname: nsname, + } +} + func createTestRouteWithPaths(name string, paths ...string) *L7Route { routeMatches := make([]v1.HTTPRouteMatch, 0, len(paths)) @@ -1589,3 +1597,370 @@ func getGatewayParentRef(gwNsName types.NamespacedName) v1.ParentReference { Name: v1.ObjectName(gwNsName.Name), } } + +func createGatewayMap(gwNsNames ...types.NamespacedName) map[types.NamespacedName]*Gateway { + gatewayMap := make(map[types.NamespacedName]*Gateway, len(gwNsNames)) + for _, gwNsName := range gwNsNames { + gatewayMap[gwNsName] = &Gateway{ + Source: &v1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: gwNsName.Name, + Namespace: gwNsName.Namespace, + }, + }, + Valid: true, + } + } + + return gatewayMap +} + +func TestAddPolicyAffectedStatusOnTargetRefs(t *testing.T) { + t.Parallel() + + cspGVK := schema.GroupVersionKind{Group: "Group", Version: "Version", Kind: "ClientSettingsPolicy"} + opGVK := schema.GroupVersionKind{Group: "Group", Version: "Version", Kind: "ObservabilityPolicy"} + + gw1Ref := createTestRef(kinds.Gateway, v1.GroupName, "gw1") + gw1TargetRef := createTestPolicyTargetRef( + kinds.Gateway, + types.NamespacedName{Namespace: testNs, Name: "gw1"}, + ) + gw2Ref := createTestRef(kinds.Gateway, v1.GroupName, "gw2") + gw2TargetRef := createTestPolicyTargetRef( + kinds.Gateway, + types.NamespacedName{Namespace: testNs, Name: "gw2"}, + ) + gw3Ref := createTestRef(kinds.Gateway, v1.GroupName, "gw3") + gw3TargetRef := createTestPolicyTargetRef( + kinds.Gateway, + types.NamespacedName{Namespace: testNs, Name: "gw3"}, + ) + + hr1Ref := createTestRef(kinds.HTTPRoute, v1.GroupName, "hr1") + hr1TargetRef := createTestPolicyTargetRef( + kinds.HTTPRoute, + types.NamespacedName{Namespace: testNs, Name: "hr1"}, + ) + hr2Ref := createTestRef(kinds.HTTPRoute, v1.GroupName, "hr2") + hr2TargetRef := createTestPolicyTargetRef( + kinds.HTTPRoute, + types.NamespacedName{Namespace: testNs, Name: "hr2"}, + ) + hr3Ref := createTestRef(kinds.HTTPRoute, v1.GroupName, "hr3") + hr3TargetRef := createTestPolicyTargetRef( + kinds.HTTPRoute, + types.NamespacedName{Namespace: testNs, Name: "hr3"}, + ) + + gr1Ref := createTestRef(kinds.GRPCRoute, v1.GroupName, "gr1") + gr1TargetRef := createTestPolicyTargetRef( + kinds.GRPCRoute, + types.NamespacedName{Namespace: testNs, Name: "gr1"}, + ) + gr2Ref := createTestRef(kinds.GRPCRoute, v1.GroupName, "gr2") + gr2TargetRef := createTestPolicyTargetRef( + kinds.GRPCRoute, + types.NamespacedName{Namespace: testNs, Name: "gr2"}, + ) + + invalidRef := createTestRef(kinds.HTTPRoute, v1.GroupName, "invalid") + invalidTargetRef := createTestPolicyTargetRef( + "invalidKind", + types.NamespacedName{Namespace: testNs, Name: "invalid"}, + ) + + tests := []struct { + policies map[PolicyKey]*Policy + gws map[types.NamespacedName]*Gateway + routes map[RouteKey]*L7Route + expectedConditions map[types.NamespacedName][]conditions.Condition + name string + missingKeys bool + }{ + { + name: "no policies", + policies: nil, + gws: nil, + routes: nil, + }, + { + name: "csp policy with gateway target ref", + policies: map[PolicyKey]*Policy{ + createTestPolicyKey(cspGVK, "csp1"): { + Source: createTestPolicy(cspGVK, "csp1", gw1Ref), + TargetRefs: []PolicyTargetRef{gw1TargetRef}, + }, + }, + gws: createGatewayMap(types.NamespacedName{Namespace: testNs, Name: "gw1"}), + routes: nil, + expectedConditions: map[types.NamespacedName][]conditions.Condition{ + {Namespace: testNs, Name: "gw1"}: { + conditions.NewClientSettingsPolicyAffected(), + }, + }, + }, + { + name: "gateway attached to csp and op policy", + policies: map[PolicyKey]*Policy{ + createTestPolicyKey(cspGVK, "csp1"): { + Source: createTestPolicy(cspGVK, "csp1", gw2Ref), + TargetRefs: []PolicyTargetRef{gw2TargetRef}, + }, + createTestPolicyKey(opGVK, "observabilityPolicy1"): { + Source: createTestPolicy(opGVK, "observabilityPolicy1", gw2Ref), + TargetRefs: []PolicyTargetRef{gw2TargetRef}, + }, + }, + gws: createGatewayMap(types.NamespacedName{Namespace: testNs, Name: "gw2"}), + routes: nil, + expectedConditions: map[types.NamespacedName][]conditions.Condition{ + {Namespace: testNs, Name: "gw2"}: { + conditions.NewClientSettingsPolicyAffected(), + conditions.NewObservabilityPolicyAffected(), + }, + }, + }, + { + name: "policies with l7 routes target ref", + policies: map[PolicyKey]*Policy{ + createTestPolicyKey(opGVK, "observabilityPolicy1"): { + Source: createTestPolicy(opGVK, "observabilityPolicy1", hr1Ref), + TargetRefs: []PolicyTargetRef{hr1TargetRef}, + }, + createTestPolicyKey(cspGVK, "csp1"): { + Source: createTestPolicy(cspGVK, "csp1", gr1Ref), + TargetRefs: []PolicyTargetRef{gr1TargetRef}, + }, + }, + routes: map[RouteKey]*L7Route{ + {RouteType: RouteTypeHTTP, NamespacedName: types.NamespacedName{Namespace: testNs, Name: "hr1"}}: { + Source: &v1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hr1", + Namespace: testNs, + }, + }, + }, + {RouteType: RouteTypeGRPC, NamespacedName: types.NamespacedName{Namespace: testNs, Name: "gr1"}}: { + Source: &v1.GRPCRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gr1", + Namespace: testNs, + }, + }, + }, + }, + expectedConditions: map[types.NamespacedName][]conditions.Condition{ + {Namespace: testNs, Name: "hr1"}: { + conditions.NewObservabilityPolicyAffected(), + }, + {Namespace: testNs, Name: "gr1"}: { + conditions.NewClientSettingsPolicyAffected(), + }, + }, + }, + { + name: "policies with multiple target refs of different kinds", + policies: map[PolicyKey]*Policy{ + createTestPolicyKey(cspGVK, "csp1"): { + Source: createTestPolicy(cspGVK, "csp1", gw3Ref, hr2Ref), + TargetRefs: []PolicyTargetRef{gw3TargetRef, hr2TargetRef}, + }, + createTestPolicyKey(opGVK, "observabilityPolicy1"): { + Source: createTestPolicy(opGVK, "observabilityPolicy1", hr2Ref, gr2Ref), + TargetRefs: []PolicyTargetRef{hr2TargetRef, gr2TargetRef}, + }, + createTestPolicyKey(opGVK, "observabilityPolicy2"): { + Source: createTestPolicy(opGVK, "observabilityPolicy2", gw3Ref, gr2Ref), + TargetRefs: []PolicyTargetRef{gw3TargetRef, gr2TargetRef}, + }, + }, + gws: createGatewayMap( + types.NamespacedName{Namespace: testNs, Name: "gw3"}, + ), + routes: map[RouteKey]*L7Route{ + {RouteType: RouteTypeHTTP, NamespacedName: types.NamespacedName{Namespace: testNs, Name: "hr2"}}: { + Source: &v1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hr2", + Namespace: testNs, + }, + }, + }, + {RouteType: RouteTypeGRPC, NamespacedName: types.NamespacedName{Namespace: testNs, Name: "gr2"}}: { + Source: &v1.GRPCRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gr2", + Namespace: testNs, + }, + }, + }, + }, + expectedConditions: map[types.NamespacedName][]conditions.Condition{ + {Namespace: testNs, Name: "gw3"}: { + conditions.NewClientSettingsPolicyAffected(), + conditions.NewObservabilityPolicyAffected(), + }, + {Namespace: testNs, Name: "hr2"}: { + conditions.NewObservabilityPolicyAffected(), + conditions.NewClientSettingsPolicyAffected(), + }, + {Namespace: testNs, Name: "gr2"}: { + conditions.NewObservabilityPolicyAffected(), + }, + }, + }, + { + name: "multiple policies with same target ref, only one condition should be added", + policies: map[PolicyKey]*Policy{ + createTestPolicyKey(cspGVK, "csp1"): { + Source: createTestPolicy(cspGVK, "csp1", hr3Ref), + TargetRefs: []PolicyTargetRef{hr3TargetRef}, + }, + createTestPolicyKey(cspGVK, "csp2"): { + Source: createTestPolicy(cspGVK, "csp2", hr3Ref), + TargetRefs: []PolicyTargetRef{hr3TargetRef}, + }, + }, + routes: map[RouteKey]*L7Route{ + {RouteType: RouteTypeHTTP, NamespacedName: types.NamespacedName{Namespace: testNs, Name: "hr3"}}: { + Source: &v1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hr3", + Namespace: testNs, + }, + }, + }, + }, + expectedConditions: map[types.NamespacedName][]conditions.Condition{ + {Namespace: testNs, Name: "hr3"}: { + conditions.NewClientSettingsPolicyAffected(), + }, + }, + }, + { + name: "no condition added for invalid target ref kind", + policies: map[PolicyKey]*Policy{ + createTestPolicyKey(cspGVK, "csp1"): { + Source: createTestPolicy(cspGVK, "csp1", invalidRef), + TargetRefs: []PolicyTargetRef{invalidTargetRef}, + }, + }, + routes: map[RouteKey]*L7Route{ + {RouteType: RouteTypeHTTP, NamespacedName: types.NamespacedName{Namespace: testNs, Name: "invalid"}}: { + Source: &v1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "invalid", + Namespace: testNs, + }, + }, + }, + }, + expectedConditions: map[types.NamespacedName][]conditions.Condition{ + {Namespace: testNs, Name: "invalid"}: {}, + }, + }, + { + name: "no condition added when target ref gateway is not present in the graph", + policies: map[PolicyKey]*Policy{ + createTestPolicyKey(cspGVK, "csp1"): { + Source: createTestPolicy(cspGVK, "csp1", gw1Ref), + TargetRefs: []PolicyTargetRef{gw1TargetRef}, + }, + }, + gws: createGatewayMap( + types.NamespacedName{Namespace: testNs, Name: "gw2"}, + ), + expectedConditions: map[types.NamespacedName][]conditions.Condition{ + {Namespace: testNs, Name: "gw1"}: {}, + }, + missingKeys: true, + }, + { + name: "no condition added when target ref gateway is nil", + policies: map[PolicyKey]*Policy{ + createTestPolicyKey(cspGVK, "csp1"): { + Source: createTestPolicy(cspGVK, "csp1", gw1Ref), + TargetRefs: []PolicyTargetRef{gw1TargetRef}, + }, + }, + gws: map[types.NamespacedName]*Gateway{ + {Namespace: testNs, Name: "gw1"}: nil, + }, + expectedConditions: map[types.NamespacedName][]conditions.Condition{ + {Namespace: testNs, Name: "gw1"}: {}, + }, + missingKeys: true, + }, + { + name: "no condition added when target ref route is not present in the graph", + policies: map[PolicyKey]*Policy{ + createTestPolicyKey(opGVK, "observabilityPolicy1"): { + Source: createTestPolicy(opGVK, "observabilityPolicy1", hr1Ref), + TargetRefs: []PolicyTargetRef{hr1TargetRef}, + }, + }, + routes: map[RouteKey]*L7Route{ + {RouteType: RouteTypeHTTP, NamespacedName: types.NamespacedName{Namespace: testNs, Name: "hr3"}}: { + Source: &v1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hr3", + Namespace: testNs, + }, + }, + }, + }, + expectedConditions: map[types.NamespacedName][]conditions.Condition{ + {Namespace: testNs, Name: "hr1"}: {}, + }, + missingKeys: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + addPolicyAffectedStatusToTargetRefs(test.policies, test.routes, test.gws) + + for _, pols := range test.policies { + for _, targetRefs := range pols.TargetRefs { + switch targetRefs.Kind { + case kinds.Gateway: + if !test.missingKeys { + g.Expect(test.gws).To(HaveKey(targetRefs.Nsname)) + gateway := test.gws[targetRefs.Nsname] + g.Expect(gateway.Conditions).To(ContainElements(test.expectedConditions[targetRefs.Nsname])) + } else { + g.Expect(test.expectedConditions[types.NamespacedName{Namespace: testNs, Name: "gw1"}]).To(BeEmpty()) + } + + case kinds.HTTPRoute, kinds.GRPCRoute: + routeKey := routeKeyForKind(targetRefs.Kind, targetRefs.Nsname) + if !test.missingKeys { + g.Expect(test.routes).To(HaveKey(routeKey)) + route := test.routes[routeKeyForKind(targetRefs.Kind, targetRefs.Nsname)] + g.Expect(route.Conditions).To(ContainElements(test.expectedConditions[targetRefs.Nsname])) + } else { + g.Expect(test.expectedConditions[types.NamespacedName{Namespace: testNs, Name: "hr1"}]).To(BeEmpty()) + } + } + } + } + }) + } +} + +func TestAddStatusToTargetRefs(t *testing.T) { + t.Parallel() + + g := NewWithT(t) + + policyKind := kinds.ObservabilityPolicy + + g.Expect(func() { + addStatusToTargetRefs(policyKind, nil) + }).ToNot(Panic()) +}