Repository: nginx/nginx-gateway-fabric

Issue:
Implement PolicyAffected Status for NGF Policies
**As an** HTTPRoute or Gateway owner
**I want** to know when my object is affected by an NGF policy
**So that** I know that the policy has taken effect.

### Acceptance
- Add and document the Condition type `gateway.nginx.org/ClientSettingsPolicyAffected`, `gateway.nginx.org/ObservabilityPolicyAffected`, and 'gateway.nginx.org/Waf/WAFPolicyAffected'
- Set this Condition on all HTTPRoutes and Gateways affected by a ClientSettingPolicy or ObservabilityPolicy. 
- If an object is affected by multiple ClientSettingsPolicy, only one `gateway.nginx.org/ClientSettingsPolicyAffected` should exist. (same for ObservabilityPolicy)
- When the last policy affecting that object is removed, the Condition should be removed.
- The Observed Generation is the generation of the affected object, not the generation of the policy.

Hints from issue comments:
@sjberman: This also needs to work for the ObservabilityPolicy. Ideally our policy logic is general enough that it will apply everywhere.@salonichf5: > This also needs to work for the ObservabilityPolicy. Ideally our policy logic is general enough that it will apply everywhere.

Confirming that we need to add another field letting us know if `ObservabilityPolicyAffected` has been applied to that resource? 

Also, when a `PolicyAffected` is added to the `Conditions` of a resource, it will specify the name of the Policy, be it observability or client settings? or just true or false?@sjberman: Requirements are defined [here](https://gateway-api.sigs.k8s.io/geps/gep-713/#standard-status-condition-on-policy-affected-objects) by the spec and [here](https://github.com/nginxinc/nginx-gateway-fabric/blob/main/docs/proposals/client-settings.md#setting-status-on-objects-affected-by-a-policy) by our design.

Just the message should have the context as to which policy is attached.@kate-osborn: @sjberman @salonichf5 we may want to wait to implement this. There could be changes to how policies are stored on the graph once we fix #2105 and #2079, which could affect how we build the PolicyAffected statuses.@mpstefan: Blocked by #2105 #2079 @kate-osborn: @mpstefan can we move this up in priority? This is an important tool for troubleshooting policies. It let's the app dev know when their route is affected by a policy applied at the gateway level. @nginx-bot: Hi @kate-osborn!

Thanks for opening this issue!
Be sure to check out our [Contributing Guidelines](../blob/main/CONTRIBUTING.md) and the [Issue Lifecycle](../blob/main/ISSUE_LIFECYCLE.md) while you wait for someone on the team to take a look at this.

<!-- nginx-bot-comment -->

Solution patch:
diff --git a/internal/controller/state/conditions/conditions.go b/internal/controller/state/conditions/conditions.go
index b9ec5f10..6630e69a 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/graph/graph.go b/internal/controller/state/graph/graph.go
index 12017112..a2061525 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/policies.go b/internal/controller/state/graph/policies.go
index b5a1251a..509e41f2 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())
+	}
+}



Test patch:
diff --git a/internal/controller/state/conditions/conditions_test.go b/internal/controller/state/conditions/conditions_test.go
index b446a6ec..45ebcc58 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_test.go b/internal/controller/state/graph/graph_test.go
index 748a3aa2..e871b3c4 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_test.go b/internal/controller/state/graph/policies_test.go
index 7df4a9d4..18be2262 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())
+}