diff --git a/internal/controller/apiproduct_controller.go b/internal/controller/apiproduct_controller.go index 10a662e..e02ef06 100644 --- a/internal/controller/apiproduct_controller.go +++ b/internal/controller/apiproduct_controller.go @@ -340,6 +340,27 @@ func (r *APIProductReconciler) openAPISpecReadyCondition(openAPISpec *devportalv return &condition } +// findPlanPolicyForAPIProduct discovers the effective PlanPolicy for an APIProduct by searching +// in order of specificity: HTTPRoute-level first, then Gateway-level. +// +// Lookup hierarchy: +// 1. PlanPolicy targeting the HTTPRoute directly (most specific) +// 2. PlanPolicy targeting a Gateway that the HTTPRoute references (less specific) +// +// Multiple PlanPolicies scenario: +// When both HTTPRoute-level and Gateway-level PlanPolicies exist: +// - PlanPolicy A (targeting Gateway) creates → RateLimitPolicy A (targeting Gateway) +// - PlanPolicy B (targeting HTTPRoute) creates → RateLimitPolicy B (targeting HTTPRoute) +// - RateLimitPolicy B will atomically override RateLimitPolicy A for that specific HTTPRoute +// +// The effective policy is NOT a merge of both PlanPolicies. Instead, the most specific +// RateLimitPolicy wins via atomic override because: +// - Both use atomic merge strategy (default) +// - More specific policies (HTTPRoute-level) take precedence over less specific ones (Gateway-level) +// - The atomic strategy means the entire policy is replaced, not merged rule-by-rule +// +// This function returns only the most specific PlanPolicy found, which will be the one +// that determines the effective rate limiting behavior for the APIProduct. func (r *APIProductReconciler) findPlanPolicyForAPIProduct(ctx context.Context, apiProductObj *devportalv1alpha1.APIProduct) (*planpolicyv1alpha1.PlanPolicy, error) { route := &gwapiv1.HTTPRoute{} rKey := client.ObjectKey{ // Its deployment is built after the same name and namespace