diff --git a/api/v1alpha1/dnspolicy_types.go b/api/v1alpha1/dnspolicy_types.go index c3840b8ba..625e91214 100644 --- a/api/v1alpha1/dnspolicy_types.go +++ b/api/v1alpha1/dnspolicy_types.go @@ -26,7 +26,7 @@ import ( kuadrantdnsv1alpha1 "github.com/kuadrant/dns-operator/api/v1alpha1" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" ) type RoutingStrategy string @@ -38,6 +38,9 @@ const ( DefaultWeight Weight = 120 DefaultGeo GeoCode = "default" WildcardGeo GeoCode = "*" + + DNSPolicyBackReferenceAnnotationName = "kuadrant.io/dnspolicies" + DNSPolicyDirectReferenceAnnotationName = "kuadrant.io/dnspolicy" ) // DNSPolicySpec defines the desired state of DNSPolicy @@ -126,7 +129,8 @@ type DNSPolicyStatus struct { HealthCheck *HealthCheckStatus `json:"healthCheck,omitempty"` } -var _ common.KuadrantPolicy = &DNSPolicy{} +var _ kuadrant.Policy = &DNSPolicy{} +var _ kuadrant.Referrer = &DNSPolicy{} // +kubebuilder:object:root=true // +kubebuilder:subresource:status @@ -159,6 +163,14 @@ func (p *DNSPolicy) GetTargetRef() gatewayapiv1alpha2.PolicyTargetReference { func (p *DNSPolicy) Kind() string { return p.TypeMeta.Kind } +func (p *DNSPolicy) BackReferenceAnnotationName() string { + return DNSPolicyBackReferenceAnnotationName +} + +func (p *DNSPolicy) DirectReferenceAnnotationName() string { + return DNSPolicyDirectReferenceAnnotationName +} + // Validate ensures the resource is valid. Compatible with the validating interface // used by webhooks func (p *DNSPolicy) Validate() error { diff --git a/api/v1alpha1/tlspolicy_types.go b/api/v1alpha1/tlspolicy_types.go index d6630eb15..6952b0954 100644 --- a/api/v1alpha1/tlspolicy_types.go +++ b/api/v1alpha1/tlspolicy_types.go @@ -25,7 +25,12 @@ import ( gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" +) + +const ( + TLSPolicyBackReferenceAnnotationName = "kuadrant.io/tlspolicies" + TLSPolicyDirectReferenceAnnotationName = "kuadrant.io/tlspolicy" ) // TLSPolicySpec defines the desired state of TLSPolicy @@ -111,7 +116,8 @@ type TLSPolicyStatus struct { ObservedGeneration int64 `json:"observedGeneration,omitempty"` } -var _ common.KuadrantPolicy = &TLSPolicy{} +var _ kuadrant.Policy = &TLSPolicy{} +var _ kuadrant.Referrer = &TLSPolicy{} // +kubebuilder:object:root=true // +kubebuilder:subresource:status @@ -144,6 +150,14 @@ func (p *TLSPolicy) GetTargetRef() gatewayapiv1alpha2.PolicyTargetReference { return p.Spec.TargetRef } +func (p *TLSPolicy) BackReferenceAnnotationName() string { + return TLSPolicyBackReferenceAnnotationName +} + +func (p *TLSPolicy) DirectReferenceAnnotationName() string { + return TLSPolicyDirectReferenceAnnotationName +} + func (p *TLSPolicy) Validate() error { if p.Spec.TargetRef.Group != (gatewayapiv1.GroupName) { return fmt.Errorf("invalid targetRef.Group %s. The only supported group is %s", p.Spec.TargetRef.Group, gatewayapiv1.GroupName) diff --git a/api/v1beta1/kuadrant_types.go b/api/v1beta1/kuadrant_types.go index 0c608cd88..152d43b83 100644 --- a/api/v1beta1/kuadrant_types.go +++ b/api/v1beta1/kuadrant_types.go @@ -23,7 +23,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! @@ -76,8 +76,8 @@ func (r *KuadrantStatus) Equals(other *KuadrantStatus, logger logr.Logger) bool } // Marshalling sorts by condition type - currentMarshaledJSON, _ := common.ConditionMarshal(r.Conditions) - otherMarshaledJSON, _ := common.ConditionMarshal(other.Conditions) + currentMarshaledJSON, _ := kuadrant.ConditionMarshal(r.Conditions) + otherMarshaledJSON, _ := kuadrant.ConditionMarshal(other.Conditions) if string(currentMarshaledJSON) != string(otherMarshaledJSON) { diff := cmp.Diff(string(currentMarshaledJSON), string(otherMarshaledJSON)) logger.V(1).Info("Conditions not equal", "difference", diff) diff --git a/api/v1beta2/authpolicy_types.go b/api/v1beta2/authpolicy_types.go index 78292cac0..aac60eb66 100644 --- a/api/v1beta2/authpolicy_types.go +++ b/api/v1beta2/authpolicy_types.go @@ -5,13 +5,19 @@ import ( "github.com/go-logr/logr" "github.com/google/go-cmp/cmp" + authorinoapi "github.com/kuadrant/authorino/api/v1beta2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - authorinoapi "github.com/kuadrant/authorino/api/v1beta2" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" +) + +const ( + AuthPolicyBackReferenceAnnotationName = "kuadrant.io/authpolicies" + AuthPolicyDirectReferenceAnnotationName = "kuadrant.io/authpolicy" ) type AuthSchemeSpec struct { @@ -186,8 +192,8 @@ func (s *AuthPolicyStatus) Equals(other *AuthPolicyStatus, logger logr.Logger) b } // Marshalling sorts by condition type - currentMarshaledJSON, _ := common.ConditionMarshal(s.Conditions) - otherMarshaledJSON, _ := common.ConditionMarshal(other.Conditions) + currentMarshaledJSON, _ := kuadrant.ConditionMarshal(s.Conditions) + otherMarshaledJSON, _ := kuadrant.ConditionMarshal(other.Conditions) if string(currentMarshaledJSON) != string(otherMarshaledJSON) { diff := cmp.Diff(string(currentMarshaledJSON), string(otherMarshaledJSON)) logger.V(1).Info("Conditions not equal", "difference", diff) @@ -197,7 +203,8 @@ func (s *AuthPolicyStatus) Equals(other *AuthPolicyStatus, logger logr.Logger) b return true } -var _ common.KuadrantPolicy = &AuthPolicy{} +var _ kuadrant.Policy = &AuthPolicy{} +var _ kuadrant.Referrer = &AuthPolicy{} // +kubebuilder:object:root=true // +kubebuilder:subresource:status @@ -251,7 +258,7 @@ func (ap *AuthPolicy) GetRulesHostnames() (ruleHosts []string) { appendRuleHosts := func(obj RouteSelectorsGetter) { for _, routeSelector := range obj.GetRouteSelectors() { - ruleHosts = append(ruleHosts, common.HostnamesToStrings(routeSelector.Hostnames)...) + ruleHosts = append(ruleHosts, utils.HostnamesToStrings(routeSelector.Hostnames)...) } } @@ -284,6 +291,14 @@ func (ap *AuthPolicy) Kind() string { return ap.TypeMeta.Kind } +func (ap *AuthPolicy) BackReferenceAnnotationName() string { + return AuthPolicyBackReferenceAnnotationName +} + +func (ap *AuthPolicy) DirectReferenceAnnotationName() string { + return AuthPolicyDirectReferenceAnnotationName +} + //+kubebuilder:object:root=true // AuthPolicyList contains a list of AuthPolicy @@ -293,8 +308,8 @@ type AuthPolicyList struct { Items []AuthPolicy `json:"items"` } -func (l *AuthPolicyList) GetItems() []common.KuadrantPolicy { - return common.Map(l.Items, func(item AuthPolicy) common.KuadrantPolicy { +func (l *AuthPolicyList) GetItems() []kuadrant.Policy { + return utils.Map(l.Items, func(item AuthPolicy) kuadrant.Policy { return &item }) } diff --git a/api/v1beta2/authpolicy_types_test.go b/api/v1beta2/authpolicy_types_test.go index 7b645bd3c..4e9fbd3be 100644 --- a/api/v1beta2/authpolicy_types_test.go +++ b/api/v1beta2/authpolicy_types_test.go @@ -6,13 +6,13 @@ import ( "reflect" "testing" + authorinoapi "github.com/kuadrant/authorino/api/v1beta2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - authorinoapi "github.com/kuadrant/authorino/api/v1beta2" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" ) func TestCommonAuthRuleSpecGetRouteSelectors(t *testing.T) { @@ -55,7 +55,7 @@ func TestAuthPolicyTargetKey(t *testing.T) { }, Spec: AuthPolicySpec{ TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ - Group: "gateway.networking.k8s.io", + Group: gatewayapiv1.GroupName, Kind: "HTTPRoute", Name: "my-route", }, @@ -86,9 +86,9 @@ func TestAuthPolicyListGetItems(t *testing.T) { if len(result) != 1 { t.Errorf("Expected 1 item, got %d", len(result)) } - _, ok := result[0].(common.KuadrantPolicy) + _, ok := result[0].(kuadrant.Policy) if !ok { - t.Errorf("Expected item to be a KuadrantPolicy") + t.Errorf("Expected item to be a Policy") } } @@ -100,7 +100,7 @@ func TestAuthPolicyGetRulesHostnames(t *testing.T) { }, Spec: AuthPolicySpec{ TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ - Group: "gateway.networking.k8s.io", + Group: gatewayapiv1.GroupName, Kind: "HTTPRoute", Name: "my-route", }, @@ -256,7 +256,7 @@ func TestAuthPolicyValidate(t *testing.T) { }, Spec: AuthPolicySpec{ TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ - Group: "gateway.networking.k8s.io", + Group: gatewayapiv1.GroupName, Kind: "HTTPRoute", Name: "my-route", Namespace: ptr.To(gatewayapiv1.Namespace("other-namespace")), diff --git a/api/v1beta2/ratelimitpolicy_types.go b/api/v1beta2/ratelimitpolicy_types.go index 5da005420..38c537611 100644 --- a/api/v1beta2/ratelimitpolicy_types.go +++ b/api/v1beta2/ratelimitpolicy_types.go @@ -21,11 +21,13 @@ import ( "github.com/go-logr/logr" "github.com/google/go-cmp/cmp" - "github.com/kuadrant/kuadrant-operator/pkg/common" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! @@ -51,6 +53,9 @@ const ( IncludeOperator WhenConditionOperator = "incl" ExcludeOperator WhenConditionOperator = "excl" MatchesOperator WhenConditionOperator = "matches" + + RateLimitPolicyBackReferenceAnnotationName = "kuadrant.io/ratelimitpolicies" + RateLimitPolicyDirectReferenceAnnotationName = "kuadrant.io/ratelimitpolicy" ) // +kubebuilder:validation:Enum:=second;minute;hour;day @@ -110,7 +115,7 @@ func (l Limit) CountersAsStringList() []string { if len(l.Counters) == 0 { return nil } - return common.Map(l.Counters, func(counter ContextSelector) string { return string(counter) }) + return utils.Map(l.Counters, func(counter ContextSelector) string { return string(counter) }) } // RateLimitPolicySpec defines the desired state of RateLimitPolicy @@ -150,8 +155,8 @@ func (s *RateLimitPolicyStatus) Equals(other *RateLimitPolicyStatus, logger logr } // Marshalling sorts by condition type - currentMarshaledJSON, _ := common.ConditionMarshal(s.Conditions) - otherMarshaledJSON, _ := common.ConditionMarshal(other.Conditions) + currentMarshaledJSON, _ := kuadrant.ConditionMarshal(s.Conditions) + otherMarshaledJSON, _ := kuadrant.ConditionMarshal(other.Conditions) if string(currentMarshaledJSON) != string(otherMarshaledJSON) { if logger.V(1).Enabled() { diff := cmp.Diff(string(currentMarshaledJSON), string(otherMarshaledJSON)) @@ -163,7 +168,8 @@ func (s *RateLimitPolicyStatus) Equals(other *RateLimitPolicyStatus, logger logr return true } -var _ common.KuadrantPolicy = &RateLimitPolicy{} +var _ kuadrant.Policy = &RateLimitPolicy{} +var _ kuadrant.Referrer = &RateLimitPolicy{} // +kubebuilder:object:root=true // +kubebuilder:subresource:status @@ -212,8 +218,8 @@ type RateLimitPolicyList struct { Items []RateLimitPolicy `json:"items"` } -func (l *RateLimitPolicyList) GetItems() []common.KuadrantPolicy { - return common.Map(l.Items, func(item RateLimitPolicy) common.KuadrantPolicy { +func (l *RateLimitPolicyList) GetItems() []kuadrant.Policy { + return utils.Map(l.Items, func(item RateLimitPolicy) kuadrant.Policy { return &item }) } @@ -247,6 +253,14 @@ func (r *RateLimitPolicy) Kind() string { return r.TypeMeta.Kind } +func (r *RateLimitPolicy) BackReferenceAnnotationName() string { + return RateLimitPolicyBackReferenceAnnotationName +} + +func (r *RateLimitPolicy) DirectReferenceAnnotationName() string { + return RateLimitPolicyDirectReferenceAnnotationName +} + func init() { SchemeBuilder.Register(&RateLimitPolicy{}, &RateLimitPolicyList{}) } diff --git a/api/v1beta2/ratelimitpolicy_types_test.go b/api/v1beta2/ratelimitpolicy_types_test.go index af895e33b..5cd7b28a9 100644 --- a/api/v1beta2/ratelimitpolicy_types_test.go +++ b/api/v1beta2/ratelimitpolicy_types_test.go @@ -10,7 +10,7 @@ import ( gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" ) func testBuildBasicRLP(name string, kind gatewayapiv1.Kind) *RateLimitPolicy { @@ -25,7 +25,7 @@ func testBuildBasicRLP(name string, kind gatewayapiv1.Kind) *RateLimitPolicy { }, Spec: RateLimitPolicySpec{ TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ - Group: "gateway.networking.k8s.io", + Group: gatewayapiv1.GroupName, Kind: kind, Name: "some-name", }, @@ -70,8 +70,8 @@ func TestRateLimitPolicyListGetItems(t *testing.T) { if len(result) != 1 { t.Errorf("Expected 1 item, got %d", len(result)) } - _, ok := result[0].(common.KuadrantPolicy) + _, ok := result[0].(kuadrant.Policy) if !ok { - t.Errorf("Expected item to be a KuadrantPolicy") + t.Errorf("Expected item to be a Policy") } } diff --git a/api/v1beta2/route_selectors.go b/api/v1beta2/route_selectors.go index 880ae16be..ef494effa 100644 --- a/api/v1beta2/route_selectors.go +++ b/api/v1beta2/route_selectors.go @@ -1,11 +1,11 @@ package v1beta2 import ( + "github.com/elliotchance/orderedmap/v2" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - orderedmap "github.com/elliotchance/orderedmap/v2" - - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" ) // RouteSelector defines semantics for matching an HTTP request based on conditions @@ -32,7 +32,7 @@ type RouteSelector struct { // returns nil. func (s *RouteSelector) SelectRules(route *gatewayapiv1.HTTPRoute) (rules []gatewayapiv1.HTTPRouteRule) { rulesIndices := orderedmap.NewOrderedMap[int, gatewayapiv1.HTTPRouteRule]() - if len(s.Hostnames) > 0 && !common.Intersect(s.Hostnames, route.Spec.Hostnames) { + if len(s.Hostnames) > 0 && !utils.Intersect(s.Hostnames, route.Spec.Hostnames) { return nil } if len(s.Matches) == 0 { @@ -41,7 +41,7 @@ func (s *RouteSelector) SelectRules(route *gatewayapiv1.HTTPRoute) (rules []gate for idx := range s.Matches { routeSelectorMatch := s.Matches[idx] for idx, rule := range route.Spec.Rules { - rs := common.HTTPRouteRuleSelector{HTTPRouteMatch: &routeSelectorMatch} + rs := kuadrant.HTTPRouteRuleSelector{HTTPRouteMatch: &routeSelectorMatch} if rs.Selects(rule) { rulesIndices.Set(idx, rule) } @@ -59,10 +59,10 @@ func (s *RouteSelector) HostnamesForConditions(route *gatewayapiv1.HTTPRoute) [] hostnames := route.Spec.Hostnames if len(s.Hostnames) > 0 { - hostnames = common.Intersection(s.Hostnames, hostnames) + hostnames = utils.Intersection(s.Hostnames, hostnames) } - if common.SameElements(hostnames, route.Spec.Hostnames) { + if utils.SameElements(hostnames, route.Spec.Hostnames) { return []gatewayapiv1.Hostname{"*"} } diff --git a/api/v1beta2/route_selectors_test.go b/api/v1beta2/route_selectors_test.go index 7e061ab8e..c3934b86f 100644 --- a/api/v1beta2/route_selectors_test.go +++ b/api/v1beta2/route_selectors_test.go @@ -11,7 +11,8 @@ import ( "k8s.io/utils/ptr" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" ) func TestRouteSelectors(t *testing.T) { @@ -144,7 +145,7 @@ func TestRouteSelectors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { rules := tc.routeSelector.SelectRules(tc.route) rulesToStringSlice := func(rules []gatewayapiv1.HTTPRouteRule) []string { - return common.Map(common.Map(rules, common.HTTPRouteRuleToString), func(r string) string { return fmt.Sprintf("{%s}", r) }) + return utils.Map(utils.Map(rules, kuadrant.HTTPRouteRuleToString), func(r string) string { return fmt.Sprintf("{%s}", r) }) } if !reflect.DeepEqual(rules, tc.expected) { t.Errorf("expected %v, got %v", rulesToStringSlice(tc.expected), rulesToStringSlice(rules)) diff --git a/controllers/authpolicy_authconfig.go b/controllers/authpolicy_authconfig.go index 63f447e77..a3da417b9 100644 --- a/controllers/authpolicy_authconfig.go +++ b/controllers/authpolicy_authconfig.go @@ -8,14 +8,16 @@ import ( "strings" "github.com/go-logr/logr" + authorinoapi "github.com/kuadrant/authorino/api/v1beta2" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - authorinoapi "github.com/kuadrant/authorino/api/v1beta2" api "github.com/kuadrant/kuadrant-operator/api/v1beta2" "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" ) func (r *AuthPolicyReconciler) reconcileAuthConfigs(ctx context.Context, ap *api.AuthPolicy, targetNetworkObject client.Object) error { @@ -65,22 +67,22 @@ func (r *AuthPolicyReconciler) desiredAuthConfig(ctx context.Context, ap *api.Au case *gatewayapiv1.HTTPRoute: route = obj var err error - hosts, err = common.HostnamesFromHTTPRoute(ctx, obj, r.Client()) + hosts, err = kuadrant.HostnamesFromHTTPRoute(ctx, obj, r.Client()) if err != nil { return nil, err } case *gatewayapiv1.Gateway: // fake a single httproute with all rules from all httproutes accepted by the gateway, // that do not have an authpolicy of its own, so we can generate wasm rules for those cases - gw := common.GatewayWrapper{Gateway: obj} + gw := kuadrant.GatewayWrapper{Gateway: obj} gwHostnames := gw.Hostnames() if len(gwHostnames) == 0 { gwHostnames = []gatewayapiv1.Hostname{"*"} } - hosts = common.HostnamesToStrings(gwHostnames) + hosts = utils.HostnamesToStrings(gwHostnames) rules := make([]gatewayapiv1.HTTPRouteRule, 0) - routes := r.FetchAcceptedGatewayHTTPRoutes(ctx, ap.TargetKey()) + routes := r.TargetRefReconciler.FetchAcceptedGatewayHTTPRoutes(ctx, ap.TargetKey()) for idx := range routes { route := routes[idx] // skip routes that have an authpolicy of its own @@ -362,7 +364,7 @@ func authorinoConditionsFromHTTPRouteRule(rule gatewayapiv1.HTTPRouteRule, hostn if len(allOf) > 0 { oneOf = append(oneOf, authorinoapi.PatternExpressionOrRef{ - All: common.Map(allOf, toAuthorinoUnstructuredPatternExpressionOrRef), + All: utils.Map(allOf, toAuthorinoUnstructuredPatternExpressionOrRef), }) } } @@ -380,7 +382,7 @@ func hostnameRuleToAuthorinoCondition(hostnames []string) authorinoapi.PatternEx } func hostnamesToRegex(hostnames []string) string { - return strings.Join(common.Map(hostnames, func(hostname string) string { + return strings.Join(utils.Map(hostnames, func(hostname string) string { return strings.ReplaceAll(strings.ReplaceAll(hostname, ".", `\.`), "*", ".*") }), "|") } @@ -495,7 +497,7 @@ func toAuthorinoUnstructuredPatternExpressionOrRef(patternExpressionOrRef author func toAuthorinoOneOfPatternExpressionsOrRefs(oneOf []authorinoapi.PatternExpressionOrRef) []authorinoapi.PatternExpressionOrRef { return []authorinoapi.PatternExpressionOrRef{ { - Any: common.Map(oneOf, toAuthorinoUnstructuredPatternExpressionOrRef), + Any: utils.Map(oneOf, toAuthorinoUnstructuredPatternExpressionOrRef), }, } } diff --git a/controllers/authpolicy_controller.go b/controllers/authpolicy_controller.go index 5eec39b72..7124e143d 100644 --- a/controllers/authpolicy_controller.go +++ b/controllers/authpolicy_controller.go @@ -5,16 +5,19 @@ import ( "encoding/json" "github.com/go-logr/logr" + authorinoapi "github.com/kuadrant/authorino/api/v1beta2" apierrors "k8s.io/apimachinery/pkg/api/errors" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - authorinoapi "github.com/kuadrant/authorino/api/v1beta2" api "github.com/kuadrant/kuadrant-operator/api/v1beta2" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/library/mappers" + reconcilerutils "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" "github.com/kuadrant/kuadrant-operator/pkg/reconcilers" ) @@ -22,9 +25,10 @@ const authPolicyFinalizer = "authpolicy.kuadrant.io/finalizer" // AuthPolicyReconciler reconciles a AuthPolicy object type AuthPolicyReconciler struct { - reconcilers.TargetRefReconciler + *reconcilers.BaseReconciler + TargetRefReconciler reconcilerutils.TargetRefReconciler // OverriddenPolicyMap tracks the overridden policies to report their status. - OverriddenPolicyMap *common.OverriddenPolicyMap + OverriddenPolicyMap *kuadrant.OverriddenPolicyMap } //+kubebuilder:rbac:groups=kuadrant.io,resources=authpolicies,verbs=get;list;watch;create;update;patch;delete @@ -60,7 +64,7 @@ func (r *AuthPolicyReconciler) Reconcile(eventCtx context.Context, req ctrl.Requ markedForDeletion := ap.GetDeletionTimestamp() != nil // fetch the target network object - targetNetworkObject, err := r.FetchValidTargetRef(ctx, ap.GetTargetRef(), ap.Namespace) + targetNetworkObject, err := reconcilerutils.FetchTargetRefObject(ctx, r.Client(), ap.GetTargetRef(), ap.Namespace) if err != nil { if !markedForDeletion { if apierrors.IsNotFound(err) { @@ -69,7 +73,7 @@ func (r *AuthPolicyReconciler) Reconcile(eventCtx context.Context, req ctrl.Requ if delResErr == nil { delResErr = err } - return r.reconcileStatus(ctx, ap, targetNetworkObject, common.NewErrTargetNotFound(ap.Kind(), ap.GetTargetRef(), delResErr)) + return r.reconcileStatus(ctx, ap, targetNetworkObject, kuadrant.NewErrTargetNotFound(ap.Kind(), ap.GetTargetRef(), delResErr)) } return ctrl.Result{}, err } @@ -135,11 +139,11 @@ func (r *AuthPolicyReconciler) Reconcile(eventCtx context.Context, req ctrl.Requ // validate performs validation before proceeding with the reconcile loop, returning a common.ErrInvalid on any failing validation func (r *AuthPolicyReconciler) validate(ap *api.AuthPolicy, targetNetworkObject client.Object) error { if err := ap.Validate(); err != nil { - return common.NewErrInvalid(ap.Kind(), err) + return kuadrant.NewErrInvalid(ap.Kind(), err) } - if err := common.ValidateHierarchicalRules(ap, targetNetworkObject); err != nil { - return common.NewErrInvalid(ap.Kind(), err) + if err := kuadrant.ValidateHierarchicalRules(ap, targetNetworkObject); err != nil { + return kuadrant.NewErrInvalid(ap.Kind(), err) } return nil @@ -151,7 +155,7 @@ func (r *AuthPolicyReconciler) reconcileResources(ctx context.Context, ap *api.A } // reconcile based on gateway diffs - gatewayDiffObj, err := r.ComputeGatewayDiffs(ctx, ap, targetNetworkObject, &common.KuadrantAuthPolicyRefsConfig{}) + gatewayDiffObj, err := reconcilerutils.ComputeGatewayDiffs(ctx, r.Client(), ap, targetNetworkObject) if err != nil { return err } @@ -170,12 +174,12 @@ func (r *AuthPolicyReconciler) reconcileResources(ctx context.Context, ap *api.A } // set annotation of policies affecting the gateway - should be the last step, only when all the reconciliation steps succeed - return r.ReconcileGatewayPolicyReferences(ctx, ap, gatewayDiffObj) + return r.TargetRefReconciler.ReconcileGatewayPolicyReferences(ctx, ap, gatewayDiffObj) } func (r *AuthPolicyReconciler) deleteResources(ctx context.Context, ap *api.AuthPolicy, targetNetworkObject client.Object) error { // delete based on gateway diffs - gatewayDiffObj, err := r.ComputeGatewayDiffs(ctx, ap, targetNetworkObject, &common.KuadrantAuthPolicyRefsConfig{}) + gatewayDiffObj, err := reconcilerutils.ComputeGatewayDiffs(ctx, r.Client(), ap, targetNetworkObject) if err != nil { return err } @@ -186,22 +190,22 @@ func (r *AuthPolicyReconciler) deleteResources(ctx context.Context, ap *api.Auth // remove direct back ref if targetNetworkObject != nil { - if err := r.deleteNetworkResourceDirectBackReference(ctx, targetNetworkObject); err != nil { + if err := r.deleteNetworkResourceDirectBackReference(ctx, targetNetworkObject, ap); err != nil { return err } } // update annotation of policies affecting the gateway - return r.ReconcileGatewayPolicyReferences(ctx, ap, gatewayDiffObj) + return r.TargetRefReconciler.ReconcileGatewayPolicyReferences(ctx, ap, gatewayDiffObj) } // Ensures only one RLP targets the network resource -func (r *AuthPolicyReconciler) reconcileNetworkResourceDirectBackReference(ctx context.Context, ap common.KuadrantPolicy, targetNetworkObject client.Object) error { - return r.ReconcileTargetBackReference(ctx, ap, targetNetworkObject, common.AuthPolicyBackRefAnnotation) +func (r *AuthPolicyReconciler) reconcileNetworkResourceDirectBackReference(ctx context.Context, ap *api.AuthPolicy, targetNetworkObject client.Object) error { + return r.TargetRefReconciler.ReconcileTargetBackReference(ctx, ap, targetNetworkObject, ap.DirectReferenceAnnotationName()) } -func (r *AuthPolicyReconciler) deleteNetworkResourceDirectBackReference(ctx context.Context, targetNetworkObject client.Object) error { - return r.DeleteTargetBackReference(ctx, targetNetworkObject, common.AuthPolicyBackRefAnnotation) +func (r *AuthPolicyReconciler) deleteNetworkResourceDirectBackReference(ctx context.Context, targetNetworkObject client.Object, ap *api.AuthPolicy) error { + return r.TargetRefReconciler.DeleteTargetBackReference(ctx, targetNetworkObject, ap.DirectReferenceAnnotationName()) } // reconcileRouteParentGatewayPolicies triggers the concurrent reconciliation of all policies that target gateways that are parents of a route @@ -224,21 +228,22 @@ func (r *AuthPolicyReconciler) reconcileRouteParentGatewayPolicies(ctx context.C // SetupWithManager sets up the controller with the Manager. func (r *AuthPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error { - httpRouteEventMapper := &HTTPRouteEventMapper{ - Logger: r.Logger().WithName("httpRouteEventMapper"), - } - gatewayEventMapper := &GatewayEventMapper{ - Logger: r.Logger().WithName("gatewayEventMapper"), - } + httpRouteEventMapper := mappers.NewHTTPRouteEventMapper(mappers.WithLogger(r.Logger().WithName("httpRouteEventMapper"))) + gatewayEventMapper := mappers.NewGatewayEventMapper(mappers.WithLogger(r.Logger().WithName("gatewayEventMapper"))) return ctrl.NewControllerManagedBy(mgr). For(&api.AuthPolicy{}). Owns(&authorinoapi.AuthConfig{}). Watches( &gatewayapiv1.HTTPRoute{}, - handler.EnqueueRequestsFromMapFunc(httpRouteEventMapper.MapToAuthPolicy), + handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, object client.Object) []reconcile.Request { + return httpRouteEventMapper.MapToPolicy(object, &api.AuthPolicy{}) + }), ). Watches(&gatewayapiv1.Gateway{}, - handler.EnqueueRequestsFromMapFunc(gatewayEventMapper.MapToAuthPolicy)). + handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, object client.Object) []reconcile.Request { + return gatewayEventMapper.MapToPolicy(object, &api.AuthPolicy{}) + }), + ). Complete(r) } diff --git a/controllers/authpolicy_controller_test.go b/controllers/authpolicy_controller_test.go index 9ae2d2adc..3ac0bf529 100644 --- a/controllers/authpolicy_controller_test.go +++ b/controllers/authpolicy_controller_test.go @@ -10,6 +10,8 @@ import ( "strings" "time" + authorinoopapi "github.com/kuadrant/authorino-operator/api/v1beta1" + authorinoapi "github.com/kuadrant/authorino/api/v1beta2" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" secv1beta1resources "istio.io/client-go/pkg/apis/security/v1beta1" @@ -24,11 +26,9 @@ import ( gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - authorinoopapi "github.com/kuadrant/authorino-operator/api/v1beta1" - authorinoapi "github.com/kuadrant/authorino/api/v1beta2" kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" api "github.com/kuadrant/kuadrant-operator/api/v1beta2" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" ) const ( @@ -61,7 +61,7 @@ var _ = Describe("AuthPolicy controller", func() { }, Spec: api.AuthPolicySpec{ TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ - Group: "gateway.networking.k8s.io", + Group: gatewayapiv1.GroupName, Kind: "HTTPRoute", Name: testHTTPRouteName, Namespace: ptr.To(gatewayapiv1.Namespace(testNamespace)), @@ -89,7 +89,7 @@ var _ = Describe("AuthPolicy controller", func() { It("Attaches policy to the Gateway", func() { policy := policyFactory(func(policy *api.AuthPolicy) { policy.Name = "gw-auth" - policy.Spec.TargetRef.Group = "gateway.networking.k8s.io" + policy.Spec.TargetRef.Group = gatewayapiv1.GroupName policy.Spec.TargetRef.Kind = "Gateway" policy.Spec.TargetRef.Name = testGatewayName policy.Spec.AuthScheme.Authentication["apiKey"].ApiKey.Selector.MatchLabels["admin"] = "yes" @@ -159,7 +159,7 @@ var _ = Describe("AuthPolicy controller", func() { policy := policyFactory(func(policy *api.AuthPolicy) { policy.Name = "gw-auth" - policy.Spec.TargetRef.Group = "gateway.networking.k8s.io" + policy.Spec.TargetRef.Group = gatewayapiv1.GroupName policy.Spec.TargetRef.Kind = "Gateway" policy.Spec.TargetRef.Name = gatewayapiv1.ObjectName(gatewayName) }) @@ -260,7 +260,7 @@ var _ = Describe("AuthPolicy controller", func() { // attach policy to the gatewaay gwPolicy := policyFactory(func(policy *api.AuthPolicy) { policy.Name = "gw-auth" - policy.Spec.TargetRef.Group = "gateway.networking.k8s.io" + policy.Spec.TargetRef.Group = gatewayapiv1.GroupName policy.Spec.TargetRef.Kind = "Gateway" policy.Spec.TargetRef.Name = testGatewayName }) @@ -335,7 +335,7 @@ var _ = Describe("AuthPolicy controller", func() { return false } condition := meta.FindStatusCondition(existingPolicy.Status.Conditions, string(gatewayapiv1alpha2.PolicyConditionAccepted)) - return condition != nil && condition.Reason == string(common.PolicyReasonUnknown) && strings.Contains(condition.Message, "cannot match any route rules, check for invalid route selectors in the policy") + return condition != nil && condition.Reason == string(kuadrant.PolicyReasonUnknown) && strings.Contains(condition.Message, "cannot match any route rules, check for invalid route selectors in the policy") }, 30*time.Second, 5*time.Second).Should(BeTrue()) // check istio authorizationpolicy @@ -380,7 +380,7 @@ var _ = Describe("AuthPolicy controller", func() { return false } condition := meta.FindStatusCondition(existingPolicy.Status.Conditions, string(gatewayapiv1alpha2.PolicyConditionAccepted)) - return condition != nil && condition.Reason == string(common.PolicyReasonUnknown) && strings.Contains(condition.Message, "cannot match any route rules, check for invalid route selectors in the policy") + return condition != nil && condition.Reason == string(kuadrant.PolicyReasonUnknown) && strings.Contains(condition.Message, "cannot match any route rules, check for invalid route selectors in the policy") }, 30*time.Second, 5*time.Second).Should(BeTrue()) iapKey := types.NamespacedName{Name: istioAuthorizationPolicyName(testGatewayName, policy.Spec.TargetRef), Namespace: testNamespace} @@ -1075,7 +1075,7 @@ var _ = Describe("AuthPolicy controller", func() { acceptedCondMatch := acceptedCond.Status == metav1.ConditionFalse && acceptedCond.Reason == reason && acceptedCond.Message == message - enforcedCond := meta.FindStatusCondition(existingPolicy.Status.Conditions, string(common.PolicyReasonEnforced)) + enforcedCond := meta.FindStatusCondition(existingPolicy.Status.Conditions, string(kuadrant.PolicyReasonEnforced)) enforcedCondMatch := enforcedCond == nil return acceptedCondMatch && enforcedCondMatch @@ -1152,7 +1152,7 @@ var _ = Describe("AuthPolicy controller", func() { acceptedCondMatch := acceptedCond.Status == metav1.ConditionTrue && acceptedCond.Reason == string(gatewayapiv1alpha2.PolicyReasonAccepted) - enforcedCond := meta.FindStatusCondition(existingPolicy.Status.Conditions, string(common.PolicyReasonEnforced)) + enforcedCond := meta.FindStatusCondition(existingPolicy.Status.Conditions, string(kuadrant.PolicyReasonEnforced)) if enforcedCond == nil { return false } @@ -1179,7 +1179,7 @@ var _ = Describe("AuthPolicy controller", func() { logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(policy).String(), "error", err) Expect(err).ToNot(HaveOccurred()) - Eventually(assertAcceptedCondTrueAndEnforcedCond(policy, metav1.ConditionTrue, string(common.PolicyReasonEnforced), + Eventually(assertAcceptedCondTrueAndEnforcedCond(policy, metav1.ConditionTrue, string(kuadrant.PolicyReasonEnforced), "AuthPolicy has been successfully enforced"), 30*time.Second, 5*time.Second).Should(BeTrue()) }) @@ -1198,7 +1198,7 @@ var _ = Describe("AuthPolicy controller", func() { logf.Log.V(1).Info("Creating AuthPolicy", "key", client.ObjectKeyFromObject(policy).String(), "error", err) Expect(err).ToNot(HaveOccurred()) - Eventually(assertAcceptedCondTrueAndEnforcedCond(policy, metav1.ConditionFalse, string(common.PolicyReasonUnknown), + Eventually(assertAcceptedCondTrueAndEnforcedCond(policy, metav1.ConditionFalse, string(kuadrant.PolicyReasonUnknown), "AuthPolicy has encountered some issues: AuthScheme is not ready yet"), 30*time.Second, 5*time.Second).Should(BeTrue()) }) @@ -1216,7 +1216,7 @@ var _ = Describe("AuthPolicy controller", func() { // attach policy to the gatewaay gwPolicy := policyFactory(func(policy *api.AuthPolicy) { policy.Name = "gw-auth" - policy.Spec.TargetRef.Group = "gateway.networking.k8s.io" + policy.Spec.TargetRef.Group = gatewayapiv1.GroupName policy.Spec.TargetRef.Kind = "Gateway" policy.Spec.TargetRef.Name = testGatewayName }) @@ -1228,7 +1228,7 @@ var _ = Describe("AuthPolicy controller", func() { // check policy status Eventually(isAuthPolicyAccepted(gwPolicy), 30*time.Second, 5*time.Second).Should(BeTrue()) Eventually( - assertAcceptedCondTrueAndEnforcedCond(gwPolicy, metav1.ConditionFalse, string(common.PolicyReasonOverridden), + assertAcceptedCondTrueAndEnforcedCond(gwPolicy, metav1.ConditionFalse, string(kuadrant.PolicyReasonOverridden), fmt.Sprintf("AuthPolicy is overridden by [{\"Namespace\":\"%s\",\"Name\":\"%s\"}]", testNamespace, routePolicy.Name)), 30*time.Second, 5*time.Second).Should(BeTrue()) @@ -1277,7 +1277,7 @@ var _ = Describe("AuthPolicy CEL Validations", func() { }, Spec: api.AuthPolicySpec{ TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ - Group: "gateway.networking.k8s.io", + Group: gatewayapiv1.GroupName, Kind: "HTTPRoute", Name: "my-target", }, @@ -1354,7 +1354,7 @@ var _ = Describe("AuthPolicy CEL Validations", func() { }, Spec: api.AuthPolicySpec{ TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ - Group: "gateway.networking.k8s.io", + Group: gatewayapiv1.GroupName, Kind: "Gateway", Name: "my-gw", }, @@ -1527,7 +1527,7 @@ func isAuthPolicyAccepted(policy *api.AuthPolicy) func() bool { } func isAuthPolicyEnforced(policy *api.AuthPolicy) func() bool { - return isAuthPolicyConditionTrue(policy, string(common.PolicyConditionEnforced)) + return isAuthPolicyConditionTrue(policy, string(kuadrant.PolicyConditionEnforced)) } func isAuthPolicyConditionTrue(policy *api.AuthPolicy, condition string) func() bool { diff --git a/controllers/authpolicy_istio_authorizationpolicy.go b/controllers/authpolicy_istio_authorizationpolicy.go index 3453d21e5..b44c56a64 100644 --- a/controllers/authpolicy_istio_authorizationpolicy.go +++ b/controllers/authpolicy_istio_authorizationpolicy.go @@ -7,26 +7,27 @@ import ( "reflect" "github.com/go-logr/logr" + istiosecurity "istio.io/api/security/v1beta1" + istio "istio.io/client-go/pkg/apis/security/v1beta1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" + "k8s.io/utils/env" "sigs.k8s.io/controller-runtime/pkg/client" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - istiosecurity "istio.io/api/security/v1beta1" - istio "istio.io/client-go/pkg/apis/security/v1beta1" - api "github.com/kuadrant/kuadrant-operator/api/v1beta2" "github.com/kuadrant/kuadrant-operator/pkg/common" - "github.com/kuadrant/kuadrant-operator/pkg/reconcilers" - "k8s.io/utils/env" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" ) var KuadrantExtAuthProviderName = env.GetString("AUTH_PROVIDER", "kuadrant-authorization") // reconcileIstioAuthorizationPolicies translates and reconciles `AuthRules` into an Istio AuthorizationPoilcy containing them. -func (r *AuthPolicyReconciler) reconcileIstioAuthorizationPolicies(ctx context.Context, ap *api.AuthPolicy, targetNetworkObject client.Object, gwDiffObj *reconcilers.GatewayDiff) error { +func (r *AuthPolicyReconciler) reconcileIstioAuthorizationPolicies(ctx context.Context, ap *api.AuthPolicy, targetNetworkObject client.Object, gwDiffObj *reconcilers.GatewayDiffs) error { if err := r.deleteIstioAuthorizationPolicies(ctx, ap, gwDiffObj); err != nil { return err } @@ -52,7 +53,7 @@ func (r *AuthPolicyReconciler) reconcileIstioAuthorizationPolicies(ctx context.C } // deleteIstioAuthorizationPolicies deletes IstioAuthorizationPolicies previously created for gateways no longer targeted by the policy (directly or indirectly) -func (r *AuthPolicyReconciler) deleteIstioAuthorizationPolicies(ctx context.Context, ap *api.AuthPolicy, gwDiffObj *reconcilers.GatewayDiff) error { +func (r *AuthPolicyReconciler) deleteIstioAuthorizationPolicies(ctx context.Context, ap *api.AuthPolicy, gwDiffObj *reconcilers.GatewayDiffs) error { logger, err := logr.FromContext(ctx) if err != nil { return err @@ -78,7 +79,7 @@ func (r *AuthPolicyReconciler) deleteIstioAuthorizationPolicies(ctx context.Cont return nil } -func (r *AuthPolicyReconciler) istioAuthorizationPolicy(ctx context.Context, ap *api.AuthPolicy, targetNetworkObject client.Object, gw common.GatewayWrapper) (*istio.AuthorizationPolicy, error) { +func (r *AuthPolicyReconciler) istioAuthorizationPolicy(ctx context.Context, ap *api.AuthPolicy, targetNetworkObject client.Object, gw kuadrant.GatewayWrapper) (*istio.AuthorizationPolicy, error) { logger, _ := logr.FromContext(ctx) logger = logger.WithName("istioAuthorizationPolicy") @@ -121,7 +122,7 @@ func (r *AuthPolicyReconciler) istioAuthorizationPolicy(ctx context.Context, ap // fake a single httproute with all rules from all httproutes accepted by the gateway, // that do not have an authpolicy of its own, so we can generate wasm rules for those cases rules := make([]gatewayapiv1.HTTPRouteRule, 0) - routes := r.FetchAcceptedGatewayHTTPRoutes(ctx, ap.TargetKey()) + routes := r.TargetRefReconciler.FetchAcceptedGatewayHTTPRoutes(ctx, ap.TargetKey()) for idx := range routes { route := routes[idx] // skip routes that have an authpolicy of its own @@ -151,7 +152,7 @@ func (r *AuthPolicyReconciler) istioAuthorizationPolicy(ctx context.Context, ap if len(rules) > 0 { // make sure all istio authorizationpolicy rules include the hosts so we don't send a request to authorino for hosts that are not in the scope of the policy - hosts := common.HostnamesToStrings(routeHostnames) + hosts := utils.HostnamesToStrings(routeHostnames) for i := range rules { for j := range rules[i].To { if len(rules[i].To[j].Operation.Hosts) > 0 { diff --git a/controllers/authpolicy_status.go b/controllers/authpolicy_status.go index f36ce06f6..7fb323aa2 100644 --- a/controllers/authpolicy_status.go +++ b/controllers/authpolicy_status.go @@ -8,6 +8,7 @@ import ( "slices" "github.com/go-logr/logr" + authorinoapi "github.com/kuadrant/authorino/api/v1beta2" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -16,9 +17,9 @@ import ( gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - authorinoapi "github.com/kuadrant/authorino/api/v1beta2" api "github.com/kuadrant/kuadrant-operator/api/v1beta2" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" ) // reconcileStatus makes sure status block of AuthPolicy is up-to-date. @@ -78,8 +79,8 @@ func (r *AuthPolicyReconciler) calculateStatus(ctx context.Context, ap *api.Auth return newStatus } -func (r *AuthPolicyReconciler) acceptedCondition(policy common.KuadrantPolicy, specErr error) *metav1.Condition { - return common.AcceptedCondition(policy, specErr) +func (r *AuthPolicyReconciler) acceptedCondition(policy kuadrant.Policy, specErr error) *metav1.Condition { + return kuadrant.AcceptedCondition(policy, specErr) } // enforcedCondition checks if the provided AuthPolicy is enforced, ensuring it is properly configured and applied based @@ -100,16 +101,16 @@ func (r *AuthPolicyReconciler) enforcedCondition(ctx context.Context, policy *ap authConfigReady, err := r.isAuthConfigReady(ctx, policy) if err != nil { logger.Error(err, "Failed to check AuthConfig and Gateway") - return common.EnforcedCondition(policy, common.NewErrUnknown(policy.Kind(), err)) + return kuadrant.EnforcedCondition(policy, kuadrant.NewErrUnknown(policy.Kind(), err)) } if !authConfigReady { logger.V(1).Info("AuthConfig is not ready") - return common.EnforcedCondition(policy, common.NewErrUnknown(policy.Kind(), errors.New("AuthScheme is not ready yet"))) + return kuadrant.EnforcedCondition(policy, kuadrant.NewErrUnknown(policy.Kind(), errors.New("AuthScheme is not ready yet"))) } logger.V(1).Info("AuthPolicy is enforced") - return common.EnforcedCondition(policy, nil) + return kuadrant.EnforcedCondition(policy, nil) } // isAuthConfigReady checks if the AuthConfig is ready. @@ -133,15 +134,15 @@ func (r *AuthPolicyReconciler) isAuthConfigReady(ctx context.Context, policy *ap // and creating a corresponding error condition. func (r *AuthPolicyReconciler) handleGatewayPolicyOverride(logger logr.Logger, policy *api.AuthPolicy, targetNetworkObject client.Object) *metav1.Condition { obj := targetNetworkObject.(*gatewayapiv1.Gateway) - gatewayWrapper := common.GatewayWrapper{Gateway: obj, PolicyRefsConfig: &common.KuadrantAuthPolicyRefsConfig{}} + gatewayWrapper := kuadrant.GatewayWrapper{Gateway: obj, Referrer: policy} refs := gatewayWrapper.PolicyRefs() - filteredRef := common.Filter(refs, func(key client.ObjectKey) bool { + filteredRef := utils.Filter(refs, func(key client.ObjectKey) bool { return key != client.ObjectKeyFromObject(policy) }) jsonData, err := json.Marshal(filteredRef) if err != nil { logger.Error(err, "Failed to marshal filtered references") - return common.EnforcedCondition(policy, common.NewErrUnknown(policy.Kind(), err)) + return kuadrant.EnforcedCondition(policy, kuadrant.NewErrUnknown(policy.Kind(), err)) } - return common.EnforcedCondition(policy, common.NewErrOverridden(policy.Kind(), string(jsonData))) + return kuadrant.EnforcedCondition(policy, kuadrant.NewErrOverridden(policy.Kind(), string(jsonData))) } diff --git a/controllers/dns_helper.go b/controllers/dns_helper.go index 3a9253bb8..ca9124d49 100644 --- a/controllers/dns_helper.go +++ b/controllers/dns_helper.go @@ -3,7 +3,6 @@ package controllers import ( "context" "fmt" - "sort" "strconv" "strings" @@ -17,8 +16,10 @@ import ( gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" kuadrantdnsv1alpha1 "github.com/kuadrant/dns-operator/api/v1alpha1" + "github.com/kuadrant/kuadrant-operator/api/v1alpha1" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" "github.com/kuadrant/kuadrant-operator/pkg/multicluster" ) @@ -66,7 +67,7 @@ func findMatchingManagedZone(originalHost, host string, zones []kuadrantdnsv1alp return findMatchingManagedZone(originalHost, parentDomain, zones) } - zone, ok := common.Find(zones, func(zone kuadrantdnsv1alpha1.ManagedZone) bool { + zone, ok := utils.Find(zones, func(zone kuadrantdnsv1alpha1.ManagedZone) bool { return strings.ToLower(zone.Spec.DomainName) == host }) @@ -77,9 +78,9 @@ func findMatchingManagedZone(originalHost, host string, zones []kuadrantdnsv1alp return findMatchingManagedZone(originalHost, parentDomain, zones) } -func commonDNSRecordLabels(gwKey, apKey client.ObjectKey) map[string]string { +func commonDNSRecordLabels(gwKey client.ObjectKey, p *v1alpha1.DNSPolicy) map[string]string { commonLabels := map[string]string{} - for k, v := range policyDNSRecordLabels(apKey) { + for k, v := range policyDNSRecordLabels(p) { commonLabels[k] = v } for k, v := range gatewayDNSRecordLabels(gwKey) { @@ -88,10 +89,10 @@ func commonDNSRecordLabels(gwKey, apKey client.ObjectKey) map[string]string { return commonLabels } -func policyDNSRecordLabels(apKey client.ObjectKey) map[string]string { +func policyDNSRecordLabels(p *v1alpha1.DNSPolicy) map[string]string { return map[string]string{ - common.DNSPolicyBackRefAnnotation: apKey.Name, - fmt.Sprintf("%s-namespace", common.DNSPolicyBackRefAnnotation): apKey.Namespace, + p.DirectReferenceAnnotationName(): p.Name, + fmt.Sprintf("%s-namespace", p.DirectReferenceAnnotationName()): p.Namespace, } } @@ -102,7 +103,7 @@ func gatewayDNSRecordLabels(gwKey client.ObjectKey) map[string]string { } } -func withGatewayListener[T metav1.Object](gateway common.GatewayWrapper, listener gatewayapiv1.Listener, obj T) T { +func withGatewayListener[T metav1.Object](gateway kuadrant.GatewayWrapper, listener gatewayapiv1.Listener, obj T) T { if obj.GetAnnotations() == nil { obj.SetAnnotations(map[string]string{}) } @@ -364,13 +365,13 @@ func isWildCardHost(host string) bool { func (dh *dnsHelper) getDNSHealthCheckProbes(ctx context.Context, gateway *gatewayapiv1.Gateway, dnsPolicy *v1alpha1.DNSPolicy) ([]*kuadrantdnsv1alpha1.DNSHealthCheckProbe, error) { list := &kuadrantdnsv1alpha1.DNSHealthCheckProbeList{} if err := dh.List(ctx, list, &client.ListOptions{ - LabelSelector: labels.SelectorFromSet(commonDNSRecordLabels(client.ObjectKeyFromObject(gateway), client.ObjectKeyFromObject(dnsPolicy))), + LabelSelector: labels.SelectorFromSet(commonDNSRecordLabels(client.ObjectKeyFromObject(gateway), dnsPolicy)), Namespace: dnsPolicy.Namespace, }); err != nil { return nil, err } - return common.Map(list.Items, func(obj kuadrantdnsv1alpha1.DNSHealthCheckProbe) *kuadrantdnsv1alpha1.DNSHealthCheckProbe { + return utils.Map(list.Items, func(obj kuadrantdnsv1alpha1.DNSHealthCheckProbe) *kuadrantdnsv1alpha1.DNSHealthCheckProbe { return &obj }), nil } diff --git a/controllers/dnshealthcheckprobe_eventmapper.go b/controllers/dnshealthcheckprobe_eventmapper.go index ec0c117a9..8b4ecf037 100644 --- a/controllers/dnshealthcheckprobe_eventmapper.go +++ b/controllers/dnshealthcheckprobe_eventmapper.go @@ -1,30 +1,32 @@ package controllers import ( - "context" "fmt" - "github.com/go-logr/logr" - "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" kuadrantdnsv1alpha1 "github.com/kuadrant/dns-operator/api/v1alpha1" + "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/library/mappers" ) -// DNSHealthCheckProbeEventMapper is an EventHandler that maps DNSHealthCheckProbe object events to policy events. -type DNSHealthCheckProbeEventMapper struct { - Logger logr.Logger +var _ mappers.EventMapper = &DNSHealthCheckProbeEventMapper{} + +func NewDNSHealthCheckProbeEventMapper(o ...mappers.MapperOption) mappers.EventMapper { + return &DNSHealthCheckProbeEventMapper{opts: mappers.Apply(o...)} } -func (m *DNSHealthCheckProbeEventMapper) MapToDNSPolicy(_ context.Context, obj client.Object) []reconcile.Request { - return m.mapToPolicyRequest(obj, "dnspolicy", common.DNSPolicyBackRefAnnotation) +// DNSHealthCheckProbeEventMapper is an EventHandler that maps DNSHealthCheckProbe object events to policy events. +type DNSHealthCheckProbeEventMapper struct { + opts mappers.MapperOptions } -func (m *DNSHealthCheckProbeEventMapper) mapToPolicyRequest(obj client.Object, policyKind string, policyBackRefAnnotationName string) []reconcile.Request { - logger := m.Logger.V(1).WithValues("object", client.ObjectKeyFromObject(obj)) +func (m *DNSHealthCheckProbeEventMapper) MapToPolicy(obj client.Object, policyKind kuadrant.Referrer) []reconcile.Request { + logger := m.opts.Logger.V(1).WithValues("object", client.ObjectKeyFromObject(obj)) probe, ok := obj.(*kuadrantdnsv1alpha1.DNSHealthCheckProbe) if !ok { logger.Info("mapToPolicyRequest:", "error", fmt.Sprintf("%T is not a *v1alpha1.DNSHealthCheckProbe", obj)) @@ -33,15 +35,15 @@ func (m *DNSHealthCheckProbeEventMapper) mapToPolicyRequest(obj client.Object, p requests := make([]reconcile.Request, 0) - policyName := common.GetLabel(probe, policyBackRefAnnotationName) + policyName := common.GetLabel(probe, policyKind.DirectReferenceAnnotationName()) if policyName == "" { return requests } - policyNamespace := common.GetLabel(probe, fmt.Sprintf("%s-namespace", policyBackRefAnnotationName)) + policyNamespace := common.GetLabel(probe, fmt.Sprintf("%s-namespace", policyKind.DirectReferenceAnnotationName())) if policyNamespace == "" { return requests } - logger.Info("mapToPolicyRequest", policyKind, policyName) + logger.Info("mapToPolicyRequest", policyKind.Kind(), policyName) requests = append(requests, reconcile.Request{ NamespacedName: types.NamespacedName{ Name: policyName, diff --git a/controllers/dnspolicy_controller.go b/controllers/dnspolicy_controller.go index 2746bea02..0dad8e486 100644 --- a/controllers/dnspolicy_controller.go +++ b/controllers/dnspolicy_controller.go @@ -30,13 +30,16 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/handler" crlog "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" kuadrantdnsv1alpha1 "github.com/kuadrant/dns-operator/api/v1alpha1" "github.com/kuadrant/kuadrant-operator/api/v1alpha1" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/library/mappers" + reconcilerutils "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" "github.com/kuadrant/kuadrant-operator/pkg/reconcilers" ) @@ -49,8 +52,9 @@ type DNSPolicyRefsConfig struct{} // DNSPolicyReconciler reconciles a DNSPolicy object type DNSPolicyReconciler struct { - reconcilers.TargetRefReconciler - dnsHelper dnsHelper + *reconcilers.BaseReconciler + TargetRefReconciler reconcilerutils.TargetRefReconciler + dnsHelper dnsHelper } //+kubebuilder:rbac:groups=kuadrant.io,resources=dnspolicies,verbs=get;list;watch;update;patch;delete @@ -83,7 +87,7 @@ func (r *DNSPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( markedForDeletion := dnsPolicy.GetDeletionTimestamp() != nil - targetNetworkObject, err := r.FetchValidTargetRef(ctx, dnsPolicy.GetTargetRef(), dnsPolicy.Namespace) + targetNetworkObject, err := reconcilerutils.FetchTargetRefObject(ctx, r.Client(), dnsPolicy.GetTargetRef(), dnsPolicy.Namespace) if err != nil { if !markedForDeletion { if apierrors.IsNotFound(err) { @@ -92,7 +96,7 @@ func (r *DNSPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( if delResErr == nil { delResErr = err } - return r.reconcileStatus(ctx, dnsPolicy, common.NewErrTargetNotFound(dnsPolicy.Kind(), dnsPolicy.GetTargetRef(), delResErr)) + return r.reconcileStatus(ctx, dnsPolicy, kuadrant.NewErrTargetNotFound(dnsPolicy.Kind(), dnsPolicy.GetTargetRef(), delResErr)) } return ctrl.Result{}, err } @@ -145,7 +149,7 @@ func (r *DNSPolicyReconciler) reconcileResources(ctx context.Context, dnsPolicy dnsPolicy.Default() // reconcile based on gateway diffs - gatewayDiffObj, err := r.ComputeGatewayDiffs(ctx, dnsPolicy, targetNetworkObject, &common.KuadrantDNSPolicyRefsConfig{}) + gatewayDiffObj, err := reconcilerutils.ComputeGatewayDiffs(ctx, r.Client(), dnsPolicy, targetNetworkObject) if err != nil { return err } @@ -163,14 +167,14 @@ func (r *DNSPolicyReconciler) reconcileResources(ctx context.Context, dnsPolicy } // set direct back ref - i.e. claim the target network object as taken asap - if err = r.ReconcileTargetBackReference(ctx, dnsPolicy, targetNetworkObject, common.DNSPolicyBackRefAnnotation); err != nil { + if err = r.TargetRefReconciler.ReconcileTargetBackReference(ctx, dnsPolicy, targetNetworkObject, dnsPolicy.DirectReferenceAnnotationName()); err != nil { gatewayCondition = BuildPolicyAffectedCondition(DNSPolicyAffected, dnsPolicy, targetNetworkObject, gatewayapiv1alpha2.PolicyReasonConflicted, err) updateErr := r.updateGatewayCondition(ctx, gatewayCondition, gatewayDiffObj) return errors.Join(fmt.Errorf("reconcile TargetBackReference error %w", err), updateErr) } // set annotation of policies affecting the gateway - if err := r.ReconcileGatewayPolicyReferences(ctx, dnsPolicy, gatewayDiffObj); err != nil { + if err := r.TargetRefReconciler.ReconcileGatewayPolicyReferences(ctx, dnsPolicy, gatewayDiffObj); err != nil { gatewayCondition = BuildPolicyAffectedCondition(DNSPolicyAffected, dnsPolicy, targetNetworkObject, gatewayapiv1alpha2.PolicyConditionReason(PolicyReasonUnknown), err) updateErr := r.updateGatewayCondition(ctx, gatewayCondition, gatewayDiffObj) return errors.Join(fmt.Errorf("ReconcileGatewayPolicyReferences error %w", err), updateErr) @@ -197,18 +201,18 @@ func (r *DNSPolicyReconciler) deleteResources(ctx context.Context, dnsPolicy *v1 // remove direct back ref if targetNetworkObject != nil { - if err := r.DeleteTargetBackReference(ctx, targetNetworkObject, common.DNSPolicyBackRefAnnotation); err != nil { + if err := r.TargetRefReconciler.DeleteTargetBackReference(ctx, targetNetworkObject, dnsPolicy.DirectReferenceAnnotationName()); err != nil { return err } } - gatewayDiffObj, err := r.ComputeGatewayDiffs(ctx, dnsPolicy, targetNetworkObject, &common.KuadrantDNSPolicyRefsConfig{}) + gatewayDiffObj, err := reconcilerutils.ComputeGatewayDiffs(ctx, r.Client(), dnsPolicy, targetNetworkObject) if err != nil { return err } // update annotation of policies affecting the gateway - if err := r.ReconcileGatewayPolicyReferences(ctx, dnsPolicy, gatewayDiffObj); err != nil { + if err := r.TargetRefReconciler.ReconcileGatewayPolicyReferences(ctx, dnsPolicy, gatewayDiffObj); err != nil { return err } @@ -216,7 +220,7 @@ func (r *DNSPolicyReconciler) deleteResources(ctx context.Context, dnsPolicy *v1 return r.updateGatewayCondition(ctx, metav1.Condition{Type: DNSPolicyAffected}, gatewayDiffObj) } -func (r *DNSPolicyReconciler) updateGatewayCondition(ctx context.Context, condition metav1.Condition, gatewayDiff *reconcilers.GatewayDiff) error { +func (r *DNSPolicyReconciler) updateGatewayCondition(ctx context.Context, condition metav1.Condition, gatewayDiff *reconcilerutils.GatewayDiffs) error { // update condition if needed gatewayDiffs := append(gatewayDiff.GatewaysWithValidPolicyRef, gatewayDiff.GatewaysMissingPolicyRef...) for i, gw := range gatewayDiffs { @@ -245,23 +249,24 @@ func (r *DNSPolicyReconciler) updateGatewayCondition(ctx context.Context, condit } func (r *DNSPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error { - gatewayEventMapper := &GatewayEventMapper{ - Logger: r.Logger().WithName("gatewayEventMapper"), - } - dnsHealthCheckProbeEventMapper := &DNSHealthCheckProbeEventMapper{ - Logger: r.Logger().WithName("dnsHealthCheckProbeEventMapper"), - } + gatewayEventMapper := mappers.NewGatewayEventMapper(mappers.WithLogger(r.Logger().WithName("gatewayEventMapper"))) + dnsHealthCheckProbeEventMapper := NewDNSHealthCheckProbeEventMapper(mappers.WithLogger(r.Logger().WithName("dnsHealthCheckProbeEventMapper"))) + r.dnsHelper = dnsHelper{Client: r.Client()} ctrlr := ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.DNSPolicy{}). Owns(&kuadrantdnsv1alpha1.DNSRecord{}). Watches( &gatewayapiv1.Gateway{}, - handler.EnqueueRequestsFromMapFunc(gatewayEventMapper.MapToDNSPolicy), + handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, object client.Object) []reconcile.Request { + return gatewayEventMapper.MapToPolicy(object, &v1alpha1.DNSPolicy{}) + }), ). Watches( &kuadrantdnsv1alpha1.DNSHealthCheckProbe{}, - handler.EnqueueRequestsFromMapFunc(dnsHealthCheckProbeEventMapper.MapToDNSPolicy), + handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, object client.Object) []reconcile.Request { + return dnsHealthCheckProbeEventMapper.MapToPolicy(object, &v1alpha1.DNSPolicy{}) + }), ) return ctrlr.Complete(r) } diff --git a/controllers/dnspolicy_controller_test.go b/controllers/dnspolicy_controller_test.go index af2d91f8a..6ebf5e669 100644 --- a/controllers/dnspolicy_controller_test.go +++ b/controllers/dnspolicy_controller_test.go @@ -12,6 +12,7 @@ import ( . "github.com/onsi/gomega" . "github.com/onsi/gomega/gstruct" + kuadrantdnsv1alpha1 "github.com/kuadrant/dns-operator/api/v1alpha1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" @@ -19,9 +20,8 @@ import ( gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - kuadrantdnsv1alpha1 "github.com/kuadrant/dns-operator/api/v1alpha1" "github.com/kuadrant/kuadrant-operator/api/v1alpha1" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" "github.com/kuadrant/kuadrant-operator/pkg/multicluster" ) @@ -119,8 +119,8 @@ var _ = Describe("DNSPolicy controller", func() { Name: fmt.Sprintf("%s-%s-%s", TestIPAddressTwo, TestGatewayName, TestHostTwo), Namespace: testNamespace, Labels: map[string]string{ - common.DNSPolicyBackRefAnnotation: "test-dns-policy", - fmt.Sprintf("%s-namespace", common.DNSPolicyBackRefAnnotation): testNamespace, + v1alpha1.DNSPolicyDirectReferenceAnnotationName: "test-dns-policy", + fmt.Sprintf("%s-namespace", v1alpha1.DNSPolicyDirectReferenceAnnotationName): testNamespace, LabelGatewayNSRef: testNamespace, LabelGatewayReference: "test-gateway", }, @@ -187,7 +187,7 @@ var _ = Describe("DNSPolicy controller", func() { ContainElement(MatchFields(IgnoreExtras, Fields{ "Type": Equal(string(gatewayapiv1alpha2.PolicyConditionAccepted)), "Status": Equal(metav1.ConditionFalse), - "Reason": Equal(string(common.PolicyReasonUnknown)), + "Reason": Equal(string(kuadrant.PolicyReasonUnknown)), "Message": ContainSubstring("gateway is invalid: inconsistent status addresses"), })), ) @@ -244,8 +244,8 @@ var _ = Describe("DNSPolicy controller", func() { gw := &gatewayapiv1.Gateway{} err := k8sClient.Get(ctx, client.ObjectKey{Name: gateway.Name, Namespace: testNamespace}, gw) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(gw.Annotations).To(HaveKeyWithValue(common.DNSPolicyBackRefAnnotation, policyBackRefValue)) - g.Expect(gw.Annotations).To(HaveKeyWithValue(common.DNSPoliciesBackRefAnnotation, policiesBackRefValue)) + g.Expect(gw.Annotations).To(HaveKeyWithValue(v1alpha1.DNSPolicyDirectReferenceAnnotationName, policyBackRefValue)) + g.Expect(gw.Annotations).To(HaveKeyWithValue(v1alpha1.DNSPolicyBackReferenceAnnotationName, policiesBackRefValue)) }, TestTimeoutMedium, time.Second).Should(Succeed()) }) }) @@ -345,8 +345,8 @@ var _ = Describe("DNSPolicy controller", func() { Eventually(func(g Gomega) { err := k8sClient.Get(ctx, client.ObjectKeyFromObject(gateway), gateway) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(gateway.Annotations).To(HaveKeyWithValue(common.DNSPolicyBackRefAnnotation, policyBackRefValue)) - g.Expect(gateway.Annotations).To(HaveKeyWithValue(common.DNSPoliciesBackRefAnnotation, policiesBackRefValue)) + g.Expect(gateway.Annotations).To(HaveKeyWithValue(v1alpha1.DNSPolicyDirectReferenceAnnotationName, policyBackRefValue)) + g.Expect(gateway.Annotations).To(HaveKeyWithValue(v1alpha1.DNSPolicyBackReferenceAnnotationName, policiesBackRefValue)) }, TestTimeoutMedium, time.Second).Should(Succeed()) }) @@ -387,8 +387,8 @@ var _ = Describe("DNSPolicy controller", func() { Eventually(func(g Gomega) { err := k8sClient.Get(ctx, client.ObjectKeyFromObject(gateway), gateway) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(gateway.Annotations).To(HaveKeyWithValue(common.DNSPolicyBackRefAnnotation, policyBackRefValue)) - g.Expect(gateway.Annotations).To(HaveKeyWithValue(common.DNSPoliciesBackRefAnnotation, policiesBackRefValue)) + g.Expect(gateway.Annotations).To(HaveKeyWithValue(v1alpha1.DNSPolicyDirectReferenceAnnotationName, policyBackRefValue)) + g.Expect(gateway.Annotations).To(HaveKeyWithValue(v1alpha1.DNSPolicyBackReferenceAnnotationName, policiesBackRefValue)) g.Expect(gateway.Status.Conditions).To( ContainElement(MatchFields(IgnoreExtras, Fields{ "Type": Equal(DNSPolicyAffected), @@ -409,8 +409,8 @@ var _ = Describe("DNSPolicy controller", func() { Eventually(func(g Gomega) { err := k8sClient.Get(ctx, client.ObjectKeyFromObject(gateway), gateway) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(gateway.Annotations).ToNot(HaveKey(common.DNSPolicyBackRefAnnotation)) - g.Expect(gateway.Annotations).ToNot(HaveKeyWithValue(common.DNSPoliciesBackRefAnnotation, policiesBackRefValue)) + g.Expect(gateway.Annotations).ToNot(HaveKey(v1alpha1.DNSPolicyDirectReferenceAnnotationName)) + g.Expect(gateway.Annotations).ToNot(HaveKeyWithValue(v1alpha1.DNSPolicyBackReferenceAnnotationName, policiesBackRefValue)) g.Expect(gateway.Status.Conditions).ToNot( ContainElement(MatchFields(IgnoreExtras, Fields{ "Type": Equal(string(DNSPolicyAffected)), diff --git a/controllers/dnspolicy_dnsrecords.go b/controllers/dnspolicy_dnsrecords.go index 9bb881de4..e11ac83d9 100644 --- a/controllers/dnspolicy_dnsrecords.go +++ b/controllers/dnspolicy_dnsrecords.go @@ -12,15 +12,16 @@ import ( crlog "sigs.k8s.io/controller-runtime/pkg/log" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - "github.com/kuadrant/kuadrant-operator/pkg/reconcilers" - kuadrantdnsv1alpha1 "github.com/kuadrant/dns-operator/api/v1alpha1" + + reconcilerutils "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" + "github.com/kuadrant/kuadrant-operator/api/v1alpha1" - "github.com/kuadrant/kuadrant-operator/pkg/common" "github.com/kuadrant/kuadrant-operator/pkg/multicluster" ) -func (r *DNSPolicyReconciler) reconcileDNSRecords(ctx context.Context, dnsPolicy *v1alpha1.DNSPolicy, gwDiffObj *reconcilers.GatewayDiff) error { +func (r *DNSPolicyReconciler) reconcileDNSRecords(ctx context.Context, dnsPolicy *v1alpha1.DNSPolicy, gwDiffObj *reconcilerutils.GatewayDiffs) error { log := crlog.FromContext(ctx) log.V(3).Info("reconciling dns records") @@ -69,7 +70,7 @@ func (r *DNSPolicyReconciler) reconcileGatewayDNSRecords(ctx context.Context, gw continue } - listenerGateways := common.Filter(clusterGateways, func(cgw multicluster.ClusterGateway) bool { + listenerGateways := utils.Filter(clusterGateways, func(cgw multicluster.ClusterGateway) bool { hasAttachedRoute := false for _, statusListener := range cgw.Status.Listeners { if string(statusListener.Name) == string(listener.Name) { @@ -113,7 +114,7 @@ func (r *DNSPolicyReconciler) desiredDNSRecord(ctx context.Context, gateway *gat ObjectMeta: metav1.ObjectMeta{ Name: dnsRecordName(gateway.Name, string(targetListener.Name)), Namespace: managedZone.Namespace, - Labels: commonDNSRecordLabels(client.ObjectKeyFromObject(gateway), client.ObjectKeyFromObject(dnsPolicy)), + Labels: commonDNSRecordLabels(client.ObjectKeyFromObject(gateway), dnsPolicy), }, Spec: kuadrantdnsv1alpha1.DNSRecordSpec{ ManagedZoneRef: &kuadrantdnsv1alpha1.ManagedZoneReference{ @@ -142,11 +143,11 @@ func (r *DNSPolicyReconciler) desiredDNSRecord(ctx context.Context, gateway *gat } func (r *DNSPolicyReconciler) deleteGatewayDNSRecords(ctx context.Context, gateway *gatewayapiv1.Gateway, dnsPolicy *v1alpha1.DNSPolicy) error { - return r.deleteDNSRecordsWithLabels(ctx, commonDNSRecordLabels(client.ObjectKeyFromObject(gateway), client.ObjectKeyFromObject(dnsPolicy)), dnsPolicy.Namespace) + return r.deleteDNSRecordsWithLabels(ctx, commonDNSRecordLabels(client.ObjectKeyFromObject(gateway), dnsPolicy), dnsPolicy.Namespace) } func (r *DNSPolicyReconciler) deleteDNSRecords(ctx context.Context, dnsPolicy *v1alpha1.DNSPolicy) error { - return r.deleteDNSRecordsWithLabels(ctx, policyDNSRecordLabels(client.ObjectKeyFromObject(dnsPolicy)), dnsPolicy.Namespace) + return r.deleteDNSRecordsWithLabels(ctx, policyDNSRecordLabels(dnsPolicy), dnsPolicy.Namespace) } func (r *DNSPolicyReconciler) deleteDNSRecordsWithLabels(ctx context.Context, lbls map[string]string, namespace string) error { diff --git a/controllers/dnspolicy_healthchecks.go b/controllers/dnspolicy_healthchecks.go index 1e0a16ebd..8eab4bef4 100644 --- a/controllers/dnspolicy_healthchecks.go +++ b/controllers/dnspolicy_healthchecks.go @@ -17,12 +17,12 @@ import ( kuadrantdnsv1alpha1 "github.com/kuadrant/dns-operator/api/v1alpha1" "github.com/kuadrant/kuadrant-operator/api/v1alpha1" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + reconcilerutils "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" "github.com/kuadrant/kuadrant-operator/pkg/multicluster" - "github.com/kuadrant/kuadrant-operator/pkg/reconcilers" ) -func (r *DNSPolicyReconciler) reconcileHealthCheckProbes(ctx context.Context, dnsPolicy *v1alpha1.DNSPolicy, gwDiffObj *reconcilers.GatewayDiff) error { +func (r *DNSPolicyReconciler) reconcileHealthCheckProbes(ctx context.Context, dnsPolicy *v1alpha1.DNSPolicy, gwDiffObj *reconcilerutils.GatewayDiffs) error { log := crlog.FromContext(ctx) log.V(3).Info("reconciling health checks") @@ -69,11 +69,11 @@ func (r *DNSPolicyReconciler) createOrUpdateHealthCheckProbes(ctx context.Contex } func (r *DNSPolicyReconciler) deleteGatewayHealthCheckProbes(ctx context.Context, gateway *gatewayapiv1.Gateway, dnsPolicy *v1alpha1.DNSPolicy) error { - return r.deleteHealthCheckProbesWithLabels(ctx, commonDNSRecordLabels(client.ObjectKeyFromObject(gateway), client.ObjectKeyFromObject(dnsPolicy)), dnsPolicy.Namespace) + return r.deleteHealthCheckProbesWithLabels(ctx, commonDNSRecordLabels(client.ObjectKeyFromObject(gateway), dnsPolicy), dnsPolicy.Namespace) } func (r *DNSPolicyReconciler) deleteHealthCheckProbes(ctx context.Context, dnsPolicy *v1alpha1.DNSPolicy) error { - return r.deleteHealthCheckProbesWithLabels(ctx, policyDNSRecordLabels(client.ObjectKeyFromObject(dnsPolicy)), dnsPolicy.Namespace) + return r.deleteHealthCheckProbesWithLabels(ctx, policyDNSRecordLabels(dnsPolicy), dnsPolicy.Namespace) } func (r *DNSPolicyReconciler) deleteHealthCheckProbesWithLabels(ctx context.Context, lbls map[string]string, namespace string) error { @@ -93,7 +93,7 @@ func (r *DNSPolicyReconciler) deleteHealthCheckProbesWithLabels(ctx context.Cont func (r *DNSPolicyReconciler) deleteUnexpectedGatewayHealthCheckProbes(ctx context.Context, expectedProbes []*kuadrantdnsv1alpha1.DNSHealthCheckProbe, gateway *gatewayapiv1.Gateway, dnsPolicy *v1alpha1.DNSPolicy) error { // remove any probes for this gateway and DNS Policy that are no longer expected existingProbes := &kuadrantdnsv1alpha1.DNSHealthCheckProbeList{} - dnsLabels := commonDNSRecordLabels(client.ObjectKeyFromObject(gateway), client.ObjectKeyFromObject(dnsPolicy)) + dnsLabels := commonDNSRecordLabels(client.ObjectKeyFromObject(gateway), dnsPolicy) listOptions := &client.ListOptions{LabelSelector: labels.SelectorFromSet(dnsLabels)} if err := r.Client().List(ctx, existingProbes, listOptions); client.IgnoreNotFound(err) != nil { return err @@ -110,7 +110,7 @@ func (r *DNSPolicyReconciler) deleteUnexpectedGatewayHealthCheckProbes(ctx conte return nil } -func (r *DNSPolicyReconciler) expectedHealthCheckProbesForGateway(ctx context.Context, gw common.GatewayWrapper, dnsPolicy *v1alpha1.DNSPolicy) []*kuadrantdnsv1alpha1.DNSHealthCheckProbe { +func (r *DNSPolicyReconciler) expectedHealthCheckProbesForGateway(ctx context.Context, gw kuadrant.GatewayWrapper, dnsPolicy *v1alpha1.DNSPolicy) []*kuadrantdnsv1alpha1.DNSHealthCheckProbe { log := crlog.FromContext(ctx) var healthChecks []*kuadrantdnsv1alpha1.DNSHealthCheckProbe if dnsPolicy.Spec.HealthCheck == nil { @@ -157,7 +157,7 @@ func (r *DNSPolicyReconciler) expectedHealthCheckProbesForGateway(ctx context.Co ObjectMeta: metav1.ObjectMeta{ Name: dnsHealthCheckProbeName(address.Value, gw.Name, string(listener.Name)), Namespace: gw.Namespace, - Labels: commonDNSRecordLabels(client.ObjectKeyFromObject(gw), client.ObjectKeyFromObject(dnsPolicy)), + Labels: commonDNSRecordLabels(client.ObjectKeyFromObject(gw), dnsPolicy), }, Spec: kuadrantdnsv1alpha1.DNSHealthCheckProbeSpec{ Port: *port, diff --git a/controllers/dnspolicy_status.go b/controllers/dnspolicy_status.go index 38a59aa6e..7bc0426fd 100644 --- a/controllers/dnspolicy_status.go +++ b/controllers/dnspolicy_status.go @@ -27,7 +27,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/kuadrant/kuadrant-operator/api/v1alpha1" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" ) func (r *DNSPolicyReconciler) reconcileStatus(ctx context.Context, dnsPolicy *v1alpha1.DNSPolicy, specErr error) (ctrl.Result, error) { @@ -50,7 +50,7 @@ func (r *DNSPolicyReconciler) reconcileStatus(ctx context.Context, dnsPolicy *v1 return ctrl.Result{}, updateErr } - if common.IsTargetNotFound(specErr) { + if kuadrant.IsTargetNotFound(specErr) { return ctrl.Result{Requeue: true}, nil } @@ -64,7 +64,7 @@ func (r *DNSPolicyReconciler) calculateStatus(dnsPolicy *v1alpha1.DNSPolicy, spe ObservedGeneration: dnsPolicy.Status.ObservedGeneration, } - acceptedCond := common.AcceptedCondition(dnsPolicy, specErr) + acceptedCond := kuadrant.AcceptedCondition(dnsPolicy, specErr) meta.SetStatusCondition(&newStatus.Conditions, *acceptedCond) return newStatus diff --git a/controllers/gateway_eventmapper.go b/controllers/gateway_eventmapper.go deleted file mode 100644 index a52ba0f30..000000000 --- a/controllers/gateway_eventmapper.go +++ /dev/null @@ -1,55 +0,0 @@ -package controllers - -import ( - "context" - "fmt" - - "github.com/go-logr/logr" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - - "github.com/kuadrant/kuadrant-operator/pkg/common" -) - -// GatewayEventMapper is an EventHandler that maps Gateway object events to policy events. -type GatewayEventMapper struct { - Logger logr.Logger -} - -func (m *GatewayEventMapper) MapToRateLimitPolicy(_ context.Context, obj client.Object) []reconcile.Request { - return m.mapToPolicyRequest(obj, "ratelimitpolicy", &common.KuadrantRateLimitPolicyRefsConfig{}) -} - -func (m *GatewayEventMapper) MapToAuthPolicy(_ context.Context, obj client.Object) []reconcile.Request { - return m.mapToPolicyRequest(obj, "authpolicy", &common.KuadrantAuthPolicyRefsConfig{}) -} - -func (m *GatewayEventMapper) MapToTLSPolicy(_ context.Context, obj client.Object) []reconcile.Request { - return m.mapToPolicyRequest(obj, "tlspolicy", &common.KuadrantTLSPolicyRefsConfig{}) -} - -func (m *GatewayEventMapper) MapToDNSPolicy(_ context.Context, obj client.Object) []reconcile.Request { - return m.mapToPolicyRequest(obj, "dnspolicy", &common.KuadrantDNSPolicyRefsConfig{}) -} - -func (m *GatewayEventMapper) mapToPolicyRequest(obj client.Object, policyKind string, policyRefsConfig common.PolicyRefsConfig) []reconcile.Request { - logger := m.Logger.V(1).WithValues("object", client.ObjectKeyFromObject(obj)) - - gateway, ok := obj.(*gatewayapiv1.Gateway) - if !ok { - logger.Info("mapToPolicyRequest:", "error", fmt.Sprintf("%T is not a *gatewayapiv1.Gateway", obj)) - return []reconcile.Request{} - } - - gw := common.GatewayWrapper{Gateway: gateway, PolicyRefsConfig: policyRefsConfig} - - requests := make([]reconcile.Request, 0) - - for _, policyKey := range gw.PolicyRefs() { - logger.Info("mapToPolicyRequest", policyKind, policyKey) - requests = append(requests, reconcile.Request{NamespacedName: policyKey}) - } - - return requests -} diff --git a/controllers/gateway_kuadrant_controller.go b/controllers/gateway_kuadrant_controller.go index 21a5cd919..b0d25e87d 100644 --- a/controllers/gateway_kuadrant_controller.go +++ b/controllers/gateway_kuadrant_controller.go @@ -30,7 +30,7 @@ import ( gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" "github.com/kuadrant/kuadrant-operator/pkg/reconcilers" ) @@ -98,7 +98,7 @@ func (r *GatewayKuadrantReconciler) reconcileKuadrantNamespaceAnnotation(ctx con return false, err } - if common.IsKuadrantManaged(gw) { + if kuadrant.IsKuadrantManaged(gw) { return false, nil } @@ -122,7 +122,7 @@ func (r *GatewayKuadrantReconciler) reconcileKuadrantNamespaceAnnotation(ctx con return false, nil } - common.AnnotateObject(gw, kuadrantList.Items[0].Namespace) + kuadrant.AnnotateObject(gw, kuadrantList.Items[0].Namespace) return true, nil } diff --git a/controllers/gateway_kuadrant_controller_test.go b/controllers/gateway_kuadrant_controller_test.go index bf10dce01..20e731287 100644 --- a/controllers/gateway_kuadrant_controller_test.go +++ b/controllers/gateway_kuadrant_controller_test.go @@ -15,7 +15,7 @@ import ( gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" ) var _ = Describe("Kuadrant Gateway controller", func() { @@ -47,7 +47,7 @@ var _ = Describe("Kuadrant Gateway controller", func() { return false } - if meta.IsStatusConditionFalse(existingGateway.Status.Conditions, common.GatewayProgrammedConditionType) { + if meta.IsStatusConditionFalse(existingGateway.Status.Conditions, string(gatewayapiv1.GatewayConditionProgrammed)) { logf.Log.V(1).Info("[WARN] Gateway not ready") return false } @@ -63,7 +63,7 @@ var _ = Describe("Kuadrant Gateway controller", func() { logf.Log.V(1).Info("[WARN] Getting gateway failed", "error", err) return false } - return common.IsKuadrantManaged(existingGateway) + return kuadrant.IsKuadrantManaged(existingGateway) }, 15*time.Second, 5*time.Second).Should(BeTrue()) }) }) @@ -109,7 +109,7 @@ var _ = Describe("Kuadrant Gateway controller", func() { return false } - if meta.IsStatusConditionFalse(existingGateway.Status.Conditions, common.GatewayProgrammedConditionType) { + if meta.IsStatusConditionFalse(existingGateway.Status.Conditions, string(gatewayapiv1.GatewayConditionProgrammed)) { logf.Log.V(1).Info("[WARN] Gateway not ready") return false } @@ -125,7 +125,7 @@ var _ = Describe("Kuadrant Gateway controller", func() { logf.Log.V(1).Info("[WARN] Getting gateway failed", "error", err) return false } - return !common.IsKuadrantManaged(existingGateway) + return !kuadrant.IsKuadrantManaged(existingGateway) }, 15*time.Second, 5*time.Second).Should(BeTrue()) }) }) diff --git a/controllers/helper_test.go b/controllers/helper_test.go index fe7c2500d..9c39f090b 100644 --- a/controllers/helper_test.go +++ b/controllers/helper_test.go @@ -38,7 +38,8 @@ import ( kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" kuadrantv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" ) const ( @@ -250,7 +251,7 @@ func testBuildBasicHttpRoute(routeName, gwName, ns string, hostnames []string) * }, }, }, - Hostnames: common.Map(hostnames, func(hostname string) gatewayapiv1.Hostname { return gatewayapiv1.Hostname(hostname) }), + Hostnames: utils.Map(hostnames, func(hostname string) gatewayapiv1.Hostname { return gatewayapiv1.Hostname(hostname) }), Rules: []gatewayapiv1.HTTPRouteRule{ { Matches: []gatewayapiv1.HTTPRouteMatch{ @@ -308,7 +309,7 @@ func testRouteIsAccepted(routeKey client.ObjectKey) func() bool { return func() bool { route := &gatewayapiv1.HTTPRoute{} err := k8sClient.Get(context.Background(), routeKey, route) - return err == nil && common.IsHTTPRouteAccepted(route) + return err == nil && kuadrant.IsHTTPRouteAccepted(route) } } @@ -316,7 +317,7 @@ func testGatewayIsReady(gateway *gatewayapiv1.Gateway) func() bool { return func() bool { existingGateway := &gatewayapiv1.Gateway{} err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(gateway), existingGateway) - return err == nil && meta.IsStatusConditionTrue(existingGateway.Status.Conditions, common.GatewayProgrammedConditionType) + return err == nil && meta.IsStatusConditionTrue(existingGateway.Status.Conditions, string(gatewayapiv1.GatewayConditionProgrammed)) } } diff --git a/controllers/httproute_eventmapper.go b/controllers/httproute_eventmapper.go deleted file mode 100644 index 823a92af3..000000000 --- a/controllers/httproute_eventmapper.go +++ /dev/null @@ -1,37 +0,0 @@ -package controllers - -import ( - "context" - - "github.com/go-logr/logr" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/kuadrant/kuadrant-operator/pkg/common" -) - -// HTTPRouteEventMapper is an EventHandler that maps HTTPRoute object events to Policy events. -type HTTPRouteEventMapper struct { - Logger logr.Logger -} - -func (m *HTTPRouteEventMapper) MapToRateLimitPolicy(_ context.Context, obj client.Object) []reconcile.Request { - return m.mapToPolicyRequest(obj, "ratelimitpolicy", common.RateLimitPolicyBackRefAnnotation) -} - -func (m *HTTPRouteEventMapper) MapToAuthPolicy(_ context.Context, obj client.Object) []reconcile.Request { - return m.mapToPolicyRequest(obj, "authpolicy", common.AuthPolicyBackRefAnnotation) -} - -func (m *HTTPRouteEventMapper) mapToPolicyRequest(obj client.Object, policyKind, policyBackRefAnnotationName string) []reconcile.Request { - policyRef, found := common.ReadAnnotationsFromObject(obj)[policyBackRefAnnotationName] - if !found { - return []reconcile.Request{} - } - - policyKey := common.NamespacedNameToObjectKey(policyRef, obj.GetNamespace()) - - m.Logger.V(1).Info("Processing object", "object", client.ObjectKeyFromObject(obj), policyKind, policyKey) - - return []reconcile.Request{{NamespacedName: policyKey}} -} diff --git a/controllers/httprouteparentrefs_eventmapper.go b/controllers/httprouteparentrefs_eventmapper.go index 70bbed511..bd606416d 100644 --- a/controllers/httprouteparentrefs_eventmapper.go +++ b/controllers/httprouteparentrefs_eventmapper.go @@ -10,7 +10,7 @@ import ( gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" api "github.com/kuadrant/kuadrant-operator/api/v1beta2" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" ) // HTTPRouteParentRefsEventMapper is an EventHandler that maps HTTPRoute events to policy events, @@ -45,7 +45,7 @@ func (m *HTTPRouteParentRefsEventMapper) mapToPolicyRequest(obj client.Object, p for _, parentRef := range route.Spec.ParentRefs { // skips if parentRef is not a Gateway - if (parentRef.Group != nil && *parentRef.Group != "gateway.networking.k8s.io") || (parentRef.Kind != nil && *parentRef.Kind != "Gateway") { + if (parentRef.Group != nil && *parentRef.Group != gatewayapiv1.GroupName) || (parentRef.Kind != nil && *parentRef.Kind != "Gateway") { continue } // list policies in the same namespace as the parent gateway of the route @@ -58,14 +58,14 @@ func (m *HTTPRouteParentRefsEventMapper) mapToPolicyRequest(obj client.Object, p logger.Error(err, "failed to list policies") } // triggers the reconciliation of any policy that targets the parent gateway of the route - policies, ok := policyList.(common.KuadrantPolicyList) + policies, ok := policyList.(kuadrant.PolicyList) if !ok { - logger.Info("mapToPolicyRequest:", "error", fmt.Sprintf("%T is not a KuadrantPolicyList", policyList)) + logger.Info("mapToPolicyRequest:", "error", fmt.Sprintf("%T is not a PolicyList", policyList)) continue } for _, policy := range policies.GetItems() { targetRef := policy.GetTargetRef() - if !common.IsTargetRefGateway(targetRef) { + if !kuadrant.IsTargetRefGateway(targetRef) { continue } targetRefNamespace := targetRef.Namespace diff --git a/controllers/kuadrant_controller.go b/controllers/kuadrant_controller.go index 17a0e5162..9ba1e01c0 100644 --- a/controllers/kuadrant_controller.go +++ b/controllers/kuadrant_controller.go @@ -20,31 +20,31 @@ import ( "context" "encoding/json" - "github.com/kuadrant/kuadrant-operator/pkg/kuadranttools" - corev1 "k8s.io/api/core/v1" - "k8s.io/utils/env" - "github.com/go-logr/logr" authorinov1beta1 "github.com/kuadrant/authorino-operator/api/v1beta1" - maistrav1 "github.com/kuadrant/kuadrant-operator/api/external/maistra/v1" - maistrav2 "github.com/kuadrant/kuadrant-operator/api/external/maistra/v2" limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" "golang.org/x/sync/errgroup" iopv1alpha1 "istio.io/istio/operator/pkg/apis/istio/v1alpha1" appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/env" istiov1alpha1 "maistra.io/istio-operator/api/v1alpha1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + maistrav1 "github.com/kuadrant/kuadrant-operator/api/external/maistra/v1" + maistrav2 "github.com/kuadrant/kuadrant-operator/api/external/maistra/v2" kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" "github.com/kuadrant/kuadrant-operator/pkg/common" "github.com/kuadrant/kuadrant-operator/pkg/istio" + "github.com/kuadrant/kuadrant-operator/pkg/kuadranttools" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" "github.com/kuadrant/kuadrant-operator/pkg/log" "github.com/kuadrant/kuadrant-operator/pkg/reconcilers" ) @@ -421,8 +421,8 @@ func (r *KuadrantReconciler) reconcileClusterGateways(ctx context.Context, kObj for i := range gwList.Items { gw := &gwList.Items[i] - if !common.IsKuadrantManaged(gw) { - common.AnnotateObject(gw, kObj.Namespace) + if !kuadrant.IsKuadrantManaged(gw) { + kuadrant.AnnotateObject(gw, kObj.Namespace) errGroup.Go(func() error { select { case <-gctx.Done(): @@ -457,7 +457,7 @@ func (r *KuadrantReconciler) removeAnnotationFromGateways(ctx context.Context, k return nil default: // When RFC defined, we could indicate which gateways based on a specific annotation/label - common.DeleteKuadrantAnnotationFromGateway(gw, kObj.Namespace) + kuadrant.DeleteKuadrantAnnotationFromGateway(gw, kObj.Namespace) if err := r.Client().Update(ctx, gw); err != nil { return err } diff --git a/controllers/limitador_cluster_envoyfilter_controller.go b/controllers/limitador_cluster_envoyfilter_controller.go index 6f6a5cc20..ede8de209 100644 --- a/controllers/limitador_cluster_envoyfilter_controller.go +++ b/controllers/limitador_cluster_envoyfilter_controller.go @@ -26,7 +26,7 @@ import ( istioapinetworkingv1alpha3 "istio.io/api/networking/v1alpha3" istioclientnetworkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" apierrors "k8s.io/apimachinery/pkg/api/errors" - meta "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" @@ -34,8 +34,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + "github.com/kuadrant/kuadrant-operator/api/v1beta2" "github.com/kuadrant/kuadrant-operator/pkg/common" kuadrantistioutils "github.com/kuadrant/kuadrant-operator/pkg/istio" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" "github.com/kuadrant/kuadrant-operator/pkg/reconcilers" ) @@ -120,7 +122,7 @@ func (r *LimitadorClusterEnvoyFilterReconciler) desiredRateLimitingClusterEnvoyF }, } - gateway := common.GatewayWrapper{Gateway: gw, PolicyRefsConfig: &common.KuadrantRateLimitPolicyRefsConfig{}} + gateway := kuadrant.GatewayWrapper{Gateway: gw, Referrer: &v1beta2.RateLimitPolicy{}} rlpRefs := gateway.PolicyRefs() logger.V(1).Info("desiredRateLimitingClusterEnvoyFilter", "rlpRefs", rlpRefs) @@ -129,7 +131,7 @@ func (r *LimitadorClusterEnvoyFilterReconciler) desiredRateLimitingClusterEnvoyF return ef, nil } - kuadrantNamespace, err := common.GetKuadrantNamespace(gw) + kuadrantNamespace, err := kuadrant.GetKuadrantNamespace(gw) if err != nil { return nil, err } diff --git a/controllers/limitador_cluster_envoyfilter_controller_test.go b/controllers/limitador_cluster_envoyfilter_controller_test.go index 98fb86c57..a932aad82 100644 --- a/controllers/limitador_cluster_envoyfilter_controller_test.go +++ b/controllers/limitador_cluster_envoyfilter_controller_test.go @@ -7,6 +7,7 @@ import ( "fmt" "time" + limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" istioclientnetworkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" @@ -20,7 +21,6 @@ import ( kuadrantv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" "github.com/kuadrant/kuadrant-operator/pkg/common" - limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" ) var _ = Describe("Limitador Cluster EnvoyFilter controller", func() { @@ -45,7 +45,7 @@ var _ = Describe("Limitador Cluster EnvoyFilter controller", func() { return false } - if meta.IsStatusConditionFalse(existingGateway.Status.Conditions, common.GatewayProgrammedConditionType) { + if meta.IsStatusConditionFalse(existingGateway.Status.Conditions, string(gatewayapiv1.GatewayConditionProgrammed)) { logf.Log.V(1).Info("[WARN] Gateway not ready") return false } @@ -86,7 +86,7 @@ var _ = Describe("Limitador Cluster EnvoyFilter controller", func() { }, Spec: kuadrantv1beta2.RateLimitPolicySpec{ TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ - Group: gatewayapiv1.Group("gateway.networking.k8s.io"), + Group: gatewayapiv1.GroupName, Kind: "Gateway", Name: gatewayapiv1.ObjectName(gwName), }, diff --git a/controllers/ratelimitpolicy_controller.go b/controllers/ratelimitpolicy_controller.go index 88ac46e30..e316126e5 100644 --- a/controllers/ratelimitpolicy_controller.go +++ b/controllers/ratelimitpolicy_controller.go @@ -26,10 +26,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" kuadrantv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/library/mappers" + reconcilerutils "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" "github.com/kuadrant/kuadrant-operator/pkg/reconcilers" ) @@ -37,7 +40,8 @@ const rateLimitPolicyFinalizer = "ratelimitpolicy.kuadrant.io/finalizer" // RateLimitPolicyReconciler reconciles a RateLimitPolicy object type RateLimitPolicyReconciler struct { - reconcilers.TargetRefReconciler + *reconcilers.BaseReconciler + TargetRefReconciler reconcilerutils.TargetRefReconciler } //+kubebuilder:rbac:groups=kuadrant.io,resources=ratelimitpolicies,verbs=get;list;watch;create;update;patch;delete @@ -84,7 +88,7 @@ func (r *RateLimitPolicyReconciler) Reconcile(eventCtx context.Context, req ctrl markedForDeletion := rlp.GetDeletionTimestamp() != nil // fetch the target network object - targetNetworkObject, err := r.FetchValidTargetRef(ctx, rlp.GetTargetRef(), rlp.Namespace) + targetNetworkObject, err := reconcilerutils.FetchTargetRefObject(ctx, r.Client(), rlp.GetTargetRef(), rlp.Namespace) if err != nil { if !markedForDeletion { if apierrors.IsNotFound(err) { @@ -93,7 +97,7 @@ func (r *RateLimitPolicyReconciler) Reconcile(eventCtx context.Context, req ctrl if delResErr == nil { delResErr = err } - return r.reconcileStatus(ctx, rlp, common.NewErrTargetNotFound(rlp.Kind(), rlp.GetTargetRef(), delResErr)) + return r.reconcileStatus(ctx, rlp, kuadrant.NewErrTargetNotFound(rlp.Kind(), rlp.GetTargetRef(), delResErr)) } return ctrl.Result{}, err } @@ -152,11 +156,11 @@ func (r *RateLimitPolicyReconciler) Reconcile(eventCtx context.Context, req ctrl // validate performs validation before proceeding with the reconcile loop, returning a common.ErrInvalid on failing validation func (r *RateLimitPolicyReconciler) validate(rlp *kuadrantv1beta2.RateLimitPolicy, targetNetworkObject client.Object) error { if err := rlp.Validate(); err != nil { - return common.NewErrInvalid(rlp.Kind(), err) + return kuadrant.NewErrInvalid(rlp.Kind(), err) } - if err := common.ValidateHierarchicalRules(rlp, targetNetworkObject); err != nil { - return common.NewErrInvalid(rlp.Kind(), err) + if err := kuadrant.ValidateHierarchicalRules(rlp, targetNetworkObject); err != nil { + return kuadrant.NewErrInvalid(rlp.Kind(), err) } return nil @@ -168,7 +172,7 @@ func (r *RateLimitPolicyReconciler) reconcileResources(ctx context.Context, rlp } // reconcile based on gateway diffs - gatewayDiffObj, err := r.ComputeGatewayDiffs(ctx, rlp, targetNetworkObject, &common.KuadrantRateLimitPolicyRefsConfig{}) + gatewayDiffObj, err := reconcilerutils.ComputeGatewayDiffs(ctx, r.Client(), rlp, targetNetworkObject) if err != nil { return err } @@ -187,12 +191,12 @@ func (r *RateLimitPolicyReconciler) reconcileResources(ctx context.Context, rlp } // set annotation of policies affecting the gateway - should be the last step, only when all the reconciliation steps succeed - return r.ReconcileGatewayPolicyReferences(ctx, rlp, gatewayDiffObj) + return r.TargetRefReconciler.ReconcileGatewayPolicyReferences(ctx, rlp, gatewayDiffObj) } func (r *RateLimitPolicyReconciler) deleteResources(ctx context.Context, rlp *kuadrantv1beta2.RateLimitPolicy, targetNetworkObject client.Object) error { // delete based on gateway diffs - gatewayDiffObj, err := r.ComputeGatewayDiffs(ctx, rlp, targetNetworkObject, &common.KuadrantRateLimitPolicyRefsConfig{}) + gatewayDiffObj, err := reconcilerutils.ComputeGatewayDiffs(ctx, r.Client(), rlp, targetNetworkObject) if err != nil { return err } @@ -207,43 +211,44 @@ func (r *RateLimitPolicyReconciler) deleteResources(ctx context.Context, rlp *ku // remove direct back ref if targetNetworkObject != nil { - if err := r.deleteNetworkResourceDirectBackReference(ctx, targetNetworkObject); err != nil { + if err := r.deleteNetworkResourceDirectBackReference(ctx, targetNetworkObject, rlp); err != nil { return err } } - // update annotation of policies afftecting the gateway - return r.ReconcileGatewayPolicyReferences(ctx, rlp, gatewayDiffObj) + // update annotation of policies affecting the gateway + return r.TargetRefReconciler.ReconcileGatewayPolicyReferences(ctx, rlp, gatewayDiffObj) } // Ensures only one RLP targets the network resource -func (r *RateLimitPolicyReconciler) reconcileNetworkResourceDirectBackReference(ctx context.Context, policy common.KuadrantPolicy, targetNetworkObject client.Object) error { - return r.ReconcileTargetBackReference(ctx, policy, targetNetworkObject, common.RateLimitPolicyBackRefAnnotation) +func (r *RateLimitPolicyReconciler) reconcileNetworkResourceDirectBackReference(ctx context.Context, policy *kuadrantv1beta2.RateLimitPolicy, targetNetworkObject client.Object) error { + return r.TargetRefReconciler.ReconcileTargetBackReference(ctx, policy, targetNetworkObject, policy.DirectReferenceAnnotationName()) } -func (r *RateLimitPolicyReconciler) deleteNetworkResourceDirectBackReference(ctx context.Context, targetNetworkObject client.Object) error { - return r.DeleteTargetBackReference(ctx, targetNetworkObject, common.RateLimitPolicyBackRefAnnotation) +func (r *RateLimitPolicyReconciler) deleteNetworkResourceDirectBackReference(ctx context.Context, targetNetworkObject client.Object, policy *kuadrantv1beta2.RateLimitPolicy) error { + return r.TargetRefReconciler.DeleteTargetBackReference(ctx, targetNetworkObject, policy.DirectReferenceAnnotationName()) } // SetupWithManager sets up the controller with the Manager. func (r *RateLimitPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error { - httpRouteEventMapper := &HTTPRouteEventMapper{ - Logger: r.Logger().WithName("httpRouteEventMapper"), - } - gatewayEventMapper := &GatewayEventMapper{ - Logger: r.Logger().WithName("gatewayEventMapper"), - } + httpRouteEventMapper := mappers.NewHTTPRouteEventMapper(mappers.WithLogger(r.Logger().WithName("httpRouteEventMapper"))) + gatewayEventMapper := mappers.NewGatewayEventMapper(mappers.WithLogger(r.Logger().WithName("gatewayEventMapper"))) + return ctrl.NewControllerManagedBy(mgr). For(&kuadrantv1beta2.RateLimitPolicy{}). Watches( &gatewayapiv1.HTTPRoute{}, - handler.EnqueueRequestsFromMapFunc(httpRouteEventMapper.MapToRateLimitPolicy), + handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, object client.Object) []reconcile.Request { + return httpRouteEventMapper.MapToPolicy(object, &kuadrantv1beta2.RateLimitPolicy{}) + }), ). // Currently the purpose is to generate events when rlp references change in gateways // so the status of the rlps targeting a route can be keep in sync Watches( &gatewayapiv1.Gateway{}, - handler.EnqueueRequestsFromMapFunc(gatewayEventMapper.MapToRateLimitPolicy), + handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, object client.Object) []reconcile.Request { + return gatewayEventMapper.MapToPolicy(object, &kuadrantv1beta2.RateLimitPolicy{}) + }), ). Complete(r) } diff --git a/controllers/ratelimitpolicy_controller_test.go b/controllers/ratelimitpolicy_controller_test.go index a8b04c8d4..41c954f87 100644 --- a/controllers/ratelimitpolicy_controller_test.go +++ b/controllers/ratelimitpolicy_controller_test.go @@ -9,10 +9,6 @@ import ( "strings" "time" - kuadrantv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" - "github.com/kuadrant/kuadrant-operator/pkg/common" - "github.com/kuadrant/kuadrant-operator/pkg/rlptools" - "github.com/kuadrant/kuadrant-operator/pkg/rlptools/wasm" limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -25,6 +21,11 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + kuadrantv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" + "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/rlptools" + "github.com/kuadrant/kuadrant-operator/pkg/rlptools/wasm" ) var _ = Describe("RateLimitPolicy controller", func() { @@ -48,7 +49,7 @@ var _ = Describe("RateLimitPolicy controller", func() { }, Spec: kuadrantv1beta2.RateLimitPolicySpec{ TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ - Group: gatewayapiv1.Group("gateway.networking.k8s.io"), + Group: gatewayapiv1.GroupName, Kind: "HTTPRoute", Name: gatewayapiv1.ObjectName(routeName), }, @@ -84,7 +85,7 @@ var _ = Describe("RateLimitPolicy controller", func() { return false } - if meta.IsStatusConditionFalse(existingGateway.Status.Conditions, common.GatewayProgrammedConditionType) { + if meta.IsStatusConditionFalse(existingGateway.Status.Conditions, string(gatewayapiv1.GatewayConditionProgrammed)) { logf.Log.V(1).Info("[WARN] Gateway not ready") return false } @@ -135,7 +136,7 @@ var _ = Describe("RateLimitPolicy controller", func() { // must exist Expect(err).ToNot(HaveOccurred()) Expect(existingRoute.GetAnnotations()).To(HaveKeyWithValue( - common.RateLimitPolicyBackRefAnnotation, client.ObjectKeyFromObject(rlp).String())) + rlp.DirectReferenceAnnotationName(), client.ObjectKeyFromObject(rlp).String())) // check limits limitadorKey := client.ObjectKey{Name: common.LimitadorName, Namespace: testNamespace} @@ -211,7 +212,7 @@ var _ = Describe("RateLimitPolicy controller", func() { serialized, err := json.Marshal(refs) Expect(err).ToNot(HaveOccurred()) Expect(existingGateway.GetAnnotations()).To(HaveKeyWithValue( - common.RateLimitPoliciesBackRefAnnotation, string(serialized))) + rlp.BackReferenceAnnotationName(), string(serialized))) }) It("Creates the correct WasmPlugin for a complex HTTPRoute and a RateLimitPolicy", func() { @@ -439,7 +440,7 @@ var _ = Describe("RateLimitPolicy controller", func() { // must exist Expect(err).ToNot(HaveOccurred()) Expect(existingGateway.GetAnnotations()).To(HaveKeyWithValue( - common.RateLimitPolicyBackRefAnnotation, client.ObjectKeyFromObject(rlp).String())) + rlp.DirectReferenceAnnotationName(), client.ObjectKeyFromObject(rlp).String())) // check limits limitadorKey := client.ObjectKey{Name: common.LimitadorName, Namespace: testNamespace} @@ -512,7 +513,7 @@ var _ = Describe("RateLimitPolicy controller", func() { refs := []client.ObjectKey{rlpKey} serialized, err := json.Marshal(refs) Expect(err).ToNot(HaveOccurred()) - Expect(existingGateway.GetAnnotations()).To(HaveKeyWithValue(common.RateLimitPoliciesBackRefAnnotation, string(serialized))) + Expect(existingGateway.GetAnnotations()).To(HaveKeyWithValue(rlp.BackReferenceAnnotationName(), string(serialized))) }) It("Creates all the resources for a basic Gateway and RateLimitPolicy when missing a HTTPRoute attached to the Gateway", func() { @@ -535,7 +536,7 @@ var _ = Describe("RateLimitPolicy controller", func() { // must exist Expect(err).ToNot(HaveOccurred()) Expect(existingGateway.GetAnnotations()).To(HaveKeyWithValue( - common.RateLimitPolicyBackRefAnnotation, client.ObjectKeyFromObject(rlp).String())) + rlp.DirectReferenceAnnotationName(), client.ObjectKeyFromObject(rlp).String())) // check limits limitadorKey := client.ObjectKey{Name: common.LimitadorName, Namespace: testNamespace} @@ -568,7 +569,7 @@ var _ = Describe("RateLimitPolicy controller", func() { refs := []client.ObjectKey{rlpKey} serialized, err := json.Marshal(refs) Expect(err).ToNot(HaveOccurred()) - Expect(existingGateway.GetAnnotations()).To(HaveKeyWithValue(common.RateLimitPoliciesBackRefAnnotation, string(serialized))) + Expect(existingGateway.GetAnnotations()).To(HaveKeyWithValue(rlp.BackReferenceAnnotationName(), string(serialized))) }) }) @@ -660,7 +661,7 @@ var _ = Describe("RateLimitPolicy CEL Validations", func() { }, Spec: kuadrantv1beta2.RateLimitPolicySpec{ TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ - Group: "gateway.networking.k8s.io", + Group: gatewayapiv1.GroupName, Kind: "HTTPRoute", Name: "my-target", }, @@ -718,7 +719,7 @@ var _ = Describe("RateLimitPolicy CEL Validations", func() { }, Spec: kuadrantv1beta2.RateLimitPolicySpec{ TargetRef: gatewayapiv1alpha2.PolicyTargetReference{ - Group: "gateway.networking.k8s.io", + Group: gatewayapiv1.GroupName, Kind: "Gateway", Name: "my-gw", }, diff --git a/controllers/ratelimitpolicy_istio_wasmplugin.go b/controllers/ratelimitpolicy_istio_wasmplugin.go index 5802b8d3f..452aa4dfe 100644 --- a/controllers/ratelimitpolicy_istio_wasmplugin.go +++ b/controllers/ratelimitpolicy_istio_wasmplugin.go @@ -14,12 +14,14 @@ import ( kuadrantv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" "github.com/kuadrant/kuadrant-operator/pkg/common" - "github.com/kuadrant/kuadrant-operator/pkg/reconcilers" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" "github.com/kuadrant/kuadrant-operator/pkg/rlptools" "github.com/kuadrant/kuadrant-operator/pkg/rlptools/wasm" ) -func (r *RateLimitPolicyReconciler) reconcileWASMPluginConf(ctx context.Context, rlp *kuadrantv1beta2.RateLimitPolicy, gwDiffObj *reconcilers.GatewayDiff) error { +func (r *RateLimitPolicyReconciler) reconcileWASMPluginConf(ctx context.Context, rlp *kuadrantv1beta2.RateLimitPolicy, gwDiffObj *reconcilers.GatewayDiffs) error { logger, _ := logr.FromContext(ctx) for _, gw := range gwDiffObj.GatewaysWithInvalidPolicyRef { @@ -73,7 +75,7 @@ func (r *RateLimitPolicyReconciler) reconcileWASMPluginConf(ctx context.Context, return nil } -func (r *RateLimitPolicyReconciler) gatewayWASMPlugin(ctx context.Context, gw common.GatewayWrapper, rlpRefs []client.ObjectKey) (*istioclientgoextensionv1alpha1.WasmPlugin, error) { +func (r *RateLimitPolicyReconciler) gatewayWASMPlugin(ctx context.Context, gw kuadrant.GatewayWrapper, rlpRefs []client.ObjectKey) (*istioclientgoextensionv1alpha1.WasmPlugin, error) { logger, _ := logr.FromContext(ctx) logger.V(1).Info("gatewayWASMPlugin", "gwKey", gw.Key(), "rlpRefs", rlpRefs) @@ -121,7 +123,7 @@ func (r *RateLimitPolicyReconciler) gatewayWASMPlugin(ctx context.Context, gw co } // returns nil when there is no rate limit policy to apply -func (r *RateLimitPolicyReconciler) wasmPluginConfig(ctx context.Context, gw common.GatewayWrapper, rlpRefs []client.ObjectKey) (*wasm.Plugin, error) { +func (r *RateLimitPolicyReconciler) wasmPluginConfig(ctx context.Context, gw kuadrant.GatewayWrapper, rlpRefs []client.ObjectKey) (*wasm.Plugin, error) { logger, _ := logr.FromContext(ctx) logger = logger.WithName("wasmPluginConfig").WithValues("gateway", gw.Key()) @@ -144,12 +146,15 @@ func (r *RateLimitPolicyReconciler) wasmPluginConfig(ctx context.Context, gw com } // target ref is a HTTPRoute - if common.IsTargetRefHTTPRoute(rlp.Spec.TargetRef) { - route, err := r.FetchValidHTTPRoute(ctx, rlp.TargetKey()) + if kuadrant.IsTargetRefHTTPRoute(rlp.Spec.TargetRef) { + route, err := reconcilers.FetchTargetRefObject(ctx, r.Client(), rlp.GetTargetRef(), rlp.Namespace) if err != nil { return nil, err } - rlps[rlpKey.String()] = &store{rlp: *rlp, route: *route} + // Should only be HTTPRoute in this if block + httpRoute, _ := route.(*gatewayapiv1.HTTPRoute) + + rlps[rlpKey.String()] = &store{rlp: *rlp, route: *httpRoute} routeKeys[client.ObjectKeyFromObject(route).String()] = struct{}{} continue } @@ -171,7 +176,7 @@ func (r *RateLimitPolicyReconciler) wasmPluginConfig(ctx context.Context, gw com // that do not have a rlp of its own, so we can generate wasm rules for those cases if gwRLPKey != "" { rules := make([]gatewayapiv1.HTTPRouteRule, 0) - routes := r.FetchAcceptedGatewayHTTPRoutes(ctx, rlps[gwRLPKey].rlp.TargetKey()) + routes := r.TargetRefReconciler.FetchAcceptedGatewayHTTPRoutes(ctx, rlps[gwRLPKey].rlp.TargetKey()) for idx := range routes { route := routes[idx] // skip routes that have a rlp of its own @@ -223,7 +228,7 @@ func (r *RateLimitPolicyReconciler) wasmPluginConfig(ctx context.Context, gw com Name: rlpKey.String(), Domain: rlptools.LimitsNamespaceFromRLP(&rlp), Rules: rules, - Hostnames: common.HostnamesToStrings(hostnames), // we might be listing more hostnames than needed due to route selectors hostnames possibly being more restrictive + Hostnames: utils.HostnamesToStrings(hostnames), // we might be listing more hostnames than needed due to route selectors hostnames possibly being more restrictive Service: common.KuadrantRateLimitClusterName, }) } diff --git a/controllers/ratelimitpolicy_limits.go b/controllers/ratelimitpolicy_limits.go index 1704c74ad..f8b421962 100644 --- a/controllers/ratelimitpolicy_limits.go +++ b/controllers/ratelimitpolicy_limits.go @@ -9,11 +9,13 @@ import ( kuadrantv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" "github.com/kuadrant/kuadrant-operator/pkg/rlptools" ) func (r *RateLimitPolicyReconciler) reconcileLimits(ctx context.Context, rlp *kuadrantv1beta2.RateLimitPolicy) error { - rlpRefs, err := r.GetAllGatewayPolicyRefs(ctx, &common.KuadrantRateLimitPolicyRefsConfig{}) + rlpRefs, err := r.TargetRefReconciler.GetAllGatewayPolicyRefs(ctx, rlp) if err != nil { return err } @@ -21,12 +23,12 @@ func (r *RateLimitPolicyReconciler) reconcileLimits(ctx context.Context, rlp *ku } func (r *RateLimitPolicyReconciler) deleteLimits(ctx context.Context, rlp *kuadrantv1beta2.RateLimitPolicy) error { - rlpRefs, err := r.GetAllGatewayPolicyRefs(ctx, &common.KuadrantRateLimitPolicyRefsConfig{}) + rlpRefs, err := r.TargetRefReconciler.GetAllGatewayPolicyRefs(ctx, rlp) if err != nil { return err } - rlpRefsWithoutRLP := common.Filter(rlpRefs, func(rlpRef client.ObjectKey) bool { + rlpRefsWithoutRLP := utils.Filter(rlpRefs, func(rlpRef client.ObjectKey) bool { return rlpRef.Name != rlp.Name || rlpRef.Namespace != rlp.Namespace }) @@ -35,7 +37,7 @@ func (r *RateLimitPolicyReconciler) deleteLimits(ctx context.Context, rlp *kuadr func (r *RateLimitPolicyReconciler) reconcileLimitador(ctx context.Context, rlp *kuadrantv1beta2.RateLimitPolicy, rlpRefs []client.ObjectKey) error { logger, _ := logr.FromContext(ctx) - logger = logger.WithName("reconcileLimitador").WithValues("rlp refs", common.Map(rlpRefs, func(ref client.ObjectKey) string { return ref.String() })) + logger = logger.WithName("reconcileLimitador").WithValues("rlp refs", utils.Map(rlpRefs, func(ref client.ObjectKey) string { return ref.String() })) rateLimitIndex, err := r.buildRateLimitIndex(ctx, rlpRefs) if err != nil { @@ -45,15 +47,15 @@ func (r *RateLimitPolicyReconciler) reconcileLimitador(ctx context.Context, rlp // get the current limitador cr for the kuadrant instance so we can compare if it needs to be updated logger.V(1).Info("get kuadrant namespace") var kuadrantNamespace string - kuadrantNamespace, isSet := common.GetKuadrantNamespaceFromPolicy(rlp) + kuadrantNamespace, isSet := kuadrant.GetKuadrantNamespaceFromPolicy(rlp) if !isSet { var err error - kuadrantNamespace, err = common.GetKuadrantNamespaceFromPolicyTargetRef(ctx, r.Client(), rlp) + kuadrantNamespace, err = kuadrant.GetKuadrantNamespaceFromPolicyTargetRef(ctx, r.Client(), rlp) if err != nil { logger.Error(err, "failed to get kuadrant namespace") return err } - common.AnnotateObject(rlp, kuadrantNamespace) + kuadrant.AnnotateObject(rlp, kuadrantNamespace) err = r.UpdateResource(ctx, rlp) // @guicassolato: not sure if this belongs to here if err != nil { logger.Error(err, "failed to update policy, re-queuing") diff --git a/controllers/ratelimitpolicy_status.go b/controllers/ratelimitpolicy_status.go index f6e2bd461..0edffd78e 100644 --- a/controllers/ratelimitpolicy_status.go +++ b/controllers/ratelimitpolicy_status.go @@ -12,7 +12,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" kuadrantv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" ) func (r *RateLimitPolicyReconciler) reconcileStatus(ctx context.Context, rlp *kuadrantv1beta2.RateLimitPolicy, specErr error) (ctrl.Result, error) { @@ -58,7 +58,7 @@ func (r *RateLimitPolicyReconciler) calculateStatus(_ context.Context, rlp *kuad ObservedGeneration: rlp.Status.ObservedGeneration, } - acceptedCond := common.AcceptedCondition(rlp, specErr) + acceptedCond := kuadrant.AcceptedCondition(rlp, specErr) meta.SetStatusCondition(&newStatus.Conditions, *acceptedCond) diff --git a/controllers/suite_test.go b/controllers/suite_test.go index c6b7bbf7f..c92c05829 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -26,7 +26,7 @@ import ( certmanv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" authorinoopapi "github.com/kuadrant/authorino-operator/api/v1beta1" authorinoapi "github.com/kuadrant/authorino/api/v1beta2" - "github.com/kuadrant/kuadrant-operator/pkg/common" + kuadrantdnsv1alpha1 "github.com/kuadrant/dns-operator/api/v1alpha1" limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -40,17 +40,17 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" - metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - - "github.com/kuadrant/kuadrant-operator/pkg/log" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - kuadrantdnsv1alpha1 "github.com/kuadrant/dns-operator/api/v1alpha1" kuadrantv1alpha1 "github.com/kuadrant/kuadrant-operator/api/v1alpha1" kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" kuadrantv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + reconcilerutils "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" + "github.com/kuadrant/kuadrant-operator/pkg/log" "github.com/kuadrant/kuadrant-operator/pkg/reconcilers" //+kubebuilder:scaffold:imports ) @@ -146,10 +146,9 @@ var _ = BeforeSuite(func() { ) err = (&AuthPolicyReconciler{ - TargetRefReconciler: reconcilers.TargetRefReconciler{ - BaseReconciler: authPolicyBaseReconciler, - }, - OverriddenPolicyMap: common.NewOverriddenPolicyMap(), + BaseReconciler: authPolicyBaseReconciler, + TargetRefReconciler: reconcilerutils.TargetRefReconciler{Client: mgr.GetClient()}, + OverriddenPolicyMap: kuadrant.NewOverriddenPolicyMap(), }).SetupWithManager(mgr) Expect(err).NotTo(HaveOccurred()) @@ -160,9 +159,8 @@ var _ = BeforeSuite(func() { ) err = (&RateLimitPolicyReconciler{ - TargetRefReconciler: reconcilers.TargetRefReconciler{ - BaseReconciler: rateLimitPolicyBaseReconciler, - }, + BaseReconciler: rateLimitPolicyBaseReconciler, + TargetRefReconciler: reconcilerutils.TargetRefReconciler{Client: mgr.GetClient()}, }).SetupWithManager(mgr) Expect(err).NotTo(HaveOccurred()) @@ -174,9 +172,8 @@ var _ = BeforeSuite(func() { ) err = (&TLSPolicyReconciler{ - TargetRefReconciler: reconcilers.TargetRefReconciler{ - BaseReconciler: tlsPolicyBaseReconciler, - }, + BaseReconciler: tlsPolicyBaseReconciler, + TargetRefReconciler: reconcilerutils.TargetRefReconciler{Client: mgr.GetClient()}, }).SetupWithManager(mgr) Expect(err).NotTo(HaveOccurred()) @@ -188,9 +185,8 @@ var _ = BeforeSuite(func() { ) err = (&DNSPolicyReconciler{ - TargetRefReconciler: reconcilers.TargetRefReconciler{ - BaseReconciler: dnsPolicyBaseReconciler, - }, + BaseReconciler: dnsPolicyBaseReconciler, + TargetRefReconciler: reconcilerutils.TargetRefReconciler{Client: mgr.GetClient()}, }).SetupWithManager(mgr) Expect(err).NotTo(HaveOccurred()) diff --git a/controllers/tlspolicy_certmanager_certificates.go b/controllers/tlspolicy_certmanager_certificates.go index 2e550f251..082f62f6a 100644 --- a/controllers/tlspolicy_certmanager_certificates.go +++ b/controllers/tlspolicy_certmanager_certificates.go @@ -16,11 +16,10 @@ import ( gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" "github.com/kuadrant/kuadrant-operator/api/v1alpha1" - "github.com/kuadrant/kuadrant-operator/pkg/common" - "github.com/kuadrant/kuadrant-operator/pkg/reconcilers" + reconcilerutils "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" ) -func (r *TLSPolicyReconciler) reconcileCertificates(ctx context.Context, tlsPolicy *v1alpha1.TLSPolicy, gwDiffObj *reconcilers.GatewayDiff) error { +func (r *TLSPolicyReconciler) reconcileCertificates(ctx context.Context, tlsPolicy *v1alpha1.TLSPolicy, gwDiffObj *reconcilerutils.GatewayDiffs) error { log := crlog.FromContext(ctx) log.V(3).Info("reconciling certificates") @@ -66,11 +65,11 @@ func (r *TLSPolicyReconciler) createOrUpdateGatewayCertificates(ctx context.Cont } func (r *TLSPolicyReconciler) deleteGatewayCertificates(ctx context.Context, gateway *gatewayapiv1.Gateway, tlsPolicy *v1alpha1.TLSPolicy) error { - return r.deleteCertificatesWithLabels(ctx, commonTLSCertificateLabels(client.ObjectKeyFromObject(gateway), client.ObjectKeyFromObject(tlsPolicy)), tlsPolicy.Namespace) + return r.deleteCertificatesWithLabels(ctx, commonTLSCertificateLabels(client.ObjectKeyFromObject(gateway), tlsPolicy), tlsPolicy.Namespace) } func (r *TLSPolicyReconciler) deleteCertificates(ctx context.Context, tlsPolicy *v1alpha1.TLSPolicy) error { - return r.deleteCertificatesWithLabels(ctx, policyTLSCertificateLabels(client.ObjectKeyFromObject(tlsPolicy)), tlsPolicy.Namespace) + return r.deleteCertificatesWithLabels(ctx, policyTLSCertificateLabels(tlsPolicy), tlsPolicy.Namespace) } func (r *TLSPolicyReconciler) deleteCertificatesWithLabels(ctx context.Context, lbls map[string]string, namespace string) error { @@ -91,7 +90,7 @@ func (r *TLSPolicyReconciler) deleteCertificatesWithLabels(ctx context.Context, func (r *TLSPolicyReconciler) deleteUnexpectedCertificates(ctx context.Context, expectedCertificates []*certmanv1.Certificate, gateway *gatewayapiv1.Gateway, tlsPolicy *v1alpha1.TLSPolicy) error { // remove any certificates for this gateway and TLSPolicy that are no longer expected existingCertificates := &certmanv1.CertificateList{} - dnsLabels := commonTLSCertificateLabels(client.ObjectKeyFromObject(gateway), client.ObjectKeyFromObject(tlsPolicy)) + dnsLabels := commonTLSCertificateLabels(client.ObjectKeyFromObject(gateway), tlsPolicy) listOptions := &client.ListOptions{LabelSelector: labels.SelectorFromSet(dnsLabels)} if err := r.Client().List(ctx, existingCertificates, listOptions); client.IgnoreNotFound(err) != nil { return err @@ -142,7 +141,7 @@ func (r *TLSPolicyReconciler) expectedCertificatesForGateway(ctx context.Context } func (r *TLSPolicyReconciler) buildCertManagerCertificate(gateway *gatewayapiv1.Gateway, tlsPolicy *v1alpha1.TLSPolicy, secretRef corev1.ObjectReference, hosts []string) *certmanv1.Certificate { - tlsCertLabels := commonTLSCertificateLabels(client.ObjectKeyFromObject(gateway), client.ObjectKeyFromObject(tlsPolicy)) + tlsCertLabels := commonTLSCertificateLabels(client.ObjectKeyFromObject(gateway), tlsPolicy) crt := &certmanv1.Certificate{ ObjectMeta: metav1.ObjectMeta{ @@ -164,9 +163,9 @@ func (r *TLSPolicyReconciler) buildCertManagerCertificate(gateway *gatewayapiv1. return crt } -func commonTLSCertificateLabels(gwKey, apKey client.ObjectKey) map[string]string { +func commonTLSCertificateLabels(gwKey client.ObjectKey, p *v1alpha1.TLSPolicy) map[string]string { common := map[string]string{} - for k, v := range policyTLSCertificateLabels(apKey) { + for k, v := range policyTLSCertificateLabels(p) { common[k] = v } for k, v := range gatewayTLSCertificateLabels(gwKey) { @@ -175,10 +174,10 @@ func commonTLSCertificateLabels(gwKey, apKey client.ObjectKey) map[string]string return common } -func policyTLSCertificateLabels(apKey client.ObjectKey) map[string]string { +func policyTLSCertificateLabels(p *v1alpha1.TLSPolicy) map[string]string { return map[string]string{ - common.TLSPolicyBackRefAnnotation: apKey.Name, - fmt.Sprintf("%s-namespace", common.TLSPolicyBackRefAnnotation): apKey.Namespace, + p.DirectReferenceAnnotationName(): p.Name, + fmt.Sprintf("%s-namespace", p.DirectReferenceAnnotationName()): p.Namespace, } } diff --git a/controllers/tlspolicy_controller.go b/controllers/tlspolicy_controller.go index 8342d2330..5d79bdc7c 100644 --- a/controllers/tlspolicy_controller.go +++ b/controllers/tlspolicy_controller.go @@ -23,7 +23,6 @@ import ( "reflect" "github.com/go-logr/logr" - apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -33,11 +32,14 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/handler" crlog "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "github.com/kuadrant/kuadrant-operator/api/v1alpha1" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/library/mappers" + reconcilerutils "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" "github.com/kuadrant/kuadrant-operator/pkg/reconcilers" ) @@ -48,8 +50,9 @@ const ( // TLSPolicyReconciler reconciles a TLSPolicy object type TLSPolicyReconciler struct { - reconcilers.TargetRefReconciler - Scheme *runtime.Scheme + *reconcilers.BaseReconciler + TargetRefReconciler reconcilerutils.TargetRefReconciler + Scheme *runtime.Scheme } //+kubebuilder:rbac:groups=kuadrant.io,resources=tlspolicies,verbs=get;list;watch;update;patch;delete @@ -78,7 +81,7 @@ func (r *TLSPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( markedForDeletion := tlsPolicy.GetDeletionTimestamp() != nil - targetReferenceObject, err := r.FetchValidTargetRef(ctx, tlsPolicy.GetTargetRef(), tlsPolicy.Namespace) + targetReferenceObject, err := reconcilerutils.FetchTargetRefObject(ctx, r.Client(), tlsPolicy.GetTargetRef(), tlsPolicy.Namespace) log.V(3).Info("TLSPolicyReconciler targetReferenceObject", "targetReferenceObject", targetReferenceObject) if err != nil { if !markedForDeletion { @@ -88,7 +91,7 @@ func (r *TLSPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( if delResErr == nil { delResErr = err } - return r.reconcileStatus(ctx, tlsPolicy, common.NewErrTargetNotFound(tlsPolicy.Kind(), tlsPolicy.GetTargetRef(), delResErr)) + return r.reconcileStatus(ctx, tlsPolicy, kuadrant.NewErrTargetNotFound(tlsPolicy.Kind(), tlsPolicy.GetTargetRef(), delResErr)) } return ctrl.Result{}, err } @@ -150,7 +153,7 @@ func (r *TLSPolicyReconciler) reconcileResources(ctx context.Context, tlsPolicy } // reconcile based on gateway diffs - gatewayDiffObj, err := r.ComputeGatewayDiffs(ctx, tlsPolicy, targetNetworkObject, &common.KuadrantTLSPolicyRefsConfig{}) + gatewayDiffObj, err := reconcilerutils.ComputeGatewayDiffs(ctx, r.Client(), tlsPolicy, targetNetworkObject) if err != nil { return err } @@ -162,14 +165,14 @@ func (r *TLSPolicyReconciler) reconcileResources(ctx context.Context, tlsPolicy } // set direct back ref - i.e. claim the target network object as taken asap - if err = r.ReconcileTargetBackReference(ctx, tlsPolicy, targetNetworkObject, common.TLSPolicyBackRefAnnotation); err != nil { + if err = r.TargetRefReconciler.ReconcileTargetBackReference(ctx, tlsPolicy, targetNetworkObject, tlsPolicy.DirectReferenceAnnotationName()); err != nil { gatewayCondition = BuildPolicyAffectedCondition(TLSPolicyAffected, tlsPolicy, targetNetworkObject, gatewayapiv1alpha2.PolicyReasonConflicted, err) updateErr := r.updateGatewayCondition(ctx, gatewayCondition, gatewayDiffObj) return errors.Join(fmt.Errorf("reconcile TargetBackReference error %w", err), updateErr) } // set annotation of policies affecting the gateway - if err = r.ReconcileGatewayPolicyReferences(ctx, tlsPolicy, gatewayDiffObj); err != nil { + if err = r.TargetRefReconciler.ReconcileGatewayPolicyReferences(ctx, tlsPolicy, gatewayDiffObj); err != nil { gatewayCondition = BuildPolicyAffectedCondition(TLSPolicyAffected, tlsPolicy, targetNetworkObject, gatewayapiv1alpha2.PolicyConditionReason(PolicyReasonUnknown), err) updateErr := r.updateGatewayCondition(ctx, gatewayCondition, gatewayDiffObj) return errors.Join(fmt.Errorf("ReconcileGatewayPolicyReferences error %w", err), updateErr) @@ -186,7 +189,7 @@ func (r *TLSPolicyReconciler) reconcileResources(ctx context.Context, tlsPolicy func (r *TLSPolicyReconciler) deleteResources(ctx context.Context, tlsPolicy *v1alpha1.TLSPolicy, targetNetworkObject client.Object) error { // delete based on gateway diffs - gatewayDiffObj, err := r.ComputeGatewayDiffs(ctx, tlsPolicy, targetNetworkObject, &common.KuadrantTLSPolicyRefsConfig{}) + gatewayDiffObj, err := reconcilerutils.ComputeGatewayDiffs(ctx, r.Client(), tlsPolicy, targetNetworkObject) if err != nil { return err } @@ -197,13 +200,13 @@ func (r *TLSPolicyReconciler) deleteResources(ctx context.Context, tlsPolicy *v1 // remove direct back ref if targetNetworkObject != nil { - if err := r.DeleteTargetBackReference(ctx, targetNetworkObject, common.TLSPolicyBackRefAnnotation); err != nil { + if err := r.TargetRefReconciler.DeleteTargetBackReference(ctx, targetNetworkObject, tlsPolicy.DirectReferenceAnnotationName()); err != nil { return err } } // update annotation of policies affecting the gateway - if err := r.ReconcileGatewayPolicyReferences(ctx, tlsPolicy, gatewayDiffObj); err != nil { + if err := r.TargetRefReconciler.ReconcileGatewayPolicyReferences(ctx, tlsPolicy, gatewayDiffObj); err != nil { return err } @@ -211,7 +214,7 @@ func (r *TLSPolicyReconciler) deleteResources(ctx context.Context, tlsPolicy *v1 return r.updateGatewayCondition(ctx, metav1.Condition{Type: string(TLSPolicyAffected)}, gatewayDiffObj) } -func (r *TLSPolicyReconciler) updateGatewayCondition(ctx context.Context, condition metav1.Condition, gatewayDiff *reconcilers.GatewayDiff) error { +func (r *TLSPolicyReconciler) updateGatewayCondition(ctx context.Context, condition metav1.Condition, gatewayDiff *reconcilerutils.GatewayDiffs) error { // update condition if needed gatewayDiffs := append(gatewayDiff.GatewaysWithValidPolicyRef, gatewayDiff.GatewaysMissingPolicyRef...) for i, gw := range gatewayDiffs { @@ -241,40 +244,19 @@ func (r *TLSPolicyReconciler) updateGatewayCondition(ctx context.Context, condit // SetupWithManager sets up the controller with the Manager. func (r *TLSPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error { - gatewayEventMapper := &GatewayEventMapper{ - Logger: r.Logger().WithName("gatewayEventMapper"), - } + gatewayEventMapper := mappers.NewGatewayEventMapper(mappers.WithLogger(r.Logger().WithName("gatewayEventMapper"))) + return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.TLSPolicy{}). Watches( &gatewayapiv1.Gateway{}, - handler.EnqueueRequestsFromMapFunc(gatewayEventMapper.MapToTLSPolicy), + handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, object client.Object) []reconcile.Request { + return gatewayEventMapper.MapToPolicy(object, &v1alpha1.TLSPolicy{}) + }), ). Complete(r) } -// The following methods are here temporarily and copied from the kuadrant-operator https://github.com/Kuadrant/kuadrant-operator/blob/main/pkg/reconcilers/targetref_reconciler.go#L45 -// FetchValidTargetRef and FetchValidGateway currently expect that the gateway should have a ready condition, but in the -// case of the TLSPolicy it might not be ready because there is an invalid tls section that this policy would resolve. -// ToDo mnairn: Create issue in kuadrant-operator and link it here - -// FetchValidTargetRef fetches the target reference object and checks the status is valid -func (r *TLSPolicyReconciler) FetchValidTargetRef(ctx context.Context, targetRef gatewayapiv1alpha2.PolicyTargetReference, defaultNs string) (client.Object, error) { - tmpNS := defaultNs - if targetRef.Namespace != nil { - tmpNS = string(*targetRef.Namespace) - } - - objKey := client.ObjectKey{Name: string(targetRef.Name), Namespace: tmpNS} - if common.IsTargetRefHTTPRoute(targetRef) { - return r.FetchValidHTTPRoute(ctx, objKey) - } else if common.IsTargetRefGateway(targetRef) { - return r.FetchValidGateway(ctx, objKey) - } - - return nil, fmt.Errorf("FetchValidTargetRef: targetRef (%v) to unknown network resource", targetRef) -} - func (r *TLSPolicyReconciler) FetchValidGateway(ctx context.Context, key client.ObjectKey) (*gatewayapiv1.Gateway, error) { logger, _ := logr.FromContext(ctx) diff --git a/controllers/tlspolicy_controller_test.go b/controllers/tlspolicy_controller_test.go index 848f493b6..8251356b1 100644 --- a/controllers/tlspolicy_controller_test.go +++ b/controllers/tlspolicy_controller_test.go @@ -22,7 +22,6 @@ import ( gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "github.com/kuadrant/kuadrant-operator/api/v1alpha1" - "github.com/kuadrant/kuadrant-operator/pkg/common" ) var _ = Describe("TLSPolicy controller", Ordered, func() { @@ -167,8 +166,8 @@ var _ = Describe("TLSPolicy controller", Ordered, func() { err := k8sClient.Get(ctx, client.ObjectKey{Name: gateway.Name, Namespace: testNamespace}, gw) //Check annotations g.Expect(err).NotTo(HaveOccurred()) - g.Expect(gw.Annotations).To(HaveKeyWithValue(common.TLSPolicyBackRefAnnotation, policyBackRefValue)) - g.Expect(gw.Annotations).To(HaveKeyWithValue(common.TLSPoliciesBackRefAnnotation, policiesBackRefValue)) + g.Expect(gw.Annotations).To(HaveKeyWithValue(v1alpha1.TLSPolicyDirectReferenceAnnotationName, policyBackRefValue)) + g.Expect(gw.Annotations).To(HaveKeyWithValue(v1alpha1.TLSPolicyBackReferenceAnnotationName, policiesBackRefValue)) //Check status g.Expect(gw.Status.Conditions).To( ContainElement(MatchFields(IgnoreExtras, Fields{ diff --git a/controllers/tlspolicy_status.go b/controllers/tlspolicy_status.go index e46046baa..43384cf5d 100644 --- a/controllers/tlspolicy_status.go +++ b/controllers/tlspolicy_status.go @@ -27,7 +27,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/kuadrant/kuadrant-operator/api/v1alpha1" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" ) func (r *TLSPolicyReconciler) reconcileStatus(ctx context.Context, tlsPolicy *v1alpha1.TLSPolicy, specErr error) (ctrl.Result, error) { @@ -50,7 +50,7 @@ func (r *TLSPolicyReconciler) reconcileStatus(ctx context.Context, tlsPolicy *v1 return ctrl.Result{}, updateErr } - if common.IsTargetNotFound(specErr) { + if kuadrant.IsTargetNotFound(specErr) { return ctrl.Result{Requeue: true}, nil } @@ -64,7 +64,7 @@ func (r *TLSPolicyReconciler) calculateStatus(tlsPolicy *v1alpha1.TLSPolicy, spe ObservedGeneration: tlsPolicy.Status.ObservedGeneration, } - acceptedCond := common.AcceptedCondition(tlsPolicy, specErr) + acceptedCond := kuadrant.AcceptedCondition(tlsPolicy, specErr) meta.SetStatusCondition(&newStatus.Conditions, *acceptedCond) return newStatus diff --git a/main.go b/main.go index eecc0f875..d9cd5b24a 100644 --- a/main.go +++ b/main.go @@ -22,42 +22,38 @@ import ( "os" "runtime" - "github.com/kuadrant/kuadrant-operator/pkg/common" - "k8s.io/utils/env" - "sigs.k8s.io/controller-runtime/pkg/webhook" - - corev1 "k8s.io/api/core/v1" - // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. - _ "k8s.io/client-go/plugin/pkg/client/auth" - certmanv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" authorinoopapi "github.com/kuadrant/authorino-operator/api/v1beta1" authorinoapi "github.com/kuadrant/authorino/api/v1beta2" - maistraapis "github.com/kuadrant/kuadrant-operator/api/external/maistra" + kuadrantdnsv1alpha1 "github.com/kuadrant/dns-operator/api/v1alpha1" limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" istioextensionv1alpha1 "istio.io/client-go/pkg/apis/extensions/v1alpha1" istionetworkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" istiosecurityv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" istioapis "istio.io/istio/operator/pkg/apis" + corev1 "k8s.io/api/core/v1" apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" k8sruntime "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" + _ "k8s.io/client-go/plugin/pkg/client/auth" + "k8s.io/utils/env" istiov1alpha1 "maistra.io/istio-operator/api/v1alpha1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/healthz" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" - + "sigs.k8s.io/controller-runtime/pkg/webhook" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - kuadrantdnsv1alpha1 "github.com/kuadrant/dns-operator/api/v1alpha1" - + maistraapis "github.com/kuadrant/kuadrant-operator/api/external/maistra" kuadrantv1alpha1 "github.com/kuadrant/kuadrant-operator/api/v1alpha1" kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" kuadrantv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" "github.com/kuadrant/kuadrant-operator/controllers" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + reconcilerutils "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" "github.com/kuadrant/kuadrant-operator/pkg/log" "github.com/kuadrant/kuadrant-operator/pkg/reconcilers" //+kubebuilder:scaffold:imports @@ -160,9 +156,8 @@ func main() { ) if err = (&controllers.RateLimitPolicyReconciler{ - TargetRefReconciler: reconcilers.TargetRefReconciler{ - BaseReconciler: rateLimitPolicyBaseReconciler, - }, + TargetRefReconciler: reconcilerutils.TargetRefReconciler{Client: mgr.GetClient()}, + BaseReconciler: rateLimitPolicyBaseReconciler, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "RateLimitPolicy") os.Exit(1) @@ -175,10 +170,9 @@ func main() { ) if err = (&controllers.AuthPolicyReconciler{ - TargetRefReconciler: reconcilers.TargetRefReconciler{ - BaseReconciler: authPolicyBaseReconciler, - }, - OverriddenPolicyMap: common.NewOverriddenPolicyMap(), + TargetRefReconciler: reconcilerutils.TargetRefReconciler{Client: mgr.GetClient()}, + BaseReconciler: authPolicyBaseReconciler, + OverriddenPolicyMap: kuadrant.NewOverriddenPolicyMap(), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "AuthPolicy") os.Exit(1) @@ -191,9 +185,8 @@ func main() { ) if err = (&controllers.DNSPolicyReconciler{ - TargetRefReconciler: reconcilers.TargetRefReconciler{ - BaseReconciler: dnsPolicyBaseReconciler, - }, + BaseReconciler: dnsPolicyBaseReconciler, + TargetRefReconciler: reconcilerutils.TargetRefReconciler{Client: mgr.GetClient()}, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "DNSPolicy") os.Exit(1) @@ -206,9 +199,8 @@ func main() { ) if err = (&controllers.TLSPolicyReconciler{ - TargetRefReconciler: reconcilers.TargetRefReconciler{ - BaseReconciler: tlsPolicyBaseReconciler, - }, + BaseReconciler: tlsPolicyBaseReconciler, + TargetRefReconciler: reconcilerutils.TargetRefReconciler{Client: mgr.GetClient()}, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "TLSPolicy") os.Exit(1) diff --git a/pkg/common/common.go b/pkg/common/common.go index 3667968a0..c61b05b97 100644 --- a/pkg/common/common.go +++ b/pkg/common/common.go @@ -18,126 +18,22 @@ package common import ( "fmt" - "slices" "strings" "sigs.k8s.io/controller-runtime/pkg/client" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" ) // TODO: move the const to a proper place, or get it from config const ( - KuadrantRateLimitClusterName = "kuadrant-rate-limiting-service" - RateLimitPoliciesBackRefAnnotation = "kuadrant.io/ratelimitpolicies" - RateLimitPolicyBackRefAnnotation = "kuadrant.io/ratelimitpolicy" - AuthPoliciesBackRefAnnotation = "kuadrant.io/authpolicies" - AuthPolicyBackRefAnnotation = "kuadrant.io/authpolicy" - TLSPoliciesBackRefAnnotation = "kuadrant.io/tlspolicies" - TLSPolicyBackRefAnnotation = "kuadrant.io/tlspolicy" - DNSPoliciesBackRefAnnotation = "kuadrant.io/dnspolicies" - DNSPolicyBackRefAnnotation = "kuadrant.io/dnspolicy" - KuadrantNamespaceLabel = "kuadrant.io/namespace" - NamespaceSeparator = '/' - LimitadorName = "limitador" + KuadrantRateLimitClusterName = "kuadrant-rate-limiting-service" + AuthPolicyBackRefAnnotation = "kuadrant.io/authpolicy" + NamespaceSeparator = '/' + LimitadorName = "limitador" ) -type KuadrantPolicy interface { - client.Object - GetTargetRef() gatewayapiv1alpha2.PolicyTargetReference - GetWrappedNamespace() gatewayapiv1.Namespace - GetRulesHostnames() []string - Kind() string -} - -type KuadrantPolicyList interface { - GetItems() []KuadrantPolicy -} - -// GetEmptySliceIfNil returns a provided slice, or an empty slice of the same type if the input slice is nil. -func GetEmptySliceIfNil[T any](val []T) []T { - if val == nil { - return make([]T, 0) - } - return val -} - -// NamespacedNameToObjectKey converts format string to k8s object key. -// It's common for K8s to reference an object using this format. For e.g. gateways in VirtualService. -func NamespacedNameToObjectKey(namespacedName, defaultNamespace string) client.ObjectKey { - if i := strings.IndexRune(namespacedName, '/'); i >= 0 { - return client.ObjectKey{Namespace: namespacedName[:i], Name: namespacedName[i+1:]} - } - return client.ObjectKey{Namespace: defaultNamespace, Name: namespacedName} -} - -// SameElements checks if the two slices contain the exact same elements. Order does not matter. -func SameElements[T comparable](s1, s2 []T) bool { - if len(s1) != len(s2) { - return false - } - for _, v := range s1 { - if !slices.Contains(s2, v) { - return false - } - } - return true -} - -func Intersect[T comparable](slice1, slice2 []T) bool { - for _, item := range slice1 { - if slices.Contains(slice2, item) { - return true - } - } - return false -} - -func Intersection[T comparable](slice1, slice2 []T) []T { - smallerSlice := slice1 - largerSlice := slice2 - if len(slice1) > len(slice2) { - smallerSlice = slice2 - largerSlice = slice1 - } - var result []T - for _, item := range smallerSlice { - if slices.Contains(largerSlice, item) { - result = append(result, item) - } - } - return result -} - -func Find[T any](slice []T, match func(T) bool) (*T, bool) { - for _, item := range slice { - if match(item) { - return &item, true - } - } - return nil, false -} - -// Map applies the given mapper function to each element in the input slice and returns a new slice with the results. -func Map[T, U any](slice []T, f func(T) U) []U { - arr := make([]U, len(slice)) - for i, e := range slice { - arr[i] = f(e) - } - return arr -} - -// Filter filters the input slice using the given predicate function and returns a new slice with the results. -func Filter[T any](slice []T, f func(T) bool) []T { - arr := make([]T, 0) - for _, e := range slice { - if f(e) { - arr = append(arr, e) - } - } - return arr -} - // MergeMapStringString Merge desired into existing. // Not Thread-Safe. Does it matter? func MergeMapStringString(existing *map[string]string, desired map[string]string) bool { @@ -188,42 +84,12 @@ func UnMarshallObjectKey(keyStr string) (client.ObjectKey, error) { return client.ObjectKey{Namespace: keyStr[:namespaceEndIndex], Name: keyStr[namespaceEndIndex+1:]}, nil } -// HostnamesToStrings converts []gatewayapiv1.Hostname to []string -func HostnamesToStrings(hostnames []gatewayapiv1.Hostname) []string { - return Map(hostnames, func(hostname gatewayapiv1.Hostname) string { - return string(hostname) - }) -} - -// ValidSubdomains returns (true, "") when every single subdomains item -// is a subset of at least one of the domains. -// Domains and subdomains may be prefixed with a wildcard label (*.). -// The wildcard label must appear by itself as the first label. -// When one of the subdomains is not a subset of the domains, it returns false and -// the subdomain not being subset of the domains -func ValidSubdomains(domains, subdomains []string) (bool, string) { - for _, subdomain := range subdomains { - validSubdomain := false - for _, domain := range domains { - if Name(subdomain).SubsetOf(Name(domain)) { - validSubdomain = true - break - } - } - - if !validSubdomain { - return false, subdomain - } - } - return true, "" -} - // FilterValidSubdomains returns every subdomain that is a subset of at least one of the (super) domains specified in the first argument. func FilterValidSubdomains(domains, subdomains []gatewayapiv1.Hostname) []gatewayapiv1.Hostname { arr := make([]gatewayapiv1.Hostname, 0) for _, subsubdomain := range subdomains { - if _, found := Find(domains, func(domain gatewayapiv1.Hostname) bool { - return Name(subsubdomain).SubsetOf(Name(domain)) + if _, found := utils.Find(domains, func(domain gatewayapiv1.Hostname) bool { + return utils.Name(subsubdomain).SubsetOf(utils.Name(domain)) }); found { arr = append(arr, subsubdomain) } diff --git a/pkg/common/common_test.go b/pkg/common/common_test.go index d62df1c07..c7eabe7df 100644 --- a/pkg/common/common_test.go +++ b/pkg/common/common_test.go @@ -11,388 +11,6 @@ import ( gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" ) -func TestValidSubdomains(t *testing.T) { - testCases := []struct { - name string - domains []string - subdomains []string - expected bool - expectedHostname string - }{ - {"nil", nil, nil, true, ""}, - {"nil subdomains", []string{"*.example.com"}, nil, true, ""}, - {"nil domains", nil, []string{"*.example.com"}, false, "*.example.com"}, - {"dot matters", []string{"*.example.com"}, []string{"example.com"}, false, "example.com"}, - {"dot matters2", []string{"example.com"}, []string{"*.example.com"}, false, "*.example.com"}, - {"happy path", []string{"*.example.com", "*.net"}, []string{"*.other.net", "test.example.com"}, true, ""}, - {"not all match", []string{"*.example.com", "*.net"}, []string{"*.other.com", "*.example.com"}, false, "*.other.com"}, - {"wildcard in subdomains does not match", []string{"*.example.com", "*.net"}, []string{"*", "*.example.com"}, false, "*"}, - {"wildcard in domains matches all", []string{"*", "*.net"}, []string{"*.net", "*.example.com"}, true, ""}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(subT *testing.T) { - valid, hostname := ValidSubdomains(tc.domains, tc.subdomains) - if valid != tc.expected { - subT.Errorf("expected (%t), got (%t)", tc.expected, valid) - } - if hostname != tc.expectedHostname { - subT.Errorf("expected hostname (%s), got (%s)", tc.expectedHostname, hostname) - } - }) - } -} - -func TestFind(t *testing.T) { - s := []string{"a", "ab", "abc"} - - if r, found := Find(s, func(el string) bool { return el == "ab" }); !found || r == nil || *r != "ab" { - t.Error("should have found 'ab' in the slice") - } - - if r, found := Find(s, func(el string) bool { return len(el) <= 3 }); !found || r == nil || *r != "a" { - t.Error("should have found 'a' in the slice") - } - - if r, found := Find(s, func(el string) bool { return len(el) >= 3 }); !found || r == nil || *r != "abc" { - t.Error("should have found 'abc' in the slice") - } - - if r, found := Find(s, func(el string) bool { return len(el) == 4 }); found || r != nil { - t.Error("should not have found anything in the slice") - } - - i := []int{1, 2, 3} - - if r, found := Find(i, func(el int) bool { return el/3 == 1 }); !found || r == nil || *r != 3 { - t.Error("should have found 3 in the slice") - } - - if r, found := Find(i, func(el int) bool { return el == 75 }); found || r != nil { - t.Error("should not have found anything in the slice") - } -} - -func TestGetEmptySliceIfNil(t *testing.T) { - t.Run("when a non-nil slice is provided then return same slice", func(t *testing.T) { - value := []int{1, 2, 3} - expected := value - - result := GetEmptySliceIfNil(value) - - if !reflect.DeepEqual(result, expected) { - t.Errorf("Expected %v, but got %v", expected, result) - } - }) - - t.Run("when a nil slice is provided then return an empty slice of the same type", func(t *testing.T) { - var value []int - expected := []int{} - - result := GetEmptySliceIfNil(value) - - if !reflect.DeepEqual(result, expected) { - t.Errorf("Expected %v, but got %v", expected, result) - } - }) -} - -func TestNamespacedNameToObjectKey(t *testing.T) { - t.Run("when a namespaced name is provided then return an ObjectKey with corresponding namespace and name", func(t *testing.T) { - namespacedName := "test-namespace/test-name" - defaultNamespace := "default" - - result := NamespacedNameToObjectKey(namespacedName, defaultNamespace) - - expected := client.ObjectKey{Name: "test-name", Namespace: "test-namespace"} - if !reflect.DeepEqual(result, expected) { - t.Errorf("Expected %v, but got %v", expected, result) - } - }) - - t.Run("when only a name is provided then return an ObjectKey with the default namespace and provided name", func(t *testing.T) { - namespacedName := "test-name" - defaultNamespace := "default" - - result := NamespacedNameToObjectKey(namespacedName, defaultNamespace) - - expected := client.ObjectKey{Name: "test-name", Namespace: "default"} - if !reflect.DeepEqual(result, expected) { - t.Errorf("Expected %v, but got %v", expected, result) - } - }) - - t.Run("when an empty string is provided, then return an ObjectKey with default namespace and empty name", func(t *testing.T) { - namespacedName := "" - defaultNamespace := "default" - - result := NamespacedNameToObjectKey(namespacedName, defaultNamespace) - - expected := client.ObjectKey{Name: "", Namespace: "default"} - if !reflect.DeepEqual(result, expected) { - t.Errorf("Expected %v, but got %v", expected, result) - } - }) -} - -func TestSameElements(t *testing.T) { - testCases := []struct { - name string - slice1 []string - slice2 []string - expected bool - }{ - { - name: "when slice1 and slice2 contain the same elements then return true", - slice1: []string{"test-gw1", "test-gw2", "test-gw3"}, - slice2: []string{"test-gw1", "test-gw2", "test-gw3"}, - expected: true, - }, - { - name: "when slice1 and slice2 contain unique elements then return false", - slice1: []string{"test-gw1", "test-gw2"}, - slice2: []string{"test-gw1", "test-gw3"}, - expected: false, - }, - { - name: "when both slices are empty then return true", - slice1: []string{}, - slice2: []string{}, - expected: true, - }, - { - name: "when both slices are nil then return true", - expected: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if SameElements(tc.slice1, tc.slice2) != tc.expected { - t.Errorf("when slice1=%v and slice2=%v, expected=%v, but got=%v", tc.slice1, tc.slice2, tc.expected, !tc.expected) - } - }) - } -} - -func TestIntersect(t *testing.T) { - testCases := []struct { - name string - slice1 []string - slice2 []string - expected bool - }{ - { - name: "when slice1 and slice2 have one common item then return true", - slice1: []string{"test-gw1", "test-gw2"}, - slice2: []string{"test-gw1", "test-gw3", "test-gw4"}, - expected: true, - }, - { - name: "when slice1 and slice2 have no common item then return false", - slice1: []string{"test-gw1", "test-gw2"}, - slice2: []string{"test-gw3", "test-gw4"}, - expected: false, - }, - { - name: "when slice1 is empty then return false", - slice1: []string{}, - slice2: []string{"test-gw3", "test-gw4"}, - expected: false, - }, - { - name: "when slice2 is empty then return false", - slice1: []string{"test-gw1", "test-gw2"}, - slice2: []string{}, - expected: false, - }, - { - name: "when both slices are empty then return false", - slice1: []string{}, - slice2: []string{}, - expected: false, - }, - { - name: "when slice1 is nil then return false", - slice2: []string{"test-gw3", "test-gw4"}, - expected: false, - }, - { - name: "when slice2 is nil then return false", - slice1: []string{"test-gw1", "test-gw2"}, - expected: false, - }, - { - name: "when both slices are nil then return false", - expected: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if Intersect(tc.slice1, tc.slice2) != tc.expected { - t.Errorf("when slice1=%v and slice2=%v, expected=%v, but got=%v", tc.slice1, tc.slice2, tc.expected, !tc.expected) - } - }) - } -} - -func TestIntersectWithInts(t *testing.T) { - testCases := []struct { - name string - slice1 []int - slice2 []int - expected bool - }{ - { - name: "when slice1 and slice2 have one common item then return true", - slice1: []int{1, 2}, - slice2: []int{1, 3, 4}, - expected: true, - }, - { - name: "when slice1 and slice2 have no common item then return false", - slice1: []int{1, 2}, - slice2: []int{3, 4}, - expected: false, - }, - { - name: "when slice1 is empty then return false", - slice1: []int{}, - slice2: []int{3, 4}, - expected: false, - }, - { - name: "when slice2 is empty then return false", - slice1: []int{1, 2}, - slice2: []int{}, - expected: false, - }, - { - name: "when both slices are empty then return false", - slice1: []int{}, - slice2: []int{}, - expected: false, - }, - { - name: "when slice1 is nil then return false", - slice2: []int{3, 4}, - expected: false, - }, - { - name: "when slice2 is nil then return false", - slice1: []int{1, 2}, - expected: false, - }, - { - name: "when both slices are nil then return false", - expected: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if Intersect(tc.slice1, tc.slice2) != tc.expected { - t.Errorf("when slice1=%v and slice2=%v, expected=%v, but got=%v", tc.slice1, tc.slice2, tc.expected, !tc.expected) - } - }) - } -} - -func TestIntersection(t *testing.T) { - testCases := []struct { - name string - slice1 []string - slice2 []string - expected []string - }{ - { - name: "when slice1 and slice2 have one common item then return that item", - slice1: []string{"test-gw1", "test-gw2"}, - slice2: []string{"test-gw1", "test-gw3", "test-gw4"}, - expected: []string{"test-gw1"}, - }, - { - name: "when slice1 and slice2 have no common item then return nil", - slice1: []string{"test-gw1", "test-gw2"}, - slice2: []string{"test-gw3", "test-gw4"}, - expected: nil, - }, - { - name: "when slice1 is empty then return nil", - slice1: []string{}, - slice2: []string{"test-gw3", "test-gw4"}, - expected: nil, - }, - { - name: "when slice2 is empty then return nil", - slice1: []string{"test-gw1", "test-gw2"}, - slice2: []string{}, - expected: nil, - }, - { - name: "when both slices are empty then return nil", - slice1: []string{}, - slice2: []string{}, - expected: nil, - }, - { - name: "when slice1 is nil then return nil", - slice2: []string{"test-gw3", "test-gw4"}, - expected: nil, - }, - { - name: "when slice2 is nil then return nil", - slice1: []string{"test-gw1", "test-gw2"}, - expected: nil, - }, - { - name: "when both slices are nil then return nil", - expected: nil, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if r := Intersection(tc.slice1, tc.slice2); !reflect.DeepEqual(r, tc.expected) { - t.Errorf("expected=%v; got=%v", tc.expected, r) - } - }) - } -} - -func TestMap(t *testing.T) { - slice1 := []int{1, 2, 3, 4} - f1 := func(x int) int { return x + 1 } - expected1 := []int{2, 3, 4, 5} - result1 := Map(slice1, f1) - t.Run("when mapping an int slice with an increment function then return new slice with the incremented values", func(t *testing.T) { - if !reflect.DeepEqual(result1, expected1) { - t.Errorf("result1 = %v; expected %v", result1, expected1) - } - }) - - slice2 := []string{"hello", "world", "buz", "a"} - f2 := func(s string) int { return len(s) } - expected2 := []int{5, 5, 3, 1} - result2 := Map(slice2, f2) - t.Run("when mapping a string slice with string->int mapping then return new slice with the mapped values", func(t *testing.T) { - if !reflect.DeepEqual(result2, expected2) { - t.Errorf("result2 = %v; expected %v", result2, expected2) - } - }) - - slice3 := []int{} - f3 := func(x int) float32 { return float32(x) / 2 } - expected3 := []float32{} - result3 := Map(slice3, f3) - t.Run("when mapping an empty int slice then return an empty slice", func(t *testing.T) { - if !reflect.DeepEqual(result3, expected3) { - t.Errorf("result3 = %v; expected %v", result3, expected3) - } - }) -} - func TestMergeMapStringString(t *testing.T) { testCases := []struct { name string @@ -603,49 +221,6 @@ func TestUnMarshallObjectKey(t *testing.T) { } } -func TestHostnamesToStrings(t *testing.T) { - testCases := []struct { - name string - inputHostnames []gatewayapiv1.Hostname - expectedOutput []string - }{ - { - name: "when input is empty then return empty output", - inputHostnames: []gatewayapiv1.Hostname{}, - expectedOutput: []string{}, - }, - { - name: "when input has a single precise hostname then return a single string", - inputHostnames: []gatewayapiv1.Hostname{"example.com"}, - expectedOutput: []string{"example.com"}, - }, - { - name: "when input has multiple precise hostnames then return the corresponding strings", - inputHostnames: []gatewayapiv1.Hostname{"example.com", "test.com", "localhost"}, - expectedOutput: []string{"example.com", "test.com", "localhost"}, - }, - { - name: "when input has a wildcard hostname then return the wildcard string", - inputHostnames: []gatewayapiv1.Hostname{"*.example.com"}, - expectedOutput: []string{"*.example.com"}, - }, - { - name: "when input has both precise and wildcard hostnames then return the corresponding strings", - inputHostnames: []gatewayapiv1.Hostname{"example.com", "*.test.com"}, - expectedOutput: []string{"example.com", "*.test.com"}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - output := HostnamesToStrings(tc.inputHostnames) - if !reflect.DeepEqual(tc.expectedOutput, output) { - t.Errorf("Unexpected output. Expected %v but got %v", tc.expectedOutput, output) - } - }) - } -} - func TestFilterValidSubdomains(t *testing.T) { testCases := []struct { name string diff --git a/pkg/common/istio_utils.go b/pkg/common/istio_utils.go index d263f401d..a89e5f8bb 100644 --- a/pkg/common/istio_utils.go +++ b/pkg/common/istio_utils.go @@ -7,11 +7,13 @@ import ( istiocommon "istio.io/api/type/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" ) func IstioWorkloadSelectorFromGateway(ctx context.Context, k8sClient client.Client, gateway *gatewayapiv1.Gateway) *istiocommon.WorkloadSelector { logger, _ := logr.FromContext(ctx) - gatewayWorkloadSelector, err := GetGatewayWorkloadSelector(ctx, k8sClient, gateway) + gatewayWorkloadSelector, err := kuadrant.GetGatewayWorkloadSelector(ctx, k8sClient, gateway) if err != nil { logger.V(1).Info("failed to build Istio WorkloadSelector from Gateway service - falling back to Gateway labels") gatewayWorkloadSelector = gateway.Labels diff --git a/pkg/common/k8s_utils.go b/pkg/common/k8s_utils.go index e6c9fce2f..bb59bbf89 100644 --- a/pkg/common/k8s_utils.go +++ b/pkg/common/k8s_utils.go @@ -17,14 +17,11 @@ limitations under the License. package common import ( - "context" "encoding/json" "fmt" "sort" - "strconv" appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" @@ -40,16 +37,6 @@ func ObjectInfo(obj client.Object) string { return fmt.Sprintf("%s/%s", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName()) } -// ReadAnnotationsFromObject reads the annotations from a Kubernetes object -// and returns them as a map. If the object has no annotations, it returns an empty map. -func ReadAnnotationsFromObject(obj client.Object) map[string]string { - annotations := obj.GetAnnotations() - if annotations == nil { - annotations = map[string]string{} - } - return annotations -} - // TagObjectToDelete adds a special DeleteTagAnnotation to the object's annotations. // If the object's annotations are nil, it first initializes the Annotations field with an empty map. func TagObjectToDelete(obj client.Object) { @@ -110,46 +97,6 @@ func IsOwnedBy(owned, owner client.Object) bool { return false } -// GetServicePortNumber returns the port number from the referenced key and port info -// the port info can be named port or already a number. -func GetServicePortNumber(ctx context.Context, k8sClient client.Client, serviceKey client.ObjectKey, servicePort string) (int32, error) { - // check if the port is a number already. - if num, err := strconv.ParseInt(servicePort, 10, 32); err == nil { - return int32(num), nil - } - - // As the port is name, resolv the port from the service - service, err := GetService(ctx, k8sClient, serviceKey) - if err != nil { - // the service must exist - return 0, err - } - - for _, p := range service.Spec.Ports { - if p.Name == servicePort { - return int32(p.TargetPort.IntValue()), nil - } - } - - return 0, fmt.Errorf("service port %s was not found in %s", servicePort, serviceKey.String()) -} - -func GetServiceWorkloadSelector(ctx context.Context, k8sClient client.Client, serviceKey client.ObjectKey) (map[string]string, error) { - service, err := GetService(ctx, k8sClient, serviceKey) - if err != nil { - return nil, err - } - return service.Spec.Selector, nil -} - -func GetService(ctx context.Context, k8sClient client.Client, serviceKey client.ObjectKey) (*corev1.Service, error) { - service := &corev1.Service{} - if err := k8sClient.Get(ctx, serviceKey, service); err != nil { - return nil, err - } - return service, nil -} - // ObjectKeyListDifference computest a - b func ObjectKeyListDifference(a, b []client.ObjectKey) []client.ObjectKey { target := map[client.ObjectKey]bool{} diff --git a/pkg/common/k8s_utils_test.go b/pkg/common/k8s_utils_test.go index ad0607eb9..9a3d07283 100644 --- a/pkg/common/k8s_utils_test.go +++ b/pkg/common/k8s_utils_test.go @@ -10,15 +10,15 @@ import ( "time" appsv1 "k8s.io/api/apps/v1" - "k8s.io/apimachinery/pkg/util/intstr" - - "github.com/kuadrant/limitador-operator/api/v1alpha1" - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/kuadrant/limitador-operator/api/v1alpha1" + + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" ) func TestObjectKeyListDifference(t *testing.T) { @@ -88,70 +88,6 @@ func TestObjectKeyListDifference(t *testing.T) { } } -func TestGetService(t *testing.T) { - service := &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "svc-ns", - Name: "my-svc", - Labels: map[string]string{ - "a-label": "irrelevant", - }, - }, - Spec: corev1.ServiceSpec{ - Selector: map[string]string{ - "a-selector": "what-we-are-looking-for", - }, - }, - } - - k8sClient := fake.NewClientBuilder().WithRuntimeObjects(service).Build() - - var svc *corev1.Service - var err error - - svc, err = GetService(context.TODO(), k8sClient, client.ObjectKey{Namespace: "svc-ns", Name: "my-svc"}) - if err != nil || svc == nil || svc.GetNamespace() != service.GetNamespace() || svc.GetName() != service.GetName() { - t.Error("should have gotten Service svc-ns/my-svc") - } - - svc, err = GetService(context.TODO(), k8sClient, client.ObjectKey{Namespace: "svc-ns", Name: "unknown"}) - if err == nil || !apierrors.IsNotFound(err) || svc != nil { - t.Error("should have gotten no Service") - } -} - -func TestGetServiceWorkloadSelector(t *testing.T) { - service := &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "svc-ns", - Name: "my-svc", - Labels: map[string]string{ - "a-label": "irrelevant", - }, - }, - Spec: corev1.ServiceSpec{ - Selector: map[string]string{ - "a-selector": "what-we-are-looking-for", - }, - }, - } - - k8sClient := fake.NewClientBuilder().WithRuntimeObjects(service).Build() - - var selector map[string]string - var err error - - selector, err = GetServiceWorkloadSelector(context.TODO(), k8sClient, client.ObjectKey{Namespace: "svc-ns", Name: "my-svc"}) - if err != nil || len(selector) != 1 || selector["a-selector"] != "what-we-are-looking-for" { - t.Error("should not have failed to get the service workload selector") - } - - selector, err = GetServiceWorkloadSelector(context.TODO(), k8sClient, client.ObjectKey{Namespace: "svc-ns", Name: "unknown-svc"}) - if err == nil || !apierrors.IsNotFound(err) || selector != nil { - t.Error("should have failed to get the service workload selector") - } -} - func TestObjectInfo(t *testing.T) { testCases := []struct { name string @@ -207,46 +143,6 @@ func TestObjectInfo(t *testing.T) { } } -func TestReadAnnotationsFromObject(t *testing.T) { - testCases := []struct { - name string - input client.Object - expected map[string]string - }{ - { - name: "when object has annotations then return the annotations", - input: &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - "key1": "value1", - "key2": "value2", - }, - }, - }, - expected: map[string]string{ - "key1": "value1", - "key2": "value2", - }, - }, - { - name: "when object has no annotations then return an empty map", - input: &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{}, - }, - expected: map[string]string{}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - actual := ReadAnnotationsFromObject(tc.input) - if !reflect.DeepEqual(actual, tc.expected) { - t.Errorf("Expected annotations %v, but got %v", tc.expected, actual) - } - }) - } -} - func TestTagObjectToDelete(t *testing.T) { testCases := []struct { name string @@ -712,7 +608,7 @@ func TestGetServicePortNumber(t *testing.T) { } } - portNumber, err := GetServicePortNumber(ctx, k8sClient, tt.serviceKey, tt.servicePort) + portNumber, err := utils.GetServicePortNumber(ctx, k8sClient, tt.serviceKey, tt.servicePort) if err != nil && tt.expectedErr == nil { t.Errorf("unexpected error: %v", err) diff --git a/pkg/library/README.md b/pkg/library/README.md new file mode 100644 index 000000000..167a7145d --- /dev/null +++ b/pkg/library/README.md @@ -0,0 +1,2 @@ +Packages in this directory contains only contain common / useful functionality for policy controllers that will be +externalised to the https://github.com/Kuadrant/gateway-api-machinery diff --git a/pkg/common/apimachinery_status_conditions.go b/pkg/library/kuadrant/apimachinery_status_conditions.go similarity index 74% rename from pkg/common/apimachinery_status_conditions.go rename to pkg/library/kuadrant/apimachinery_status_conditions.go index c500aaf0f..e16da4ce8 100644 --- a/pkg/common/apimachinery_status_conditions.go +++ b/pkg/library/kuadrant/apimachinery_status_conditions.go @@ -1,4 +1,4 @@ -package common +package kuadrant import ( "encoding/json" @@ -32,8 +32,8 @@ type OverriddenPolicyMap struct { mu sync.RWMutex } -// SetOverriddenPolicy sets the provided KuadrantPolicy as overridden in the tracking map. -func (o *OverriddenPolicyMap) SetOverriddenPolicy(p KuadrantPolicy) { +// SetOverriddenPolicy sets the provided Policy as overridden in the tracking map. +func (o *OverriddenPolicyMap) SetOverriddenPolicy(p Policy) { o.mu.Lock() defer o.mu.Unlock() @@ -43,16 +43,16 @@ func (o *OverriddenPolicyMap) SetOverriddenPolicy(p KuadrantPolicy) { o.policies[p.GetUID()] = true } -// RemoveOverriddenPolicy removes the provided KuadrantPolicy from the tracking map of overridden policies. -func (o *OverriddenPolicyMap) RemoveOverriddenPolicy(p KuadrantPolicy) { +// RemoveOverriddenPolicy removes the provided Policy from the tracking map of overridden policies. +func (o *OverriddenPolicyMap) RemoveOverriddenPolicy(p Policy) { o.mu.Lock() defer o.mu.Unlock() delete(o.policies, p.GetUID()) } -// IsPolicyOverridden checks if the provided KuadrantPolicy is overridden based on the tracking map maintained. -func (o *OverriddenPolicyMap) IsPolicyOverridden(p KuadrantPolicy) bool { +// IsPolicyOverridden checks if the provided Policy is overridden based on the tracking map maintained. +func (o *OverriddenPolicyMap) IsPolicyOverridden(p Policy) bool { return o.policies[p.GetUID()] && IsTargetRefGateway(p.GetTargetRef()) } @@ -66,13 +66,13 @@ func ConditionMarshal(conditions []metav1.Condition) ([]byte, error) { } // AcceptedCondition returns an accepted conditions with common reasons for a kuadrant policy -func AcceptedCondition(policy KuadrantPolicy, err error) *metav1.Condition { +func AcceptedCondition(p Policy, err error) *metav1.Condition { // Accepted cond := &metav1.Condition{ Type: string(gatewayapiv1alpha2.PolicyConditionAccepted), Status: metav1.ConditionTrue, Reason: string(gatewayapiv1alpha2.PolicyReasonAccepted), - Message: fmt.Sprintf("%s has been accepted", policy.Kind()), + Message: fmt.Sprintf("%s has been accepted", p.Kind()), } if err == nil { return cond @@ -81,7 +81,7 @@ func AcceptedCondition(policy KuadrantPolicy, err error) *metav1.Condition { // Wrap error into a PolicyError if it is not this type var policyErr PolicyError if !errors.As(err, &policyErr) { - policyErr = NewErrUnknown(policy.Kind(), err) + policyErr = NewErrUnknown(p.Kind(), err) } cond.Status = metav1.ConditionFalse @@ -92,7 +92,7 @@ func AcceptedCondition(policy KuadrantPolicy, err error) *metav1.Condition { } // EnforcedCondition returns an enforced conditions with common reasons for a kuadrant policy -func EnforcedCondition(policy KuadrantPolicy, err PolicyError) *metav1.Condition { +func EnforcedCondition(policy Policy, err PolicyError) *metav1.Condition { // Enforced cond := &metav1.Condition{ Type: string(PolicyConditionEnforced), diff --git a/pkg/common/apimachinery_status_conditions_test.go b/pkg/library/kuadrant/apimachinery_status_conditions_test.go similarity index 98% rename from pkg/common/apimachinery_status_conditions_test.go rename to pkg/library/kuadrant/apimachinery_status_conditions_test.go index c1f86d29c..ffb6819a0 100644 --- a/pkg/common/apimachinery_status_conditions_test.go +++ b/pkg/library/kuadrant/apimachinery_status_conditions_test.go @@ -1,6 +1,6 @@ //go:build unit -package common +package kuadrant import ( "errors" @@ -12,6 +12,7 @@ import ( apiErrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" ) @@ -121,7 +122,7 @@ func TestConditionMarshal(t *testing.T) { func TestAcceptedCondition(t *testing.T) { type args struct { - policy KuadrantPolicy + policy Policy err error } tests := []struct { @@ -146,7 +147,7 @@ func TestAcceptedCondition(t *testing.T) { args: args{ policy: &FakePolicy{}, err: NewErrTargetNotFound("FakePolicy", gatewayapiv1alpha2.PolicyTargetReference{ - Group: "gateway.networking.k8s.io", + Group: gatewayapiv1.GroupName, Kind: "HTTPRoute", Name: "my-target-ref", }, apiErrors.NewNotFound(schema.GroupResource{}, "my-target-ref")), @@ -163,7 +164,7 @@ func TestAcceptedCondition(t *testing.T) { args: args{ policy: &FakePolicy{}, err: NewErrTargetNotFound("FakePolicy", gatewayapiv1alpha2.PolicyTargetReference{ - Group: "gateway.networking.k8s.io", + Group: gatewayapiv1.GroupName, Kind: "HTTPRoute", Name: "my-target-ref", }, errors.New("deletion err")), @@ -226,7 +227,7 @@ func TestAcceptedCondition(t *testing.T) { func TestEnforcedCondition(t *testing.T) { type args struct { - policy KuadrantPolicy + policy Policy err PolicyError } policy := &FakePolicy{} diff --git a/pkg/common/errors.go b/pkg/library/kuadrant/errors.go similarity index 99% rename from pkg/common/errors.go rename to pkg/library/kuadrant/errors.go index e8bc21e46..42847d823 100644 --- a/pkg/common/errors.go +++ b/pkg/library/kuadrant/errors.go @@ -1,4 +1,4 @@ -package common +package kuadrant import ( "errors" diff --git a/pkg/common/errors_test.go b/pkg/library/kuadrant/errors_test.go similarity index 97% rename from pkg/common/errors_test.go rename to pkg/library/kuadrant/errors_test.go index 2ed777dce..aa20d1bee 100644 --- a/pkg/common/errors_test.go +++ b/pkg/library/kuadrant/errors_test.go @@ -1,4 +1,4 @@ -package common +package kuadrant import ( "errors" diff --git a/pkg/library/kuadrant/gateway_wrapper.go b/pkg/library/kuadrant/gateway_wrapper.go new file mode 100644 index 000000000..72ea38379 --- /dev/null +++ b/pkg/library/kuadrant/gateway_wrapper.go @@ -0,0 +1,160 @@ +package kuadrant + +import ( + "encoding/json" + "slices" + + "sigs.k8s.io/controller-runtime/pkg/client" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" +) + +// GatewayWrapper wraps a Gateway API Gateway adding methods and configs to manage policy references in annotations +type GatewayWrapper struct { + *gatewayapiv1.Gateway + Referrer +} + +func (g GatewayWrapper) Key() client.ObjectKey { + if g.Gateway == nil { + return client.ObjectKey{} + } + return client.ObjectKeyFromObject(g.Gateway) +} + +func (g GatewayWrapper) PolicyRefs() []client.ObjectKey { + if g.Gateway == nil { + return make([]client.ObjectKey, 0) + } + + gwAnnotations := utils.ReadAnnotationsFromObject(g) + + val, ok := gwAnnotations[g.BackReferenceAnnotationName()] + if !ok { + return make([]client.ObjectKey, 0) + } + + refs := BackReferencesFromObject(g.Gateway, g.Referrer) + + err := json.Unmarshal([]byte(val), &refs) + if err != nil { + return make([]client.ObjectKey, 0) + } + + return refs +} + +func (g GatewayWrapper) ContainsPolicy(policyKey client.ObjectKey) bool { + if g.Gateway == nil { + return false + } + refs := BackReferencesFromObject(g.Gateway, g.Referrer) + return slices.Contains(refs, policyKey) +} + +// AddPolicy tries to add a policy to the existing ref list. +// Returns true if policy was added, false otherwise +func (g GatewayWrapper) AddPolicy(policyKey client.ObjectKey) bool { + if g.Gateway == nil { + return false + } + + // annotation exists and contains a back reference to the policy → nothing to do + if g.ContainsPolicy(policyKey) { + return false + } + + gwAnnotations := utils.ReadAnnotationsFromObject(g) + _, annotationFound := gwAnnotations[g.BackReferenceAnnotationName()] + + // annotation does not exist → create it + if !annotationFound { + refs := []client.ObjectKey{policyKey} + serialized, err := json.Marshal(refs) + if err != nil { + return false + } + gwAnnotations[g.BackReferenceAnnotationName()] = string(serialized) + g.SetAnnotations(gwAnnotations) + return true + } + + // annotation exists and does not contain a back reference to the policy → add the policy to it + refs := append(BackReferencesFromObject(g.Gateway, g.Referrer), policyKey) + serialized, err := json.Marshal(refs) + if err != nil { + return false + } + gwAnnotations[g.BackReferenceAnnotationName()] = string(serialized) + g.SetAnnotations(gwAnnotations) + return true +} + +// DeletePolicy tries to delete a policy from the existing ref list. +// Returns true if the policy was deleted, false otherwise +func (g GatewayWrapper) DeletePolicy(policyKey client.ObjectKey) bool { + if g.Gateway == nil { + return false + } + + gwAnnotations := utils.ReadAnnotationsFromObject(g) + + // annotation does not exist → nothing to do + refsAsStr, annotationFound := gwAnnotations[g.BackReferenceAnnotationName()] + if !annotationFound { + return false + } + + var refs []client.ObjectKey + err := json.Unmarshal([]byte(refsAsStr), &refs) + if err != nil { + return false + } + + // annotation exists and contains a back reference to the policy → remove the policy from it + if idx := slices.Index(refs, policyKey); idx >= 0 { + refs = append(refs[:idx], refs[idx+1:]...) + serialized, err := json.Marshal(refs) + if err != nil { + return false + } + gwAnnotations[g.BackReferenceAnnotationName()] = string(serialized) + g.SetAnnotations(gwAnnotations) + return true + } + + // annotation exists and does not contain a back reference the policy → nothing to do + return false +} + +// Hostnames builds a list of hostnames from the listeners. +func (g GatewayWrapper) Hostnames() []gatewayapiv1.Hostname { + hostnames := make([]gatewayapiv1.Hostname, 0) + if g.Gateway == nil { + return hostnames + } + + for idx := range g.Spec.Listeners { + if g.Spec.Listeners[idx].Hostname != nil { + hostnames = append(hostnames, *g.Spec.Listeners[idx].Hostname) + } + } + + return hostnames +} + +// GatewayWrapperList is a list of GatewayWrappers that implements sort.Interface +type GatewayWrapperList []GatewayWrapper + +func (g GatewayWrapperList) Len() int { + return len(g) +} + +func (g GatewayWrapperList) Less(i, j int) bool { + return g[i].CreationTimestamp.Before(&g[j].CreationTimestamp) +} + +func (g GatewayWrapperList) Swap(i, j int) { + g[i], g[j] = g[j], g[i] +} diff --git a/pkg/library/kuadrant/gateway_wrapper_test.go b/pkg/library/kuadrant/gateway_wrapper_test.go new file mode 100644 index 000000000..40a98f59f --- /dev/null +++ b/pkg/library/kuadrant/gateway_wrapper_test.go @@ -0,0 +1,173 @@ +package kuadrant + +import ( + "slices" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8stypes "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" +) + +func TestGatewayWrapperKey(t *testing.T) { + gw := GatewayWrapper{ + Gateway: &gatewayapiv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "gw-ns", + Name: "gw-1", + Annotations: map[string]string{"kuadrant.io/testpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}, + }, + }, + Referrer: &PolicyKindStub{}, + } + if gw.Key().Namespace != "gw-ns" || gw.Key().Name != "gw-1" { + t.Fail() + } +} + +func TestGatewayWrapperPolicyRefs(t *testing.T) { + gw := GatewayWrapper{ + Gateway: &gatewayapiv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "gw-ns", + Name: "gw-1", + Annotations: map[string]string{"kuadrant.io/testpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}, + }, + }, + Referrer: &PolicyKindStub{}, + } + refs := utils.Map(gw.PolicyRefs(), func(ref k8stypes.NamespacedName) string { return ref.String() }) + if !slices.Contains(refs, "app-ns/policy-1") { + t.Error("GatewayWrapper.PolicyRefs() should contain app-ns/policy-1") + } + if !slices.Contains(refs, "app-ns/policy-2") { + t.Error("GatewayWrapper.PolicyRefs() should contain app-ns/policy-2") + } + if len(refs) != 2 { + t.Fail() + } +} + +func TestGatewayWrapperContainsPolicy(t *testing.T) { + gw := GatewayWrapper{ + Gateway: &gatewayapiv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "gw-ns", + Name: "gw-1", + Annotations: map[string]string{"kuadrant.io/testpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}, + }, + }, + Referrer: &PolicyKindStub{}, + } + if !gw.ContainsPolicy(client.ObjectKey{Namespace: "app-ns", Name: "policy-1"}) { + t.Error("GatewayWrapper.ContainsPolicy() should contain app-ns/policy-1") + } + if !gw.ContainsPolicy(client.ObjectKey{Namespace: "app-ns", Name: "policy-2"}) { + t.Error("GatewayWrapper.ContainsPolicy() should contain app-ns/policy-1") + } + if gw.ContainsPolicy(client.ObjectKey{Namespace: "app-ns", Name: "policy-3"}) { + t.Error("GatewayWrapper.ContainsPolicy() should not contain app-ns/policy-1") + } +} + +func TestGatewayWrapperAddPolicy(t *testing.T) { + gateway := gatewayapiv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "gw-ns", + Name: "gw-1", + Annotations: map[string]string{"kuadrant.io/testpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}, + }, + } + gw := GatewayWrapper{ + Gateway: &gateway, + Referrer: &PolicyKindStub{}, + } + if gw.AddPolicy(client.ObjectKey{Namespace: "app-ns", Name: "policy-1"}) { + t.Error("GatewayWrapper.AddPolicy() expected to return false") + } + if !gw.AddPolicy(client.ObjectKey{Namespace: "app-ns", Name: "policy-3"}) { + t.Error("GatewayWrapper.AddPolicy() expected to return true") + } + if gw.Annotations["kuadrant.io/testpolicies"] != `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"},{"Namespace":"app-ns","Name":"policy-3"}]` { + t.Error("GatewayWrapper.AddPolicy() expected to have added policy ref to the annotations") + } +} + +func TestGatewayDeletePolicy(t *testing.T) { + gateway := gatewayapiv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "gw-ns", + Name: "gw-1", + Annotations: map[string]string{"kuadrant.io/testpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}, + }, + } + gw := GatewayWrapper{ + Gateway: &gateway, + Referrer: &PolicyKindStub{}, + } + if !gw.DeletePolicy(client.ObjectKey{Namespace: "app-ns", Name: "policy-1"}) { + t.Error("GatewayWrapper.DeletePolicy() expected to return true") + } + if gw.DeletePolicy(client.ObjectKey{Namespace: "app-ns", Name: "policy-3"}) { + t.Error("GatewayWrapper.DeletePolicy() expected to return false") + } + if gw.Annotations["kuadrant.io/testpolicies"] != `[{"Namespace":"app-ns","Name":"policy-2"}]` { + t.Error("GatewayWrapper.DeletePolicy() expected to have deleted policy ref from the annotations") + } +} + +func TestGatewayHostnames(t *testing.T) { + hostname := gatewayapiv1.Hostname("toystore.com") + gateway := gatewayapiv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "gw-ns", + Name: "gw-1", + Annotations: map[string]string{"kuadrant.io/ratelimitpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}, + }, + Spec: gatewayapiv1.GatewaySpec{ + Listeners: []gatewayapiv1.Listener{ + { + Name: "my-listener", + Hostname: &hostname, + }, + }, + }, + } + gw := GatewayWrapper{ + Gateway: &gateway, + Referrer: &PolicyKindStub{}, + } + hostnames := gw.Hostnames() + if !slices.Contains(hostnames, "toystore.com") { + t.Error("GatewayWrapper.Hostnames() expected to contain 'toystore.com'") + } + if len(hostnames) != 1 { + t.Fail() + } +} + +func TestBackReferencesFromGatewayWrapper(t *testing.T) { + gw := GatewayWrapper{ + Gateway: &gatewayapiv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "gw-ns", + Name: "gw-1", + Annotations: map[string]string{"kuadrant.io/testpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}, + }, + }, + Referrer: &PolicyKindStub{}, + } + refs := utils.Map(BackReferencesFromObject(gw.Gateway, gw.Referrer), func(ref client.ObjectKey) string { return ref.String() }) + if !slices.Contains(refs, "app-ns/policy-1") { + t.Error("GatewayWrapper.PolicyRefs() should contain app-ns/policy-1") + } + if !slices.Contains(refs, "app-ns/policy-2") { + t.Error("GatewayWrapper.PolicyRefs() should contain app-ns/policy-2") + } + if len(refs) != 2 { + t.Fail() + } +} diff --git a/pkg/common/gatewayapi_utils.go b/pkg/library/kuadrant/gatewayapi_utils.go similarity index 58% rename from pkg/common/gatewayapi_utils.go rename to pkg/library/kuadrant/gatewayapi_utils.go index dc4a0feda..fcaaa17e0 100644 --- a/pkg/common/gatewayapi_utils.go +++ b/pkg/library/kuadrant/gatewayapi_utils.go @@ -1,11 +1,9 @@ -package common +package kuadrant import ( "context" - "encoding/json" "fmt" "reflect" - "slices" "strings" "k8s.io/apimachinery/pkg/api/errors" @@ -15,9 +13,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" -) -const GatewayProgrammedConditionType = "Programmed" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" +) type HTTPRouteRule struct { Paths []string @@ -26,11 +24,11 @@ type HTTPRouteRule struct { } func IsTargetRefHTTPRoute(targetRef gatewayapiv1alpha2.PolicyTargetReference) bool { - return targetRef.Group == ("gateway.networking.k8s.io") && targetRef.Kind == ("HTTPRoute") + return targetRef.Group == (gatewayapiv1.GroupName) && targetRef.Kind == ("HTTPRoute") } func IsTargetRefGateway(targetRef gatewayapiv1alpha2.PolicyTargetReference) bool { - return targetRef.Group == ("gateway.networking.k8s.io") && targetRef.Kind == ("Gateway") + return targetRef.Group == (gatewayapiv1.GroupName) && targetRef.Kind == ("Gateway") } func RouteHTTPMethodToRuleMethod(httpMethod *gatewayapiv1.HTTPMethod) []string { @@ -102,7 +100,7 @@ func (s *HTTPRouteRuleSelector) Selects(rule gatewayapiv1.HTTPRouteRule) bool { return true } - _, found := Find(rule.Matches, func(ruleMatch gatewayapiv1.HTTPRouteMatch) bool { + _, found := utils.Find(rule.Matches, func(ruleMatch gatewayapiv1.HTTPRouteMatch) bool { // path if s.Path != nil && !reflect.DeepEqual(s.Path, ruleMatch.Path) { return false @@ -115,7 +113,7 @@ func (s *HTTPRouteRuleSelector) Selects(rule gatewayapiv1.HTTPRouteRule) bool { // headers for _, header := range s.Headers { - if _, found := Find(ruleMatch.Headers, func(otherHeader gatewayapiv1.HTTPHeaderMatch) bool { + if _, found := utils.Find(ruleMatch.Headers, func(otherHeader gatewayapiv1.HTTPHeaderMatch) bool { return reflect.DeepEqual(header, otherHeader) }); !found { return false @@ -124,7 +122,7 @@ func (s *HTTPRouteRuleSelector) Selects(rule gatewayapiv1.HTTPRouteRule) bool { // query params for _, param := range s.QueryParams { - if _, found := Find(ruleMatch.QueryParams, func(otherParam gatewayapiv1.HTTPQueryParamMatch) bool { + if _, found := utils.Find(ruleMatch.QueryParams, func(otherParam gatewayapiv1.HTTPQueryParamMatch) bool { return reflect.DeepEqual(param, otherParam) }); !found { return false @@ -139,7 +137,7 @@ func (s *HTTPRouteRuleSelector) Selects(rule gatewayapiv1.HTTPRouteRule) bool { // HTTPRouteRuleToString prints the matches of a HTTPRouteRule as string func HTTPRouteRuleToString(rule gatewayapiv1.HTTPRouteRule) string { - matches := Map(rule.Matches, HTTPRouteMatchToString) + matches := utils.Map(rule.Matches, HTTPRouteMatchToString) return fmt.Sprintf("{matches:[%s]}", strings.Join(matches, ",")) } @@ -152,11 +150,11 @@ func HTTPRouteMatchToString(match gatewayapiv1.HTTPRouteMatch) string { patterns = append(patterns, fmt.Sprintf("path:%s", HTTPPathMatchToString(path))) } if len(match.QueryParams) > 0 { - queryParams := Map(match.QueryParams, HTTPQueryParamMatchToString) + queryParams := utils.Map(match.QueryParams, HTTPQueryParamMatchToString) patterns = append(patterns, fmt.Sprintf("queryParams:[%s]", strings.Join(queryParams, ","))) } if len(match.Headers) > 0 { - headers := Map(match.Headers, HTTPHeaderMatchToString) + headers := utils.Map(match.Headers, HTTPHeaderMatchToString) patterns = append(patterns, fmt.Sprintf("headers:[%s]", strings.Join(headers, ","))) } return fmt.Sprintf("{%s}", strings.Join(patterns, ",")) @@ -204,7 +202,7 @@ func HTTPMethodToString(method *gatewayapiv1.HTTPMethod) string { return string(*method) } -func GetKuadrantNamespaceFromPolicyTargetRef(ctx context.Context, cli client.Client, policy KuadrantPolicy) (string, error) { +func GetKuadrantNamespaceFromPolicyTargetRef(ctx context.Context, cli client.Client, policy Policy) (string, error) { targetRef := policy.GetTargetRef() gwNamespacedName := types.NamespacedName{Namespace: string(ptr.Deref(targetRef.Namespace, policy.GetWrappedNamespace())), Name: string(targetRef.Name)} if IsTargetRefHTTPRoute(targetRef) { @@ -227,8 +225,8 @@ func GetKuadrantNamespaceFromPolicyTargetRef(ctx context.Context, cli client.Cli return GetKuadrantNamespace(gw) } -func GetKuadrantNamespaceFromPolicy(policy KuadrantPolicy) (string, bool) { - if kuadrantNamespace, isSet := policy.GetAnnotations()[KuadrantNamespaceLabel]; isSet { +func GetKuadrantNamespaceFromPolicy(p Policy) (string, bool) { + if kuadrantNamespace, isSet := p.GetAnnotations()[KuadrantNamespaceLabel]; isSet { return kuadrantNamespace, true } return "", false @@ -241,11 +239,6 @@ func GetKuadrantNamespace(obj client.Object) (string, error) { return obj.GetAnnotations()[KuadrantNamespaceLabel], nil } -func IsKuadrantManaged(obj client.Object) bool { - _, isSet := obj.GetAnnotations()[KuadrantNamespaceLabel] - return isSet -} - func AnnotateObject(obj client.Object, namespace string) { annotations := obj.GetAnnotations() if len(annotations) == 0 { @@ -296,239 +289,6 @@ func routePathMatchToRulePath(pathMatch *gatewayapiv1.HTTPPathMatch) []string { return []string{val + suffix} } -type PolicyRefsConfig interface { - PolicyRefsAnnotation() string -} - -type KuadrantRateLimitPolicyRefsConfig struct{} - -func (c *KuadrantRateLimitPolicyRefsConfig) PolicyRefsAnnotation() string { - return RateLimitPoliciesBackRefAnnotation -} - -type KuadrantAuthPolicyRefsConfig struct{} - -func (c *KuadrantAuthPolicyRefsConfig) PolicyRefsAnnotation() string { - return AuthPoliciesBackRefAnnotation -} - -type KuadrantTLSPolicyRefsConfig struct{} - -func (c *KuadrantTLSPolicyRefsConfig) PolicyRefsAnnotation() string { - return TLSPoliciesBackRefAnnotation -} - -type KuadrantDNSPolicyRefsConfig struct{} - -func (c *KuadrantDNSPolicyRefsConfig) PolicyRefsAnnotation() string { - return DNSPoliciesBackRefAnnotation -} - -func GatewaysMissingPolicyRef(gwList *gatewayapiv1.GatewayList, policyKey client.ObjectKey, policyGwKeys []client.ObjectKey, config PolicyRefsConfig) []GatewayWrapper { - // gateways referenced by the policy but do not have reference to it in the annotations - gateways := make([]GatewayWrapper, 0) - for i := range gwList.Items { - gateway := gwList.Items[i] - gw := GatewayWrapper{&gateway, config} - if slices.Contains(policyGwKeys, client.ObjectKeyFromObject(&gateway)) && !gw.ContainsPolicy(policyKey) { - gateways = append(gateways, gw) - } - } - return gateways -} - -func GatewaysWithValidPolicyRef(gwList *gatewayapiv1.GatewayList, policyKey client.ObjectKey, policyGwKeys []client.ObjectKey, config PolicyRefsConfig) []GatewayWrapper { - // gateways referenced by the policy but also have reference to it in the annotations - gateways := make([]GatewayWrapper, 0) - for i := range gwList.Items { - gateway := gwList.Items[i] - gw := GatewayWrapper{&gateway, config} - if slices.Contains(policyGwKeys, client.ObjectKeyFromObject(&gateway)) && gw.ContainsPolicy(policyKey) { - gateways = append(gateways, gw) - } - } - return gateways -} - -func GatewaysWithInvalidPolicyRef(gwList *gatewayapiv1.GatewayList, policyKey client.ObjectKey, policyGwKeys []client.ObjectKey, config PolicyRefsConfig) []GatewayWrapper { - // gateways not referenced by the policy but still have reference in the annotations - gateways := make([]GatewayWrapper, 0) - for i := range gwList.Items { - gateway := gwList.Items[i] - gw := GatewayWrapper{&gateway, config} - if !slices.Contains(policyGwKeys, client.ObjectKeyFromObject(&gateway)) && gw.ContainsPolicy(policyKey) { - gateways = append(gateways, gw) - } - } - return gateways -} - -// GatewayWrapper wraps a Gateway API Gateway adding methods and configs to manage policy references in annotations -type GatewayWrapper struct { - *gatewayapiv1.Gateway - PolicyRefsConfig -} - -func (g GatewayWrapper) Key() client.ObjectKey { - if g.Gateway == nil { - return client.ObjectKey{} - } - return client.ObjectKeyFromObject(g.Gateway) -} - -func (g GatewayWrapper) PolicyRefs() []client.ObjectKey { - if g.Gateway == nil { - return make([]client.ObjectKey, 0) - } - - gwAnnotations := ReadAnnotationsFromObject(g) - - val, ok := gwAnnotations[g.PolicyRefsAnnotation()] - if !ok { - return make([]client.ObjectKey, 0) - } - - var refs []client.ObjectKey - - err := json.Unmarshal([]byte(val), &refs) - if err != nil { - return make([]client.ObjectKey, 0) - } - - return refs -} - -func (g GatewayWrapper) ContainsPolicy(policyKey client.ObjectKey) bool { - if g.Gateway == nil { - return false - } - - gwAnnotations := ReadAnnotationsFromObject(g) - - val, ok := gwAnnotations[g.PolicyRefsAnnotation()] - if !ok { - return false - } - - var refs []client.ObjectKey - - err := json.Unmarshal([]byte(val), &refs) - if err != nil { - return false - } - - return slices.Contains(refs, policyKey) -} - -// AddPolicy tries to add a policy to the existing ref list. -// Returns true if policy was added, false otherwise -func (g GatewayWrapper) AddPolicy(policyKey client.ObjectKey) bool { - if g.Gateway == nil { - return false - } - - gwAnnotations := ReadAnnotationsFromObject(g) - - val, ok := gwAnnotations[g.PolicyRefsAnnotation()] - if !ok { - refs := []client.ObjectKey{policyKey} - serialized, err := json.Marshal(refs) - if err != nil { - return false - } - gwAnnotations[g.PolicyRefsAnnotation()] = string(serialized) - g.SetAnnotations(gwAnnotations) - return true - } - - var refs []client.ObjectKey - - err := json.Unmarshal([]byte(val), &refs) - if err != nil { - return false - } - - if slices.Contains(refs, policyKey) { - return false - } - - refs = append(refs, policyKey) - serialized, err := json.Marshal(refs) - if err != nil { - return false - } - gwAnnotations[g.PolicyRefsAnnotation()] = string(serialized) - g.SetAnnotations(gwAnnotations) - return true -} - -// DeletePolicy tries to delete a policy from the existing ref list. -// Returns true if the policy was deleted, false otherwise -func (g GatewayWrapper) DeletePolicy(policyKey client.ObjectKey) bool { - if g.Gateway == nil { - return false - } - - gwAnnotations := ReadAnnotationsFromObject(g) - - val, ok := gwAnnotations[g.PolicyRefsAnnotation()] - if !ok { - return false - } - - var refs []client.ObjectKey - - err := json.Unmarshal([]byte(val), &refs) - if err != nil { - return false - } - - if refID := FindObjectKey(refs, policyKey); refID != len(refs) { - // remove index - refs = append(refs[:refID], refs[refID+1:]...) - serialized, err := json.Marshal(refs) - if err != nil { - return false - } - gwAnnotations[g.PolicyRefsAnnotation()] = string(serialized) - g.SetAnnotations(gwAnnotations) - return true - } - - return false -} - -// Hostnames builds a list of hostnames from the listeners. -func (g GatewayWrapper) Hostnames() []gatewayapiv1.Hostname { - hostnames := make([]gatewayapiv1.Hostname, 0) - if g.Gateway == nil { - return hostnames - } - - for idx := range g.Spec.Listeners { - if g.Spec.Listeners[idx].Hostname != nil { - hostnames = append(hostnames, *g.Spec.Listeners[idx].Hostname) - } - } - - return hostnames -} - -// GatewayWrapperList is a list of GatewayWrappers that implements sort.Interface -type GatewayWrapperList []GatewayWrapper - -func (g GatewayWrapperList) Len() int { - return len(g) -} - -func (g GatewayWrapperList) Less(i, j int) bool { - return g[i].CreationTimestamp.Before(&g[j].CreationTimestamp) -} - -func (g GatewayWrapperList) Swap(i, j int) { - g[i], g[j] = g[j], g[i] -} - // TargetHostnames returns an array of hostnames coming from the network object (HTTPRoute, Gateway) func TargetHostnames(targetNetworkObject client.Object) ([]string, error) { hosts := make([]string, 0) @@ -561,7 +321,7 @@ func HostnamesFromHTTPRoute(ctx context.Context, route *gatewayapiv1.HTTPRoute, hosts := []string{} for _, ref := range route.Spec.ParentRefs { - if (ref.Kind != nil && *ref.Kind != "Gateway") || (ref.Group != nil && *ref.Group != "gateway.networking.k8s.io") { + if (ref.Kind != nil && *ref.Kind != "Gateway") || (ref.Group != nil && *ref.Group != gatewayapiv1.GroupName) { continue } gw := &gatewayapiv1.Gateway{} @@ -572,7 +332,7 @@ func HostnamesFromHTTPRoute(ctx context.Context, route *gatewayapiv1.HTTPRoute, if err := cli.Get(ctx, types.NamespacedName{Namespace: ns, Name: string(ref.Name)}, gw); err != nil { return nil, err } - gwHostanmes := HostnamesToStrings(GatewayWrapper{Gateway: gw}.Hostnames()) + gwHostanmes := utils.HostnamesToStrings(GatewayWrapper{Gateway: gw}.Hostnames()) hosts = append(hosts, gwHostanmes...) } @@ -580,13 +340,13 @@ func HostnamesFromHTTPRoute(ctx context.Context, route *gatewayapiv1.HTTPRoute, } // ValidateHierarchicalRules returns error if the policy rules hostnames fail to match the target network hosts -func ValidateHierarchicalRules(policy KuadrantPolicy, targetNetworkObject client.Object) error { +func ValidateHierarchicalRules(policy Policy, targetNetworkObject client.Object) error { targetHostnames, err := TargetHostnames(targetNetworkObject) if err != nil { return err } - if valid, invalidHost := ValidSubdomains(targetHostnames, policy.GetRulesHostnames()); !valid { + if valid, invalidHost := utils.ValidSubdomains(targetHostnames, policy.GetRulesHostnames()); !valid { return fmt.Errorf( "rule host (%s) does not follow any hierarchical constraints, "+ "for the %T to be validated, it must match with at least one of the target network hostnames %+q", @@ -600,7 +360,7 @@ func ValidateHierarchicalRules(policy KuadrantPolicy, targetNetworkObject client } func GetGatewayWorkloadSelector(ctx context.Context, cli client.Client, gateway *gatewayapiv1.Gateway) (map[string]string, error) { - address, found := Find( + address, found := utils.Find( gateway.Status.Addresses, func(address gatewayapiv1.GatewayStatusAddress) bool { return address.Type != nil && *address.Type == gatewayapiv1.HostnameAddressType @@ -614,7 +374,7 @@ func GetGatewayWorkloadSelector(ctx context.Context, cli client.Client, gateway Name: serviceNameParts[0], Namespace: serviceNameParts[1], } - return GetServiceWorkloadSelector(ctx, cli, serviceKey) + return utils.GetServiceWorkloadSelector(ctx, cli, serviceKey) } func IsHTTPRouteAccepted(httpRoute *gatewayapiv1.HTTPRoute) bool { diff --git a/pkg/common/gatewayapi_utils_test.go b/pkg/library/kuadrant/gatewayapi_utils_test.go similarity index 66% rename from pkg/common/gatewayapi_utils_test.go rename to pkg/library/kuadrant/gatewayapi_utils_test.go index c15f1a1d9..3eb3a96ee 100644 --- a/pkg/common/gatewayapi_utils_test.go +++ b/pkg/library/kuadrant/gatewayapi_utils_test.go @@ -1,18 +1,17 @@ //go:build unit -package common +package kuadrant import ( "context" "fmt" "reflect" - "slices" "testing" + "gotest.tools/assert" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - k8stypes "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" @@ -680,384 +679,6 @@ func TestHTTPRouteRuleToString(t *testing.T) { } } -func TestGatewaysMissingPolicyRef(t *testing.T) { - gwList := &gatewayapiv1.GatewayList{ - Items: []gatewayapiv1.Gateway{ - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "gw-ns", - Name: "gw-1", - Annotations: map[string]string{"kuadrant.io/ratelimitpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "gw-ns", - Name: "gw-2", - Annotations: map[string]string{"kuadrant.io/ratelimitpolicies": `[{"Namespace":"app-ns","Name":"policy-1"}]`}, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "gw-ns", - Name: "gw-3", - }, - }, - }, - } - - var gws []string - policyRefConfig := &KuadrantRateLimitPolicyRefsConfig{} - gwName := func(gw GatewayWrapper) string { return gw.Gateway.Name } - - gws = Map(GatewaysMissingPolicyRef(gwList, k8stypes.NamespacedName{Namespace: "app-ns", Name: "policy-1"}, []client.ObjectKey{ - {Namespace: "gw-ns", Name: "gw-2"}, - {Namespace: "gw-ns", Name: "gw-3"}, - }, policyRefConfig), gwName) - - if slices.Contains(gws, "gw-1") { - t.Error("gateway expected not to be listed as missing policy ref") - } - if slices.Contains(gws, "gw-2") { - t.Error("gateway expected not to be listed as missing policy ref") - } - if !slices.Contains(gws, "gw-3") { - t.Error("gateway expected to be listed as missing policy ref") - } - - gws = Map(GatewaysMissingPolicyRef(gwList, k8stypes.NamespacedName{Namespace: "app-ns", Name: "policy-2"}, []client.ObjectKey{ - {Namespace: "gw-ns", Name: "gw-1"}, - }, policyRefConfig), gwName) - - if slices.Contains(gws, "gw-1") { - t.Error("gateway expected not to be listed as missing policy ref") - } - if slices.Contains(gws, "gw-2") { - t.Error("gateway expected not to be listed as missing policy ref") - } - if slices.Contains(gws, "gw-3") { - t.Error("gateway expected not to be listed as missing policy ref") - } - - gws = Map(GatewaysMissingPolicyRef(gwList, k8stypes.NamespacedName{Namespace: "app-ns", Name: "policy-3"}, []client.ObjectKey{ - {Namespace: "gw-ns", Name: "gw-1"}, - {Namespace: "gw-ns", Name: "gw-3"}, - }, policyRefConfig), gwName) - - if !slices.Contains(gws, "gw-1") { - t.Error("gateway expected to be listed as missing policy ref") - } - if slices.Contains(gws, "gw-2") { - t.Error("gateway expected not to be listed as missing policy ref") - } - if !slices.Contains(gws, "gw-3") { - t.Error("gateway expected to be listed as missing policy ref") - } -} - -func TestGatewaysWithValidPolicyRef(t *testing.T) { - gwList := &gatewayapiv1.GatewayList{ - Items: []gatewayapiv1.Gateway{ - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "gw-ns", - Name: "gw-1", - Annotations: map[string]string{"kuadrant.io/ratelimitpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "gw-ns", - Name: "gw-2", - Annotations: map[string]string{"kuadrant.io/ratelimitpolicies": `[{"Namespace":"app-ns","Name":"policy-1"}]`}, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "gw-ns", - Name: "gw-3", - }, - }, - }, - } - - var gws []string - policyRefConfig := &KuadrantRateLimitPolicyRefsConfig{} - gwName := func(gw GatewayWrapper) string { return gw.Gateway.Name } - - gws = Map(GatewaysWithValidPolicyRef(gwList, k8stypes.NamespacedName{Namespace: "app-ns", Name: "policy-1"}, []client.ObjectKey{ - {Namespace: "gw-ns", Name: "gw-2"}, - {Namespace: "gw-ns", Name: "gw-3"}, - }, policyRefConfig), gwName) - - if slices.Contains(gws, "gw-1") { - t.Error("gateway expected not to be listed as with valid policy ref") - } - if !slices.Contains(gws, "gw-2") { - t.Error("gateway expected to be listed as with valid policy ref") - } - if slices.Contains(gws, "gw-3") { - t.Error("gateway expected not to be listed as with valid policy ref") - } - - gws = Map(GatewaysWithValidPolicyRef(gwList, k8stypes.NamespacedName{Namespace: "app-ns", Name: "policy-2"}, []client.ObjectKey{ - {Namespace: "gw-ns", Name: "gw-1"}, - }, policyRefConfig), gwName) - - if !slices.Contains(gws, "gw-1") { - t.Error("gateway expected to be listed as with valid policy ref") - } - if slices.Contains(gws, "gw-2") { - t.Error("gateway expected not to be listed as with valid policy ref") - } - if slices.Contains(gws, "gw-3") { - t.Error("gateway expected not to be listed as with valid policy ref") - } - - gws = Map(GatewaysWithValidPolicyRef(gwList, k8stypes.NamespacedName{Namespace: "app-ns", Name: "policy-3"}, []client.ObjectKey{ - {Namespace: "gw-ns", Name: "gw-1"}, - {Namespace: "gw-ns", Name: "gw-3"}, - }, policyRefConfig), gwName) - - if slices.Contains(gws, "gw-1") { - t.Error("gateway expected not to be listed as with valid policy ref") - } - if slices.Contains(gws, "gw-2") { - t.Error("gateway expected not to be listed as with valid policy ref") - } - if slices.Contains(gws, "gw-3") { - t.Error("gateway expected not to be listed as with valid policy ref") - } -} - -func TestGatewaysWithInvalidPolicyRef(t *testing.T) { - gwList := &gatewayapiv1.GatewayList{ - Items: []gatewayapiv1.Gateway{ - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "gw-ns", - Name: "gw-1", - Annotations: map[string]string{"kuadrant.io/ratelimitpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "gw-ns", - Name: "gw-2", - Annotations: map[string]string{"kuadrant.io/ratelimitpolicies": `[{"Namespace":"app-ns","Name":"policy-1"}]`}, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "gw-ns", - Name: "gw-3", - }, - }, - }, - } - - var gws []string - policyRefConfig := &KuadrantRateLimitPolicyRefsConfig{} - gwName := func(gw GatewayWrapper) string { return gw.Gateway.Name } - - gws = Map(GatewaysWithInvalidPolicyRef(gwList, k8stypes.NamespacedName{Namespace: "app-ns", Name: "policy-1"}, []client.ObjectKey{ - {Namespace: "gw-ns", Name: "gw-2"}, - {Namespace: "gw-ns", Name: "gw-3"}, - }, policyRefConfig), gwName) - - if !slices.Contains(gws, "gw-1") { - t.Error("gateway expected to be listed as with invalid policy ref") - } - if slices.Contains(gws, "gw-2") { - t.Error("gateway expected not to be listed as with invalid policy ref") - } - if slices.Contains(gws, "gw-3") { - t.Error("gateway expected not to be listed as with invalid policy ref") - } - - gws = Map(GatewaysWithInvalidPolicyRef(gwList, k8stypes.NamespacedName{Namespace: "app-ns", Name: "policy-2"}, []client.ObjectKey{ - {Namespace: "gw-ns", Name: "gw-1"}, - }, policyRefConfig), gwName) - - if slices.Contains(gws, "gw-1") { - t.Error("gateway expected not to be listed as with invalid policy ref") - } - if slices.Contains(gws, "gw-2") { - t.Error("gateway expected not to be listed as with invalid policy ref") - } - if slices.Contains(gws, "gw-3") { - t.Error("gateway expected not to be listed as with invalid policy ref") - } - - gws = Map(GatewaysWithInvalidPolicyRef(gwList, k8stypes.NamespacedName{Namespace: "app-ns", Name: "policy-3"}, []client.ObjectKey{ - {Namespace: "gw-ns", Name: "gw-1"}, - {Namespace: "gw-ns", Name: "gw-3"}, - }, policyRefConfig), gwName) - - if slices.Contains(gws, "gw-1") { - t.Error("gateway expected not to be listed as with invalid policy ref") - } - if slices.Contains(gws, "gw-2") { - t.Error("gateway expected not to be listed as with invalid policy ref") - } - if slices.Contains(gws, "gw-3") { - t.Error("gateway expected not to be listed as with invalid policy ref") - } -} - -func TestGatewayWrapperKey(t *testing.T) { - gw := GatewayWrapper{ - Gateway: &gatewayapiv1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "gw-ns", - Name: "gw-1", - Annotations: map[string]string{"kuadrant.io/ratelimitpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}, - }, - }, - PolicyRefsConfig: &KuadrantRateLimitPolicyRefsConfig{}, - } - if gw.Key().Namespace != "gw-ns" || gw.Key().Name != "gw-1" { - t.Fail() - } -} - -func TestGatewayWrapperPolicyRefs(t *testing.T) { - gw := GatewayWrapper{ - Gateway: &gatewayapiv1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "gw-ns", - Name: "gw-1", - Annotations: map[string]string{"kuadrant.io/ratelimitpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}, - }, - }, - PolicyRefsConfig: &KuadrantRateLimitPolicyRefsConfig{}, - } - refs := Map(gw.PolicyRefs(), func(ref k8stypes.NamespacedName) string { return ref.String() }) - if !slices.Contains(refs, "app-ns/policy-1") { - t.Error("GatewayWrapper.PolicyRefs() should contain app-ns/policy-1") - } - if !slices.Contains(refs, "app-ns/policy-2") { - t.Error("GatewayWrapper.PolicyRefs() should contain app-ns/policy-2") - } - if len(refs) != 2 { - t.Fail() - } -} - -func TestGatewayWrapperContainsPolicy(t *testing.T) { - gw := GatewayWrapper{ - Gateway: &gatewayapiv1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "gw-ns", - Name: "gw-1", - Annotations: map[string]string{"kuadrant.io/ratelimitpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}, - }, - }, - PolicyRefsConfig: &KuadrantRateLimitPolicyRefsConfig{}, - } - if !gw.ContainsPolicy(client.ObjectKey{Namespace: "app-ns", Name: "policy-1"}) { - t.Error("GatewayWrapper.ContainsPolicy() should contain app-ns/policy-1") - } - if !gw.ContainsPolicy(client.ObjectKey{Namespace: "app-ns", Name: "policy-2"}) { - t.Error("GatewayWrapper.ContainsPolicy() should contain app-ns/policy-1") - } - if gw.ContainsPolicy(client.ObjectKey{Namespace: "app-ns", Name: "policy-3"}) { - t.Error("GatewayWrapper.ContainsPolicy() should not contain app-ns/policy-1") - } -} - -func TestGatewayWrapperAddPolicy(t *testing.T) { - gateway := gatewayapiv1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "gw-ns", - Name: "gw-1", - Annotations: map[string]string{"kuadrant.io/ratelimitpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}, - }, - } - gw := GatewayWrapper{ - Gateway: &gateway, - PolicyRefsConfig: &KuadrantRateLimitPolicyRefsConfig{}, - } - if gw.AddPolicy(client.ObjectKey{Namespace: "app-ns", Name: "policy-1"}) { - t.Error("GatewayWrapper.AddPolicy() expected to return false") - } - if !gw.AddPolicy(client.ObjectKey{Namespace: "app-ns", Name: "policy-3"}) { - t.Error("GatewayWrapper.AddPolicy() expected to return true") - } - if gw.Annotations["kuadrant.io/ratelimitpolicies"] != `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"},{"Namespace":"app-ns","Name":"policy-3"}]` { - t.Error("GatewayWrapper.AddPolicy() expected to have added policy ref to the annotations") - } -} - -func TestGatewayDeletePolicy(t *testing.T) { - gateway := gatewayapiv1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "gw-ns", - Name: "gw-1", - Annotations: map[string]string{"kuadrant.io/ratelimitpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}, - }, - } - gw := GatewayWrapper{ - Gateway: &gateway, - PolicyRefsConfig: &KuadrantRateLimitPolicyRefsConfig{}, - } - if !gw.DeletePolicy(client.ObjectKey{Namespace: "app-ns", Name: "policy-1"}) { - t.Error("GatewayWrapper.DeletePolicy() expected to return true") - } - if gw.DeletePolicy(client.ObjectKey{Namespace: "app-ns", Name: "policy-3"}) { - t.Error("GatewayWrapper.DeletePolicy() expected to return false") - } - if gw.Annotations["kuadrant.io/ratelimitpolicies"] != `[{"Namespace":"app-ns","Name":"policy-2"}]` { - t.Error("GatewayWrapper.DeletePolicy() expected to have deleted policy ref from the annotations") - } -} - -func TestGatewayHostnames(t *testing.T) { - hostname := gatewayapiv1.Hostname("toystore.com") - gateway := gatewayapiv1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "gw-ns", - Name: "gw-1", - Annotations: map[string]string{"kuadrant.io/ratelimitpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}, - }, - Spec: gatewayapiv1.GatewaySpec{ - Listeners: []gatewayapiv1.Listener{ - { - Name: "my-listener", - Hostname: &hostname, - }, - }, - }, - } - gw := GatewayWrapper{ - Gateway: &gateway, - PolicyRefsConfig: &KuadrantRateLimitPolicyRefsConfig{}, - } - hostnames := gw.Hostnames() - if !slices.Contains(hostnames, "toystore.com") { - t.Error("GatewayWrapper.Hostnames() expected to contain 'toystore.com'") - } - if len(hostnames) != 1 { - t.Fail() - } -} - -func TestGatewayWrapperPolicyRefsAnnotation(t *testing.T) { - gw := GatewayWrapper{ - Gateway: &gatewayapiv1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "gw-ns", - Name: "gw-1", - Annotations: map[string]string{"kuadrant.io/ratelimitpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}, - }, - }, - PolicyRefsConfig: &KuadrantRateLimitPolicyRefsConfig{}, - } - if gw.PolicyRefsAnnotation() != RateLimitPoliciesBackRefAnnotation { - t.Fail() - } -} - func TestGetGatewayWorkloadSelector(t *testing.T) { hostnameAddress := gatewayapiv1.AddressType("Hostname") gateway := &gatewayapiv1.Gateway{ @@ -1196,7 +817,7 @@ func TestGetKuadrantNamespaceFromPolicyTargetRef(t *testing.T) { }, }, targetRef: gatewayapiv1alpha2.PolicyTargetReference{ - Group: "gateway.networking.k8s.io", + Group: gatewayapiv1.GroupName, Kind: "HTTPRoute", Name: "my-httproute", Namespace: ptr.To[gatewayapiv1.Namespace](gatewayapiv1.Namespace("my-ns")), @@ -1239,7 +860,7 @@ func TestGetKuadrantNamespaceFromPolicyTargetRef(t *testing.T) { }, }, targetRef: gatewayapiv1alpha2.PolicyTargetReference{ - Group: "gateway.networking.k8s.io", + Group: gatewayapiv1.GroupName, Kind: "HTTPRoute", Name: "my-httproute", }, @@ -1280,7 +901,7 @@ func TestGetKuadrantNamespaceFromPolicyTargetRef(t *testing.T) { }, }, targetRef: gatewayapiv1alpha2.PolicyTargetReference{ - Group: "gateway.networking.k8s.io", + Group: gatewayapiv1.GroupName, Kind: "HTTPRoute", Name: "my-httproute", }, @@ -1305,16 +926,18 @@ func TestGetKuadrantNamespaceFromPolicyTargetRef(t *testing.T) { func TestValidateHierarchicalRules(t *testing.T) { hostname := gatewayapiv1.Hostname("*.example.com") gateway := &gatewayapiv1.Gateway{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "cool-namespace", - Name: "cool-gateway", - }, Spec: gatewayapiv1.GatewaySpec{Listeners: []gatewayapiv1.Listener{ { Hostname: &hostname, }, }}, } + httpRoute := &gatewayapiv1.HTTPRoute{ + Spec: gatewayapiv1.HTTPRouteSpec{ + Hostnames: []gatewayapiv1.Hostname{hostname}, + }, + } + policy1 := FakePolicy{Hosts: []string{"this.example.com", "*.example.com"}} policy2 := FakePolicy{Hosts: []string{"*.z.com"}} @@ -1322,18 +945,27 @@ func TestValidateHierarchicalRules(t *testing.T) { t.Fatal(err) } - expectedError := fmt.Errorf( - "rule host (%s) does not follow any hierarchical constraints, "+ - "for the %T to be validated, it must match with at least one of the target network hostnames %+q", - "*.z.com", - &policy2, - []string{"*.example.com"}, - ) - - if err := ValidateHierarchicalRules(&policy2, gateway); err.Error() != expectedError.Error() { - t.Fatal("the error message does not match the expected error one", expectedError.Error(), err.Error()) - } + t.Run("gateway - contains host", func(subT *testing.T) { + assert.NilError(subT, ValidateHierarchicalRules(&policy1, gateway)) + }) + t.Run("gateway error - host has no match", func(subT *testing.T) { + expectedError := fmt.Sprintf("rule host (%s) does not follow any hierarchical constraints, "+ + "for the %T to be validated, it must match with at least one of the target network hostnames %+q", + "*.z.com", + &policy2, + []string{"*.example.com"}, + ) + assert.Error(subT, ValidateHierarchicalRules(&policy2, gateway), expectedError) + }) + + t.Run("gateway - no hosts", func(subT *testing.T) { + assert.NilError(subT, ValidateHierarchicalRules(&policy1, &gatewayapiv1.Gateway{})) + }) + + t.Run("httpRoute - contains host ", func(subT *testing.T) { + assert.NilError(subT, ValidateHierarchicalRules(&policy1, httpRoute)) + }) } func TestIsHTTPRouteAccepted(t *testing.T) { diff --git a/pkg/library/kuadrant/kuadrant.go b/pkg/library/kuadrant/kuadrant.go new file mode 100644 index 000000000..b01f71d94 --- /dev/null +++ b/pkg/library/kuadrant/kuadrant.go @@ -0,0 +1,28 @@ +package kuadrant + +import ( + "sigs.k8s.io/controller-runtime/pkg/client" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +const ( + KuadrantNamespaceLabel = "kuadrant.io/namespace" +) + +type Policy interface { + client.Object + GetTargetRef() gatewayapiv1alpha2.PolicyTargetReference + GetWrappedNamespace() gatewayapiv1.Namespace + GetRulesHostnames() []string + Kind() string +} + +type PolicyList interface { + GetItems() []Policy +} + +func IsKuadrantManaged(obj client.Object) bool { + _, isSet := obj.GetAnnotations()[KuadrantNamespaceLabel] + return isSet +} diff --git a/pkg/library/kuadrant/referrer.go b/pkg/library/kuadrant/referrer.go new file mode 100644 index 000000000..5fce0d01c --- /dev/null +++ b/pkg/library/kuadrant/referrer.go @@ -0,0 +1,35 @@ +package kuadrant + +import ( + "encoding/json" + + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" +) + +type Referrer interface { + // Kind returns the kind of the referrer object, typically a Kuadrant Policy kind. + Kind() string + // BackReferenceAnnotationName returns the name of the annotation in a target reference object that contains the back references to the referrer objects. + BackReferenceAnnotationName() string + // DirectReferenceAnnotationName return the name of the annotation for direct reference + DirectReferenceAnnotationName() string +} + +// BackReferencesFromObject returns the names of the policies listed in the annotations of a target ref object. +func BackReferencesFromObject(obj client.Object, referrer Referrer) []client.ObjectKey { + backRefs, found := utils.ReadAnnotationsFromObject(obj)[referrer.BackReferenceAnnotationName()] + if !found { + return make([]client.ObjectKey, 0) + } + + var refs []client.ObjectKey + + err := json.Unmarshal([]byte(backRefs), &refs) + if err != nil { + return make([]client.ObjectKey, 0) + } + + return refs +} diff --git a/pkg/library/kuadrant/referrer_test.go b/pkg/library/kuadrant/referrer_test.go new file mode 100644 index 000000000..3fd4d7d6e --- /dev/null +++ b/pkg/library/kuadrant/referrer_test.go @@ -0,0 +1,36 @@ +package kuadrant + +import ( + "slices" + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" +) + +func TestBackReferencesFromObject(t *testing.T) { + obj := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "gw-ns", + Name: "gw-1", + Annotations: map[string]string{"kuadrant.io/testpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}, + }, + Spec: corev1.ServiceSpec{}, + } + + policyKind := &PolicyKindStub{} + + refs := utils.Map(BackReferencesFromObject(obj, policyKind), func(ref client.ObjectKey) string { return ref.String() }) + if !slices.Contains(refs, "app-ns/policy-1") { + t.Error("GatewayWrapper.PolicyRefs() should contain app-ns/policy-1") + } + if !slices.Contains(refs, "app-ns/policy-2") { + t.Error("GatewayWrapper.PolicyRefs() should contain app-ns/policy-2") + } + if len(refs) != 2 { + t.Fail() + } +} diff --git a/pkg/common/test_utils.go b/pkg/library/kuadrant/test_utils.go similarity index 63% rename from pkg/common/test_utils.go rename to pkg/library/kuadrant/test_utils.go index 8195b085d..ea7c419f2 100644 --- a/pkg/common/test_utils.go +++ b/pkg/library/kuadrant/test_utils.go @@ -1,4 +1,4 @@ -package common +package kuadrant import ( "sigs.k8s.io/controller-runtime/pkg/client" @@ -6,6 +6,22 @@ import ( gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" ) +var _ Referrer = &PolicyKindStub{} + +type PolicyKindStub struct{} + +func (tpk *PolicyKindStub) Kind() string { + return "TestPolicy" +} + +func (tpk *PolicyKindStub) BackReferenceAnnotationName() string { + return "kuadrant.io/testpolicies" +} + +func (tpk *PolicyKindStub) DirectReferenceAnnotationName() string { + return "kuadrant.io/testpolicy" +} + type FakePolicy struct { client.Object Hosts []string diff --git a/pkg/library/mappers/event_mapper.go b/pkg/library/mappers/event_mapper.go new file mode 100644 index 000000000..0fc8a2a9f --- /dev/null +++ b/pkg/library/mappers/event_mapper.go @@ -0,0 +1,56 @@ +package mappers + +import ( + "github.com/go-logr/logr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" +) + +type EventMapper interface { + MapToPolicy(client.Object, kuadrant.Referrer) []reconcile.Request +} + +// options + +// TODO(@guicassolato): unit test +func WithLogger(logger logr.Logger) MapperOption { + return newFuncMapperOption(func(o *MapperOptions) { + o.Logger = logger + }) +} + +type MapperOption interface { + apply(*MapperOptions) +} + +type MapperOptions struct { + Logger logr.Logger +} + +var defaultMapperOptions = MapperOptions{ + Logger: logr.Discard(), +} + +func newFuncMapperOption(f func(*MapperOptions)) *funcMapperOption { + return &funcMapperOption{ + f: f, + } +} + +type funcMapperOption struct { + f func(*MapperOptions) +} + +func (fmo *funcMapperOption) apply(opts *MapperOptions) { + fmo.f(opts) +} + +func Apply(opt ...MapperOption) MapperOptions { + opts := defaultMapperOptions + for _, o := range opt { + o.apply(&opts) + } + return opts +} diff --git a/pkg/library/mappers/gateway.go b/pkg/library/mappers/gateway.go new file mode 100644 index 000000000..431d4dbfe --- /dev/null +++ b/pkg/library/mappers/gateway.go @@ -0,0 +1,44 @@ +package mappers + +import ( + "fmt" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" +) + +func NewGatewayEventMapper(o ...MapperOption) EventMapper { + return &gatewayEventMapper{opts: Apply(o...)} +} + +var _ EventMapper = &gatewayEventMapper{} + +type gatewayEventMapper struct { + opts MapperOptions +} + +func (m *gatewayEventMapper) MapToPolicy(obj client.Object, policyKind kuadrant.Referrer) []reconcile.Request { + logger := m.opts.Logger.WithValues("gateway", client.ObjectKeyFromObject(obj)) + + gateway, ok := obj.(*gatewayapiv1.Gateway) + if !ok { + logger.Info("cannot map gateway related event to kuadrant policy", "error", fmt.Sprintf("%T is not a *gatewayapiv1beta1.Gateway", obj)) + return []reconcile.Request{} + } + + requests := make([]reconcile.Request, 0) + + for _, policyKey := range kuadrant.BackReferencesFromObject(gateway, policyKind) { + logger.V(1).Info("kuadrant policy possibly affected by the gateway related event found", policyKind.Kind(), policyKey) + requests = append(requests, reconcile.Request{NamespacedName: policyKey}) + } + + if len(requests) == 0 { + logger.V(1).Info("no kuadrant policy possibly affected by the gateway related event") + } + + return requests +} diff --git a/pkg/library/mappers/gateway_test.go b/pkg/library/mappers/gateway_test.go new file mode 100644 index 000000000..3b4477971 --- /dev/null +++ b/pkg/library/mappers/gateway_test.go @@ -0,0 +1,35 @@ +package mappers + +import ( + "testing" + + "gotest.tools/assert" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/log" +) + +func TestNewGatewayEventMapper(t *testing.T) { + em := NewGatewayEventMapper(WithLogger(log.NewLogger())) + + t.Run("not gateway related event", func(subT *testing.T) { + requests := em.MapToPolicy(&gatewayapiv1.HTTPRoute{}, &kuadrant.PolicyKindStub{}) + assert.DeepEqual(subT, []reconcile.Request{}, requests) + }) + + t.Run("gateway related event - no requests", func(subT *testing.T) { + requests := em.MapToPolicy(&gatewayapiv1.Gateway{}, &kuadrant.PolicyKindStub{}) + assert.DeepEqual(subT, []reconcile.Request{}, requests) + }) + + t.Run("gateway related event - requests", func(subT *testing.T) { + gateway := &gatewayapiv1.Gateway{} + gateway.SetAnnotations(map[string]string{"kuadrant.io/testpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}) + requests := em.MapToPolicy(gateway, &kuadrant.PolicyKindStub{}) + expected := []reconcile.Request{{NamespacedName: types.NamespacedName{Namespace: "app-ns", Name: "policy-1"}}, {NamespacedName: types.NamespacedName{Namespace: "app-ns", Name: "policy-2"}}} + assert.DeepEqual(subT, expected, requests) + }) +} diff --git a/pkg/library/mappers/httproute.go b/pkg/library/mappers/httproute.go new file mode 100644 index 000000000..721ae9c51 --- /dev/null +++ b/pkg/library/mappers/httproute.go @@ -0,0 +1,44 @@ +package mappers + +import ( + "fmt" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" +) + +func NewHTTPRouteEventMapper(o ...MapperOption) EventMapper { + return &httpRouteEventMapper{opts: Apply(o...)} +} + +var _ EventMapper = &httpRouteEventMapper{} + +type httpRouteEventMapper struct { + opts MapperOptions +} + +func (m *httpRouteEventMapper) MapToPolicy(obj client.Object, policyKind kuadrant.Referrer) []reconcile.Request { + logger := m.opts.Logger.WithValues("httproute", client.ObjectKeyFromObject(obj)) + + httpRoute, ok := obj.(*gatewayapiv1.HTTPRoute) + if !ok { + logger.Info("cannot map httproute event to kuadrant policy", "error", fmt.Sprintf("%T is not a *gatewayapiv1beta1.HTTPRoute", obj)) + return []reconcile.Request{} + } + + requests := make([]reconcile.Request, 0) + + for _, policyKey := range kuadrant.BackReferencesFromObject(httpRoute, policyKind) { + logger.V(1).Info("kuadrant policy possibly affected by the httproute related event found", policyKind.Kind(), policyKey) + requests = append(requests, reconcile.Request{NamespacedName: policyKey}) + } + + if len(requests) == 0 { + logger.V(1).Info("no kuadrant policy possibly affected by the httproute related event") + } + + return requests +} diff --git a/pkg/library/mappers/httproute_test.go b/pkg/library/mappers/httproute_test.go new file mode 100644 index 000000000..ea043d1a7 --- /dev/null +++ b/pkg/library/mappers/httproute_test.go @@ -0,0 +1,35 @@ +package mappers + +import ( + "testing" + + "gotest.tools/assert" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/log" +) + +func TestNewHTTPRouteEventMapper(t *testing.T) { + em := NewHTTPRouteEventMapper(WithLogger(log.NewLogger())) + + t.Run("not http route related event", func(subT *testing.T) { + requests := em.MapToPolicy(&gatewayapiv1.Gateway{}, &kuadrant.PolicyKindStub{}) + assert.DeepEqual(subT, []reconcile.Request{}, requests) + }) + + t.Run("http route related event - no requests", func(subT *testing.T) { + requests := em.MapToPolicy(&gatewayapiv1.HTTPRoute{}, &kuadrant.PolicyKindStub{}) + assert.DeepEqual(subT, []reconcile.Request{}, requests) + }) + + t.Run("http related event - requests", func(subT *testing.T) { + httpRoute := &gatewayapiv1.HTTPRoute{} + httpRoute.SetAnnotations(map[string]string{"kuadrant.io/testpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}) + requests := em.MapToPolicy(httpRoute, &kuadrant.PolicyKindStub{}) + expected := []reconcile.Request{{NamespacedName: types.NamespacedName{Namespace: "app-ns", Name: "policy-1"}}, {NamespacedName: types.NamespacedName{Namespace: "app-ns", Name: "policy-2"}}} + assert.DeepEqual(subT, expected, requests) + }) +} diff --git a/pkg/library/reconcilers/fetcher.go b/pkg/library/reconcilers/fetcher.go new file mode 100644 index 000000000..64a4a2a7c --- /dev/null +++ b/pkg/library/reconcilers/fetcher.go @@ -0,0 +1,95 @@ +package reconcilers + +import ( + "context" + "fmt" + "reflect" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/api/meta" + "sigs.k8s.io/controller-runtime/pkg/client" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +// FetchTargetRefObject fetches the target reference object and checks the status is valid +func FetchTargetRefObject(ctx context.Context, k8sClient client.Reader, targetRef gatewayapiv1alpha2.PolicyTargetReference, defaultNs string) (client.Object, error) { + ns := defaultNs + if targetRef.Namespace != nil { + ns = string(*targetRef.Namespace) + } + + objKey := client.ObjectKey{Name: string(targetRef.Name), Namespace: ns} + + switch targetRef.Kind { + case "Gateway": + return fetchGateway(ctx, k8sClient, objKey) + case "HTTPRoute": + return fetchHTTPRoute(ctx, k8sClient, objKey) + default: + return nil, fmt.Errorf("FetchValidTargetRef: targetRef (%v) to unknown network resource", targetRef) + } +} + +func fetchGateway(ctx context.Context, k8sClient client.Reader, key client.ObjectKey) (*gatewayapiv1.Gateway, error) { + logger, _ := logr.FromContext(ctx) + + gw := &gatewayapiv1.Gateway{} + err := k8sClient.Get(ctx, key, gw) + logger.V(1).Info("fetch Gateway policy targetRef", "gateway", key, "err", err) + if err != nil { + return nil, err + } + + if meta.IsStatusConditionFalse(gw.Status.Conditions, string(gatewayapiv1.GatewayConditionProgrammed)) { + return nil, fmt.Errorf("gateway (%v) not ready", key) + } + + return gw, nil +} + +func fetchHTTPRoute(ctx context.Context, k8sClient client.Reader, key client.ObjectKey) (*gatewayapiv1.HTTPRoute, error) { + logger, _ := logr.FromContext(ctx) + + httpRoute := &gatewayapiv1.HTTPRoute{} + err := k8sClient.Get(ctx, key, httpRoute) + logger.V(1).Info("fetch HTTPRoute policy targetRef", "httpRoute", key, "err", err) + if err != nil { + return nil, err + } + + if !httpRouteAccepted(httpRoute) { + return nil, fmt.Errorf("httproute (%v) not accepted", key) + } + + return httpRoute, nil +} + +func httpRouteAccepted(httpRoute *gatewayapiv1.HTTPRoute) bool { + if httpRoute == nil { + return false + } + + if len(httpRoute.Spec.CommonRouteSpec.ParentRefs) == 0 { + return false + } + + // Check HTTProute parents (gateways) in the status object + // if any of the current parent gateways reports not "Admitted", return false + for _, parentRef := range httpRoute.Spec.CommonRouteSpec.ParentRefs { + routeParentStatus := func(pRef gatewayapiv1.ParentReference) *gatewayapiv1.RouteParentStatus { + for idx := range httpRoute.Status.RouteStatus.Parents { + if reflect.DeepEqual(pRef, httpRoute.Status.RouteStatus.Parents[idx].ParentRef) { + return &httpRoute.Status.RouteStatus.Parents[idx] + } + } + return nil + }(parentRef) + + if routeParentStatus == nil || meta.IsStatusConditionFalse(routeParentStatus.Conditions, string(gatewayapiv1.RouteReasonAccepted)) { + return false + } + } + + return true +} diff --git a/pkg/library/reconcilers/fetcher_test.go b/pkg/library/reconcilers/fetcher_test.go new file mode 100644 index 000000000..4a6be5d2e --- /dev/null +++ b/pkg/library/reconcilers/fetcher_test.go @@ -0,0 +1,485 @@ +package reconcilers + +import ( + "context" + "fmt" + "reflect" + "testing" + + "github.com/go-logr/logr" + "github.com/google/go-cmp/cmp" + "gotest.tools/assert" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/log" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +func TestFetchTargetRefObject(t *testing.T) { + var ( + namespace = "operator-unittest" + routeName = "my-route" + gatewayName = "my-gw" + ) + baseCtx := context.Background() + ctx := logr.NewContext(baseCtx, log.Log) + + s := scheme.Scheme + err := appsv1.AddToScheme(s) + if err != nil { + t.Fatal(err) + } + err = gatewayapiv1.AddToScheme(s) + if err != nil { + t.Fatal(err) + } + + routeTargetRef := gatewayapiv1alpha2.PolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "HTTPRoute", + Name: gatewayapiv1.ObjectName(routeName), + } + + gatewayTargetRef := gatewayapiv1alpha2.PolicyTargetReference{ + Group: gatewayapiv1.GroupName, + Kind: "Gateway", + Name: gatewayapiv1.ObjectName(gatewayName), + } + + routeFactory := func(status metav1.ConditionStatus) *gatewayapiv1.HTTPRoute { + return &gatewayapiv1.HTTPRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "gateway.networking.k8s.io/v1", + Kind: "HTTPRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: routeName, + Namespace: namespace, + }, + Spec: gatewayapiv1.HTTPRouteSpec{ + CommonRouteSpec: gatewayapiv1.CommonRouteSpec{ + ParentRefs: []gatewayapiv1.ParentReference{ + { + Name: "gwName", + }, + }, + }, + }, + Status: gatewayapiv1.HTTPRouteStatus{ + RouteStatus: gatewayapiv1.RouteStatus{ + Parents: []gatewayapiv1.RouteParentStatus{ + { + ParentRef: gatewayapiv1.ParentReference{ + Name: "gwName", + }, + Conditions: []metav1.Condition{ + { + Type: "Accepted", + Status: status, + }, + }, + }, + }, + }, + }, + } + } + + gatewayFactory := func(status metav1.ConditionStatus) *gatewayapiv1.Gateway { + return &gatewayapiv1.Gateway{ + TypeMeta: metav1.TypeMeta{ + Kind: "Gateway", + APIVersion: "gateway.networking.k8s.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: gatewayName, + Namespace: namespace, + }, + Status: gatewayapiv1.GatewayStatus{ + Conditions: []metav1.Condition{ + { + Type: string(gatewayapiv1.GatewayConditionProgrammed), + Status: status, + }, + }, + }, + } + } + + clientFactory := func(objs ...runtime.Object) client.WithWatch { + return fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() + } + + assertion := func(res, existing client.Object) { + switch obj := res.(type) { + case *gatewayapiv1.HTTPRoute: + if !reflect.DeepEqual(obj, existing) { + t.Fatal("res spec not as expected", cmp.Diff(obj, existing)) + } + case *gatewayapiv1.Gateway: + if !reflect.DeepEqual(obj, existing) { + t.Fatal("res spec not as expected", cmp.Diff(obj, existing)) + } + default: + t.Fatal("res type not known") + } + } + + t.Run("fetch http route", func(subT *testing.T) { + existingRoute := routeFactory(metav1.ConditionTrue) + clientAPIReader := clientFactory(existingRoute) + res, err := FetchTargetRefObject(ctx, clientAPIReader, routeTargetRef, namespace) + assert.NilError(subT, err) + assert.Equal(subT, res == nil, false) + assertion(res, existingRoute) + }) + + t.Run("fetch http route - not accepted", func(subT *testing.T) { + existingRoute := routeFactory(metav1.ConditionFalse) + clientAPIReader := clientFactory(existingRoute) + res, err := FetchTargetRefObject(ctx, clientAPIReader, routeTargetRef, namespace) + assert.Error(subT, err, fmt.Sprintf("httproute (%s/%s) not accepted", namespace, routeName)) + assert.DeepEqual(subT, res, (*gatewayapiv1.HTTPRoute)(nil)) + }) + + t.Run("fetch gateway", func(subT *testing.T) { + existingGateway := gatewayFactory(metav1.ConditionTrue) + clientAPIReader := clientFactory(existingGateway) + res, err := FetchTargetRefObject(ctx, clientAPIReader, gatewayTargetRef, namespace) + assert.NilError(subT, err) + assert.Equal(subT, res == nil, false) + assertion(res, existingGateway) + }) + + t.Run("fetch gateway - not ready", func(subT *testing.T) { + existingGateway := gatewayFactory(metav1.ConditionFalse) + clientAPIReader := clientFactory(existingGateway) + res, err := FetchTargetRefObject(ctx, clientAPIReader, gatewayTargetRef, namespace) + assert.Error(subT, err, fmt.Sprintf("gateway (%s/%s) not ready", namespace, gatewayName)) + assert.DeepEqual(subT, res, (*gatewayapiv1.Gateway)(nil)) + }) + + t.Run("unknown network resource", func(subT *testing.T) { + ns := gatewayapiv1.Namespace(namespace) + targetRef := gatewayapiv1alpha2.PolicyTargetReference{Kind: "Service", Name: "my-sv", Namespace: &ns} + clientAPIReader := clientFactory() + res, err := FetchTargetRefObject(ctx, clientAPIReader, targetRef, namespace) + assert.Error(subT, err, fmt.Sprintf("FetchValidTargetRef: targetRef (%v) to unknown network resource", targetRef)) + assert.DeepEqual(subT, res, nil) + }) +} + +func TestFetchGateway(t *testing.T) { + var ( + namespace = "operator-unittest" + gwName = "my-gateway" + ) + baseCtx := context.Background() + ctx := logr.NewContext(baseCtx, log.Log) + + s := scheme.Scheme + err := appsv1.AddToScheme(s) + if err != nil { + t.Fatal(err) + } + err = gatewayapiv1.AddToScheme(s) + if err != nil { + t.Fatal(err) + } + + existingGateway := &gatewayapiv1.Gateway{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "gateway.networking.k8s.io/v1", + Kind: "Gateway", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: gwName, + Namespace: namespace, + }, + Spec: gatewayapiv1.GatewaySpec{ + GatewayClassName: "istio", + }, + Status: gatewayapiv1.GatewayStatus{ + Conditions: []metav1.Condition{ + { + Type: "Ready", + Status: metav1.ConditionTrue, + }, + }, + }, + } + + // Objects to track in the fake client. + objs := []runtime.Object{existingGateway} + + // Create a fake client to mock API calls. + clientAPIReader := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() + + key := client.ObjectKey{Name: gwName, Namespace: namespace} + + res, err := fetchGateway(ctx, clientAPIReader, key) + if err != nil { + t.Fatal(err) + } + + if res == nil { + t.Fatal("res is nil") + } + + if !reflect.DeepEqual(res.Spec, existingGateway.Spec) { + t.Fatal("res spec not as expected") + } +} + +func TestFetchHTTPRoute(t *testing.T) { + var ( + namespace = "operator-unittest" + routeName = "my-route" + ) + baseCtx := context.Background() + ctx := logr.NewContext(baseCtx, log.Log) + + s := scheme.Scheme + err := appsv1.AddToScheme(s) + if err != nil { + t.Fatal(err) + } + err = gatewayapiv1.AddToScheme(s) + if err != nil { + t.Fatal(err) + } + + existingRoute := &gatewayapiv1.HTTPRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "gateway.networking.k8s.io/v1", + Kind: "HTTPRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: routeName, + Namespace: namespace, + }, + Spec: gatewayapiv1.HTTPRouteSpec{ + CommonRouteSpec: gatewayapiv1.CommonRouteSpec{ + ParentRefs: []gatewayapiv1.ParentReference{ + { + Name: "gwName", + }, + }, + }, + }, + Status: gatewayapiv1.HTTPRouteStatus{ + RouteStatus: gatewayapiv1.RouteStatus{ + Parents: []gatewayapiv1.RouteParentStatus{ + { + ParentRef: gatewayapiv1.ParentReference{ + Name: "gwName", + }, + Conditions: []metav1.Condition{ + { + Type: "Accepted", + Status: metav1.ConditionTrue, + }, + }, + }, + }, + }, + }, + } + + // Objects to track in the fake client. + objs := []runtime.Object{existingRoute} + + // Create a fake client to mock API calls. + clientAPIReader := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() + + key := client.ObjectKey{Name: routeName, Namespace: namespace} + + res, err := fetchHTTPRoute(ctx, clientAPIReader, key) + if err != nil { + t.Fatal(err) + } + + if res == nil { + t.Fatal("res is nil") + } + + if !reflect.DeepEqual(res.Spec, existingRoute.Spec) { + t.Fatal("res spec not as expected") + } +} + +func TestHTTPRouteAccepted(t *testing.T) { + testCases := []struct { + name string + route *gatewayapiv1.HTTPRoute + expected bool + }{ + { + "nil", + nil, + false, + }, + { + "empty parent refs", + &gatewayapiv1.HTTPRoute{ + Spec: gatewayapiv1.HTTPRouteSpec{}, + }, + false, + }, + { + "single parent accepted", + &gatewayapiv1.HTTPRoute{ + Spec: gatewayapiv1.HTTPRouteSpec{ + CommonRouteSpec: gatewayapiv1.CommonRouteSpec{ + ParentRefs: []gatewayapiv1.ParentReference{ + { + Name: "a", + }, + }, + }, + }, + Status: gatewayapiv1.HTTPRouteStatus{ + RouteStatus: gatewayapiv1.RouteStatus{ + Parents: []gatewayapiv1.RouteParentStatus{ + { + ParentRef: gatewayapiv1.ParentReference{ + Name: "a", + }, + Conditions: []metav1.Condition{ + { + Type: "Accepted", + Status: metav1.ConditionTrue, + }, + }, + }, + }, + }, + }, + }, + true, + }, + { + "single parent not accepted", + &gatewayapiv1.HTTPRoute{ + Spec: gatewayapiv1.HTTPRouteSpec{ + CommonRouteSpec: gatewayapiv1.CommonRouteSpec{ + ParentRefs: []gatewayapiv1.ParentReference{ + { + Name: "a", + }, + }, + }, + }, + Status: gatewayapiv1.HTTPRouteStatus{ + RouteStatus: gatewayapiv1.RouteStatus{ + Parents: []gatewayapiv1.RouteParentStatus{ + { + ParentRef: gatewayapiv1.ParentReference{ + Name: "a", + }, + Conditions: []metav1.Condition{ + { + Type: "Accepted", + Status: metav1.ConditionFalse, + }, + }, + }, + }, + }, + }, + }, + false, + }, + { + "wrong parent is accepted", + &gatewayapiv1.HTTPRoute{ + Spec: gatewayapiv1.HTTPRouteSpec{ + CommonRouteSpec: gatewayapiv1.CommonRouteSpec{ + ParentRefs: []gatewayapiv1.ParentReference{ + { + Name: "a", + }, + }, + }, + }, + Status: gatewayapiv1.HTTPRouteStatus{ + RouteStatus: gatewayapiv1.RouteStatus{ + Parents: []gatewayapiv1.RouteParentStatus{ + { + ParentRef: gatewayapiv1.ParentReference{ + Name: "b", + }, + Conditions: []metav1.Condition{ + { + Type: "Accepted", + Status: metav1.ConditionTrue, + }, + }, + }, + }, + }, + }, + }, + false, + }, + { + "multiple parents only one is accepted", + &gatewayapiv1.HTTPRoute{ + Spec: gatewayapiv1.HTTPRouteSpec{ + CommonRouteSpec: gatewayapiv1.CommonRouteSpec{ + ParentRefs: []gatewayapiv1.ParentReference{ + { + Name: "a", + }, + { + Name: "b", + }, + }, + }, + }, + Status: gatewayapiv1.HTTPRouteStatus{ + RouteStatus: gatewayapiv1.RouteStatus{ + Parents: []gatewayapiv1.RouteParentStatus{ + { + ParentRef: gatewayapiv1.ParentReference{ + Name: "a", + }, + Conditions: []metav1.Condition{ + { + Type: "Accepted", + Status: metav1.ConditionTrue, + }, + }, + }, + { + ParentRef: gatewayapiv1.ParentReference{ + Name: "b", + }, + Conditions: []metav1.Condition{ + { + Type: "Accepted", + Status: metav1.ConditionFalse, + }, + }, + }, + }, + }, + }, + }, + false, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(subT *testing.T) { + res := httpRouteAccepted(tc.route) + if res != tc.expected { + subT.Errorf("result (%t) does not match expected (%t)", res, tc.expected) + } + }) + } +} diff --git a/pkg/library/reconcilers/gateway_diffs.go b/pkg/library/reconcilers/gateway_diffs.go new file mode 100644 index 000000000..05ee74ee6 --- /dev/null +++ b/pkg/library/reconcilers/gateway_diffs.go @@ -0,0 +1,123 @@ +package reconcilers + +import ( + "context" + "fmt" + "slices" + + "github.com/go-logr/logr" + "sigs.k8s.io/controller-runtime/pkg/client" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" +) + +type GatewayDiffs struct { + GatewaysMissingPolicyRef []kuadrant.GatewayWrapper + GatewaysWithValidPolicyRef []kuadrant.GatewayWrapper + GatewaysWithInvalidPolicyRef []kuadrant.GatewayWrapper +} + +// ComputeGatewayDiffs computes all the differences to reconcile regarding the gateways whose behaviors should/should not be extended by the policy. +// These include gateways directly referenced by the policy and gateways indirectly referenced through the policy's target network objects. +// * list of gateways to which the policy applies for the first time +// * list of gateways to which the policy no longer applies +// * list of gateways to which the policy still applies +// TODO(@guicassolato): unit test +func ComputeGatewayDiffs(ctx context.Context, k8sClient client.Reader, policy, targetNetworkObject client.Object) (*GatewayDiffs, error) { + logger, _ := logr.FromContext(ctx) + + var gwKeys []client.ObjectKey + if policy.GetDeletionTimestamp() == nil { + gwKeys = targetedGatewayKeys(targetNetworkObject) + } + + // TODO(rahulanand16nov): maybe think about optimizing it with a label later + allGwList := &gatewayapiv1.GatewayList{} + err := k8sClient.List(ctx, allGwList) + if err != nil { + return nil, err + } + + policyKind, ok := policy.(kuadrant.Referrer) + if !ok { + return nil, fmt.Errorf("policy %s is not a referrer", policy.GetObjectKind().GroupVersionKind()) + } + + gwDiff := &GatewayDiffs{ + GatewaysMissingPolicyRef: gatewaysMissingPolicyRef(allGwList, client.ObjectKeyFromObject(policy), gwKeys, policyKind), + GatewaysWithValidPolicyRef: gatewaysWithValidPolicyRef(allGwList, client.ObjectKeyFromObject(policy), gwKeys, policyKind), + GatewaysWithInvalidPolicyRef: gatewaysWithInvalidPolicyRef(allGwList, client.ObjectKeyFromObject(policy), gwKeys, policyKind), + } + + logger.V(1).Info("ComputeGatewayDiffs", + "missing-policy-ref", len(gwDiff.GatewaysMissingPolicyRef), + "valid-policy-ref", len(gwDiff.GatewaysWithValidPolicyRef), + "invalid-policy-ref", len(gwDiff.GatewaysWithInvalidPolicyRef), + ) + + return gwDiff, nil +} + +// gatewaysMissingPolicyRef returns gateways referenced by the policy but that miss the reference to it the annotations +func gatewaysMissingPolicyRef(gwList *gatewayapiv1.GatewayList, policyKey client.ObjectKey, policyGwKeys []client.ObjectKey, policyKind kuadrant.Referrer) []kuadrant.GatewayWrapper { + gateways := make([]kuadrant.GatewayWrapper, 0) + for i := range gwList.Items { + gateway := gwList.Items[i] + gw := kuadrant.GatewayWrapper{Gateway: &gateway, Referrer: policyKind} + if slices.Contains(policyGwKeys, client.ObjectKeyFromObject(&gateway)) && !gw.ContainsPolicy(policyKey) { + gateways = append(gateways, gw) + } + } + return gateways +} + +// gatewaysWithValidPolicyRef returns gateways referenced by the policy that also have the reference in the annotations +func gatewaysWithValidPolicyRef(gwList *gatewayapiv1.GatewayList, policyKey client.ObjectKey, policyGwKeys []client.ObjectKey, policyKind kuadrant.Referrer) []kuadrant.GatewayWrapper { + gateways := make([]kuadrant.GatewayWrapper, 0) + for i := range gwList.Items { + gateway := gwList.Items[i] + gw := kuadrant.GatewayWrapper{Gateway: &gateway, Referrer: policyKind} + if slices.Contains(policyGwKeys, client.ObjectKeyFromObject(&gateway)) && gw.ContainsPolicy(policyKey) { + gateways = append(gateways, gw) + } + } + return gateways +} + +// gatewaysWithInvalidPolicyRef returns gateways not referenced by the policy that still have the reference in the annotations +func gatewaysWithInvalidPolicyRef(gwList *gatewayapiv1.GatewayList, policyKey client.ObjectKey, policyGwKeys []client.ObjectKey, policyKind kuadrant.Referrer) []kuadrant.GatewayWrapper { + gateways := make([]kuadrant.GatewayWrapper, 0) + for i := range gwList.Items { + gateway := gwList.Items[i] + gw := kuadrant.GatewayWrapper{Gateway: &gateway, Referrer: policyKind} + if !slices.Contains(policyGwKeys, client.ObjectKeyFromObject(&gateway)) && gw.ContainsPolicy(policyKey) { + gateways = append(gateways, gw) + } + } + return gateways +} + +// targetedGatewayKeys returns the list of gateways in the hierarchy of a target network object +func targetedGatewayKeys(targetNetworkObject client.Object) []client.ObjectKey { + switch obj := targetNetworkObject.(type) { + case *gatewayapiv1.HTTPRoute: + gwKeys := make([]client.ObjectKey, 0) + for _, parentRef := range obj.Spec.CommonRouteSpec.ParentRefs { + gwKey := client.ObjectKey{Name: string(parentRef.Name), Namespace: obj.Namespace} + if parentRef.Namespace != nil { + gwKey.Namespace = string(*parentRef.Namespace) + } + gwKeys = append(gwKeys, gwKey) + } + return gwKeys + + case *gatewayapiv1.Gateway: + return []client.ObjectKey{client.ObjectKeyFromObject(targetNetworkObject)} + + // If the targetNetworkObject is nil, we don't fail; instead, we return an empty slice of gateway keys. + // This is for supporting a smooth cleanup in cases where the network object has been deleted already + default: + return []client.ObjectKey{} + } +} diff --git a/pkg/library/reconcilers/gateway_diffs_test.go b/pkg/library/reconcilers/gateway_diffs_test.go new file mode 100644 index 000000000..942415d7d --- /dev/null +++ b/pkg/library/reconcilers/gateway_diffs_test.go @@ -0,0 +1,306 @@ +package reconcilers + +import ( + "slices" + "testing" + + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" +) + +func TestGatewaysMissingPolicyRef(t *testing.T) { + gwList := &gatewayapiv1.GatewayList{ + Items: []gatewayapiv1.Gateway{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "gw-ns", + Name: "gw-1", + Annotations: map[string]string{"kuadrant.io/testpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "gw-ns", + Name: "gw-2", + Annotations: map[string]string{"kuadrant.io/testpolicies": `[{"Namespace":"app-ns","Name":"policy-1"}]`}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "gw-ns", + Name: "gw-3", + }, + }, + }, + } + + var gws []string + policyKind := &kuadrant.PolicyKindStub{} + gwName := func(gw kuadrant.GatewayWrapper) string { return gw.Gateway.Name } + + gws = utils.Map(gatewaysMissingPolicyRef(gwList, client.ObjectKey{Namespace: "app-ns", Name: "policy-1"}, []client.ObjectKey{ + {Namespace: "gw-ns", Name: "gw-2"}, + {Namespace: "gw-ns", Name: "gw-3"}, + }, policyKind), gwName) + + if slices.Contains(gws, "gw-1") { + t.Error("gateway expected not to be listed as missing policy ref") + } + if slices.Contains(gws, "gw-2") { + t.Error("gateway expected not to be listed as missing policy ref") + } + if !slices.Contains(gws, "gw-3") { + t.Error("gateway expected to be listed as missing policy ref") + } + + gws = utils.Map(gatewaysMissingPolicyRef(gwList, client.ObjectKey{Namespace: "app-ns", Name: "policy-2"}, []client.ObjectKey{ + {Namespace: "gw-ns", Name: "gw-1"}, + }, policyKind), gwName) + + if slices.Contains(gws, "gw-1") { + t.Error("gateway expected not to be listed as missing policy ref") + } + if slices.Contains(gws, "gw-2") { + t.Error("gateway expected not to be listed as missing policy ref") + } + if slices.Contains(gws, "gw-3") { + t.Error("gateway expected not to be listed as missing policy ref") + } + + gws = utils.Map(gatewaysMissingPolicyRef(gwList, client.ObjectKey{Namespace: "app-ns", Name: "policy-3"}, []client.ObjectKey{ + {Namespace: "gw-ns", Name: "gw-1"}, + {Namespace: "gw-ns", Name: "gw-3"}, + }, policyKind), gwName) + + if !slices.Contains(gws, "gw-1") { + t.Error("gateway expected to be listed as missing policy ref") + } + if slices.Contains(gws, "gw-2") { + t.Error("gateway expected not to be listed as missing policy ref") + } + if !slices.Contains(gws, "gw-3") { + t.Error("gateway expected to be listed as missing policy ref") + } +} + +func TestGatewaysWithValidPolicyRef(t *testing.T) { + gwList := &gatewayapiv1.GatewayList{ + Items: []gatewayapiv1.Gateway{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "gw-ns", + Name: "gw-1", + Annotations: map[string]string{"kuadrant.io/testpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "gw-ns", + Name: "gw-2", + Annotations: map[string]string{"kuadrant.io/testpolicies": `[{"Namespace":"app-ns","Name":"policy-1"}]`}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "gw-ns", + Name: "gw-3", + }, + }, + }, + } + + var gws []string + policyKind := &kuadrant.PolicyKindStub{} + gwName := func(gw kuadrant.GatewayWrapper) string { return gw.Gateway.Name } + + gws = utils.Map(gatewaysWithValidPolicyRef(gwList, client.ObjectKey{Namespace: "app-ns", Name: "policy-1"}, []client.ObjectKey{ + {Namespace: "gw-ns", Name: "gw-2"}, + {Namespace: "gw-ns", Name: "gw-3"}, + }, policyKind), gwName) + + if slices.Contains(gws, "gw-1") { + t.Error("gateway expected not to be listed as with valid policy ref") + } + if !slices.Contains(gws, "gw-2") { + t.Error("gateway expected to be listed as with valid policy ref") + } + if slices.Contains(gws, "gw-3") { + t.Error("gateway expected not to be listed as with valid policy ref") + } + + gws = utils.Map(gatewaysWithValidPolicyRef(gwList, client.ObjectKey{Namespace: "app-ns", Name: "policy-2"}, []client.ObjectKey{ + {Namespace: "gw-ns", Name: "gw-1"}, + }, policyKind), gwName) + + if !slices.Contains(gws, "gw-1") { + t.Error("gateway expected to be listed as with valid policy ref") + } + if slices.Contains(gws, "gw-2") { + t.Error("gateway expected not to be listed as with valid policy ref") + } + if slices.Contains(gws, "gw-3") { + t.Error("gateway expected not to be listed as with valid policy ref") + } + + gws = utils.Map(gatewaysWithValidPolicyRef(gwList, client.ObjectKey{Namespace: "app-ns", Name: "policy-3"}, []client.ObjectKey{ + {Namespace: "gw-ns", Name: "gw-1"}, + {Namespace: "gw-ns", Name: "gw-3"}, + }, policyKind), gwName) + + if slices.Contains(gws, "gw-1") { + t.Error("gateway expected not to be listed as with valid policy ref") + } + if slices.Contains(gws, "gw-2") { + t.Error("gateway expected not to be listed as with valid policy ref") + } + if slices.Contains(gws, "gw-3") { + t.Error("gateway expected not to be listed as with valid policy ref") + } +} + +func TestGatewaysWithInvalidPolicyRef(t *testing.T) { + gwList := &gatewayapiv1.GatewayList{ + Items: []gatewayapiv1.Gateway{ + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "gw-ns", + Name: "gw-1", + Annotations: map[string]string{"kuadrant.io/testpolicies": `[{"Namespace":"app-ns","Name":"policy-1"},{"Namespace":"app-ns","Name":"policy-2"}]`}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "gw-ns", + Name: "gw-2", + Annotations: map[string]string{"kuadrant.io/testpolicies": `[{"Namespace":"app-ns","Name":"policy-1"}]`}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Namespace: "gw-ns", + Name: "gw-3", + }, + }, + }, + } + + var gws []string + policyKind := &kuadrant.PolicyKindStub{} + gwName := func(gw kuadrant.GatewayWrapper) string { return gw.Gateway.Name } + + gws = utils.Map(gatewaysWithInvalidPolicyRef(gwList, client.ObjectKey{Namespace: "app-ns", Name: "policy-1"}, []client.ObjectKey{ + {Namespace: "gw-ns", Name: "gw-2"}, + {Namespace: "gw-ns", Name: "gw-3"}, + }, policyKind), gwName) + + if !slices.Contains(gws, "gw-1") { + t.Error("gateway expected to be listed as with invalid policy ref") + } + if slices.Contains(gws, "gw-2") { + t.Error("gateway expected not to be listed as with invalid policy ref") + } + if slices.Contains(gws, "gw-3") { + t.Error("gateway expected not to be listed as with invalid policy ref") + } + + gws = utils.Map(gatewaysWithInvalidPolicyRef(gwList, client.ObjectKey{Namespace: "app-ns", Name: "policy-2"}, []client.ObjectKey{ + {Namespace: "gw-ns", Name: "gw-1"}, + }, policyKind), gwName) + + if slices.Contains(gws, "gw-1") { + t.Error("gateway expected not to be listed as with invalid policy ref") + } + if slices.Contains(gws, "gw-2") { + t.Error("gateway expected not to be listed as with invalid policy ref") + } + if slices.Contains(gws, "gw-3") { + t.Error("gateway expected not to be listed as with invalid policy ref") + } + + gws = utils.Map(gatewaysWithInvalidPolicyRef(gwList, client.ObjectKey{Namespace: "app-ns", Name: "policy-3"}, []client.ObjectKey{ + {Namespace: "gw-ns", Name: "gw-1"}, + {Namespace: "gw-ns", Name: "gw-3"}, + }, policyKind), gwName) + + if slices.Contains(gws, "gw-1") { + t.Error("gateway expected not to be listed as with invalid policy ref") + } + if slices.Contains(gws, "gw-2") { + t.Error("gateway expected not to be listed as with invalid policy ref") + } + if slices.Contains(gws, "gw-3") { + t.Error("gateway expected not to be listed as with invalid policy ref") + } +} + +func TestTargetedGatewayKeys(t *testing.T) { + var ( + namespace = "operator-unittest" + routeName = "my-route" + ) + + s := scheme.Scheme + err := appsv1.AddToScheme(s) + if err != nil { + t.Fatal(err) + } + err = gatewayapiv1.AddToScheme(s) + if err != nil { + t.Fatal(err) + } + + httpRoute := &gatewayapiv1.HTTPRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "gateway.networking.k8s.io/v1", + Kind: "HTTPRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: routeName, + Namespace: namespace, + }, + Spec: gatewayapiv1.HTTPRouteSpec{ + CommonRouteSpec: gatewayapiv1.CommonRouteSpec{ + ParentRefs: []gatewayapiv1.ParentReference{ + { + Name: "gwName", + }, + }, + }, + }, + Status: gatewayapiv1.HTTPRouteStatus{ + RouteStatus: gatewayapiv1.RouteStatus{ + Parents: []gatewayapiv1.RouteParentStatus{ + { + ParentRef: gatewayapiv1.ParentReference{ + Name: "gwName", + }, + Conditions: []metav1.Condition{ + { + Type: "Accepted", + Status: metav1.ConditionTrue, + }, + }, + }, + }, + }, + }, + } + + keys := targetedGatewayKeys(httpRoute) + + if len(keys) != 1 { + t.Fatalf("gateway key slice length is %d and it was expected to be 1", len(keys)) + } + + expectedKey := client.ObjectKey{Name: "gwName", Namespace: namespace} + + if keys[0] != expectedKey { + t.Fatalf("gwKey value (%+v) does not match expected (%+v)", keys[0], expectedKey) + } +} diff --git a/pkg/reconcilers/targetref_reconciler.go b/pkg/library/reconcilers/target_ref_reconciler.go similarity index 57% rename from pkg/reconcilers/targetref_reconciler.go rename to pkg/library/reconcilers/target_ref_reconciler.go index 76a0dab91..e09283b4f 100644 --- a/pkg/reconcilers/targetref_reconciler.go +++ b/pkg/library/reconcilers/target_ref_reconciler.go @@ -23,76 +23,15 @@ import ( "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/api/meta" - ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/reconcile" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" ) type TargetRefReconciler struct { - *BaseReconciler -} - -// blank assignment to verify that BaseReconciler implements reconcile.Reconciler -var _ reconcile.Reconciler = &TargetRefReconciler{} - -func (r *TargetRefReconciler) Reconcile(context.Context, ctrl.Request) (ctrl.Result, error) { - return reconcile.Result{}, nil -} - -func (r *TargetRefReconciler) FetchValidGateway(ctx context.Context, key client.ObjectKey) (*gatewayapiv1.Gateway, error) { - logger, _ := logr.FromContext(ctx) - - gw := &gatewayapiv1.Gateway{} - err := r.Client().Get(ctx, key, gw) - logger.V(1).Info("FetchValidGateway", "gateway", key, "err", err) - if err != nil { - return nil, err - } - - if meta.IsStatusConditionFalse(gw.Status.Conditions, common.GatewayProgrammedConditionType) { - return nil, fmt.Errorf("FetchValidGateway: gateway (%v) not ready", key) - } - - return gw, nil -} - -func (r *TargetRefReconciler) FetchValidHTTPRoute(ctx context.Context, key client.ObjectKey) (*gatewayapiv1.HTTPRoute, error) { - logger, _ := logr.FromContext(ctx) - - httpRoute := &gatewayapiv1.HTTPRoute{} - err := r.Client().Get(ctx, key, httpRoute) - logger.V(1).Info("FetchValidHTTPRoute", "httpRoute", key, "err", err) - if err != nil { - return nil, err - } - - if !common.IsHTTPRouteAccepted(httpRoute) { - return nil, fmt.Errorf("FetchValidHTTPRoute: httproute (%v) not accepted", key) - } - - return httpRoute, nil -} - -// FetchValidTargetRef fetches the target reference object and checks the status is valid -func (r *TargetRefReconciler) FetchValidTargetRef(ctx context.Context, targetRef gatewayapiv1alpha2.PolicyTargetReference, defaultNs string) (client.Object, error) { - tmpNS := defaultNs - if targetRef.Namespace != nil { - tmpNS = string(*targetRef.Namespace) - } - - objKey := client.ObjectKey{Name: string(targetRef.Name), Namespace: tmpNS} - - if common.IsTargetRefHTTPRoute(targetRef) { - return r.FetchValidHTTPRoute(ctx, objKey) - } else if common.IsTargetRefGateway(targetRef) { - return r.FetchValidGateway(ctx, objKey) - } - - return nil, fmt.Errorf("FetchValidTargetRef: targetRef (%v) to unknown network resource", targetRef) + client.Client } // FetchAcceptedGatewayHTTPRoutes returns the list of HTTPRoutes that have been accepted as children of a gateway. @@ -101,7 +40,7 @@ func (r *TargetRefReconciler) FetchAcceptedGatewayHTTPRoutes(ctx context.Context logger = logger.WithName("FetchAcceptedGatewayHTTPRoutes").WithValues("gateway", gwKey) routeList := &gatewayapiv1.HTTPRouteList{} - err := r.Client().List(ctx, routeList) + err := r.Client.List(ctx, routeList) if err != nil { logger.V(1).Info("failed to list httproutes", "err", err) return @@ -109,7 +48,7 @@ func (r *TargetRefReconciler) FetchAcceptedGatewayHTTPRoutes(ctx context.Context for idx := range routeList.Items { route := routeList.Items[idx] - routeParentStatus, found := common.Find(route.Status.RouteStatus.Parents, func(p gatewayapiv1.RouteParentStatus) bool { + routeParentStatus, found := utils.Find(route.Status.RouteStatus.Parents, func(p gatewayapiv1.RouteParentStatus) bool { return *p.ParentRef.Kind == ("Gateway") && ((p.ParentRef.Namespace == nil && route.GetNamespace() == gwKey.Namespace) || string(*p.ParentRef.Namespace) == gwKey.Namespace) && string(p.ParentRef.Name) == gwKey.Name @@ -154,24 +93,24 @@ func (r *TargetRefReconciler) TargetedGatewayKeys(_ context.Context, targetNetwo } // ReconcileTargetBackReference adds policy key in annotations of the target object -func (r *TargetRefReconciler) ReconcileTargetBackReference(ctx context.Context, policy common.KuadrantPolicy, targetNetworkObject client.Object, annotationName string) error { +func (r *TargetRefReconciler) ReconcileTargetBackReference(ctx context.Context, p kuadrant.Policy, targetNetworkObject client.Object, annotationName string) error { logger, _ := logr.FromContext(ctx) - policyKey := client.ObjectKeyFromObject(policy) + policyKey := client.ObjectKeyFromObject(p) targetNetworkObjectKey := client.ObjectKeyFromObject(targetNetworkObject) targetNetworkObjectKind := targetNetworkObject.GetObjectKind().GroupVersionKind() // Reconcile the back reference: - objAnnotations := common.ReadAnnotationsFromObject(targetNetworkObject) + objAnnotations := utils.ReadAnnotationsFromObject(targetNetworkObject) if val, ok := objAnnotations[annotationName]; ok { if val != policyKey.String() { - return common.NewErrConflict(policy.Kind(), val, fmt.Errorf("the %s target %s is already referenced by policy %s", targetNetworkObjectKind, targetNetworkObjectKey, val)) + return kuadrant.NewErrConflict(p.Kind(), val, fmt.Errorf("the %s target %s is already referenced by policy %s", targetNetworkObjectKind, targetNetworkObjectKey, val)) } } else { objAnnotations[annotationName] = policyKey.String() targetNetworkObject.SetAnnotations(objAnnotations) - err := r.UpdateResource(ctx, targetNetworkObject) + err := r.Client.Update(ctx, targetNetworkObject) logger.V(1).Info("ReconcileTargetBackReference: update target object", "kind", targetNetworkObjectKind, "name", targetNetworkObjectKey, "err", err) if err != nil { return err @@ -188,12 +127,12 @@ func (r *TargetRefReconciler) DeleteTargetBackReference(ctx context.Context, tar targetNetworkObjectKind := targetNetworkObject.GetObjectKind().GroupVersionKind() // Reconcile the back reference: - objAnnotations := common.ReadAnnotationsFromObject(targetNetworkObject) + objAnnotations := utils.ReadAnnotationsFromObject(targetNetworkObject) if _, ok := objAnnotations[annotationName]; ok { delete(objAnnotations, annotationName) targetNetworkObject.SetAnnotations(objAnnotations) - err := r.UpdateResource(ctx, targetNetworkObject) + err := r.Client.Update(ctx, targetNetworkObject) logger.V(1).Info("DeleteTargetBackReference: update network resource", "kind", targetNetworkObjectKind, "name", targetNetworkObjectKey, "err", err) if err != nil { return err @@ -208,24 +147,24 @@ func (r *TargetRefReconciler) DeleteTargetBackReference(ctx context.Context, tar // this list of policy refs; nevertheless, the actual order of returned policy refs depends on the order the policy refs // appear in the annotations of the gateways. // Only gateways with status programmed are considered. -func (r *TargetRefReconciler) GetAllGatewayPolicyRefs(ctx context.Context, policyRefsConfig common.PolicyRefsConfig) ([]client.ObjectKey, error) { +func (r *TargetRefReconciler) GetAllGatewayPolicyRefs(ctx context.Context, policyRefsConfig kuadrant.Referrer) ([]client.ObjectKey, error) { var uniquePolicyRefs map[string]struct{} var policyRefs []client.ObjectKey gwList := &gatewayapiv1.GatewayList{} - if err := r.Client().List(ctx, gwList); err != nil { + if err := r.Client.List(ctx, gwList); err != nil { return nil, err } // sort the gateways by creation timestamp to mitigate the risk of non-idenpotent reconciliations - var gateways common.GatewayWrapperList + var gateways kuadrant.GatewayWrapperList for i := range gwList.Items { gateway := gwList.Items[i] // skip gateways that are not managed by kuadrant or that are not ready - if !common.IsKuadrantManaged(&gateway) || meta.IsStatusConditionFalse(gateway.Status.Conditions, common.GatewayProgrammedConditionType) { + if !kuadrant.IsKuadrantManaged(&gateway) || meta.IsStatusConditionFalse(gateway.Status.Conditions, string(gatewayapiv1.GatewayConditionProgrammed)) { continue } - gateways = append(gateways, common.GatewayWrapper{Gateway: &gateway, PolicyRefsConfig: policyRefsConfig}) + gateways = append(gateways, kuadrant.GatewayWrapper{Gateway: &gateway, Referrer: policyRefsConfig}) } sort.Sort(gateways) @@ -241,49 +180,15 @@ func (r *TargetRefReconciler) GetAllGatewayPolicyRefs(ctx context.Context, polic return policyRefs, nil } -// Returns: -// * list of gateways to which the policy applies for the first time -// * list of gateways to which the policy no longer applies -// * list of gateways to which the policy still applies -func (r *TargetRefReconciler) ComputeGatewayDiffs(ctx context.Context, policy common.KuadrantPolicy, targetNetworkObject client.Object, policyRefsConfig common.PolicyRefsConfig) (*GatewayDiff, error) { - logger, _ := logr.FromContext(ctx) - - var gwKeys []client.ObjectKey - if policy.GetDeletionTimestamp() == nil { - gwKeys = r.TargetedGatewayKeys(ctx, targetNetworkObject) - } - - // TODO(rahulanand16nov): maybe think about optimizing it with a label later - allGwList := &gatewayapiv1.GatewayList{} - err := r.Client().List(ctx, allGwList) - if err != nil { - return nil, err - } - - gwDiff := &GatewayDiff{ - GatewaysMissingPolicyRef: common.GatewaysMissingPolicyRef(allGwList, client.ObjectKeyFromObject(policy), gwKeys, policyRefsConfig), - GatewaysWithValidPolicyRef: common.GatewaysWithValidPolicyRef(allGwList, client.ObjectKeyFromObject(policy), gwKeys, policyRefsConfig), - GatewaysWithInvalidPolicyRef: common.GatewaysWithInvalidPolicyRef(allGwList, client.ObjectKeyFromObject(policy), gwKeys, policyRefsConfig), - } - - logger.V(1).Info("ComputeGatewayDiffs", - "#missing-policy-ref", len(gwDiff.GatewaysMissingPolicyRef), - "#valid-policy-ref", len(gwDiff.GatewaysWithValidPolicyRef), - "#invalid-policy-ref", len(gwDiff.GatewaysWithInvalidPolicyRef), - ) - - return gwDiff, nil -} - // ReconcileGatewayPolicyReferences updates the annotations in the Gateway resources that list to all the policies // that directly or indirectly target the gateway, based upon a pre-computed gateway diff object -func (r *TargetRefReconciler) ReconcileGatewayPolicyReferences(ctx context.Context, policy client.Object, gwDiffObj *GatewayDiff) error { +func (r *TargetRefReconciler) ReconcileGatewayPolicyReferences(ctx context.Context, policy client.Object, gwDiffObj *GatewayDiffs) error { logger, _ := logr.FromContext(ctx) // delete the policy from the annotations of the gateways no longer target by the policy for _, gw := range gwDiffObj.GatewaysWithInvalidPolicyRef { if gw.DeletePolicy(client.ObjectKeyFromObject(policy)) { - err := r.UpdateResource(ctx, gw.Gateway) + err := r.Client.Update(ctx, gw.Gateway) logger.V(1).Info("ReconcileGatewayPolicyReferences: update gateway", "gateway with invalid policy ref", gw.Key(), "err", err) if err != nil { return err @@ -294,7 +199,7 @@ func (r *TargetRefReconciler) ReconcileGatewayPolicyReferences(ctx context.Conte // add the policy to the annotations of the gateways target by the policy for _, gw := range gwDiffObj.GatewaysMissingPolicyRef { if gw.AddPolicy(client.ObjectKeyFromObject(policy)) { - err := r.UpdateResource(ctx, gw.Gateway) + err := r.Client.Update(ctx, gw.Gateway) logger.V(1).Info("ReconcileGatewayPolicyReferences: update gateway", "gateway missinf policy ref", gw.Key(), "err", err) if err != nil { return err @@ -304,9 +209,3 @@ func (r *TargetRefReconciler) ReconcileGatewayPolicyReferences(ctx context.Conte return nil } - -type GatewayDiff struct { - GatewaysMissingPolicyRef []common.GatewayWrapper - GatewaysWithValidPolicyRef []common.GatewayWrapper - GatewaysWithInvalidPolicyRef []common.GatewayWrapper -} diff --git a/pkg/library/reconcilers/target_ref_reconciler_test.go b/pkg/library/reconcilers/target_ref_reconciler_test.go new file mode 100644 index 000000000..04a0d6d37 --- /dev/null +++ b/pkg/library/reconcilers/target_ref_reconciler_test.go @@ -0,0 +1,205 @@ +package reconcilers + +import ( + "context" + "testing" + + "github.com/go-logr/logr" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/log" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" +) + +func TestReconcileTargetBackReference(t *testing.T) { + var ( + namespace = "operator-unittest" + routeName = "my-route" + annotationName = "some-annotation" + ) + baseCtx := context.Background() + ctx := logr.NewContext(baseCtx, log.Log) + + s := scheme.Scheme + err := appsv1.AddToScheme(s) + if err != nil { + t.Fatal(err) + } + err = gatewayapiv1.AddToScheme(s) + if err != nil { + t.Fatal(err) + } + + policy := &kuadrant.FakePolicy{ + Object: &metav1.PartialObjectMetadata{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-policy", + Namespace: "my-ns", + }, + }, + } + + existingRoute := &gatewayapiv1.HTTPRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "gateway.networking.k8s.io/v1", + Kind: "HTTPRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: routeName, + Namespace: namespace, + }, + Spec: gatewayapiv1.HTTPRouteSpec{ + CommonRouteSpec: gatewayapiv1.CommonRouteSpec{ + ParentRefs: []gatewayapiv1.ParentReference{ + { + Name: "gwName", + }, + }, + }, + }, + Status: gatewayapiv1.HTTPRouteStatus{ + RouteStatus: gatewayapiv1.RouteStatus{ + Parents: []gatewayapiv1.RouteParentStatus{ + { + ParentRef: gatewayapiv1.ParentReference{ + Name: "gwName", + }, + Conditions: []metav1.Condition{ + { + Type: "Accepted", + Status: metav1.ConditionTrue, + }, + }, + }, + }, + }, + }, + } + + // Objects to track in the fake client. + objs := []runtime.Object{existingRoute} + + // Create a fake client to mock API calls. + cl := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() + + targetRefReconciler := TargetRefReconciler{ + Client: cl, + } + + err = targetRefReconciler.ReconcileTargetBackReference(ctx, policy, existingRoute, annotationName) + if err != nil { + t.Fatal(err) + } + + res := &gatewayapiv1.HTTPRoute{} + err = cl.Get(ctx, client.ObjectKey{Name: routeName, Namespace: namespace}, res) + if err != nil { + t.Fatal(err) + } + + if len(res.GetAnnotations()) == 0 { + t.Fatal("annotations are empty") + } + + val, ok := res.GetAnnotations()[annotationName] + if !ok { + t.Fatal("expected annotation not found") + } + + if val != client.ObjectKeyFromObject(policy).String() { + t.Fatalf("annotation value (%s) does not match expected (%s)", val, client.ObjectKeyFromObject(policy).String()) + } +} + +func TestDeleteTargetBackReference(t *testing.T) { + var ( + namespace = "operator-unittest" + routeName = "my-route" + annotationName = "some-annotation" + ) + baseCtx := context.Background() + ctx := logr.NewContext(baseCtx, log.Log) + + s := scheme.Scheme + err := appsv1.AddToScheme(s) + if err != nil { + t.Fatal(err) + } + err = gatewayapiv1.AddToScheme(s) + if err != nil { + t.Fatal(err) + } + + existingRoute := &gatewayapiv1.HTTPRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "gateway.networking.k8s.io/v1", + Kind: "HTTPRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: routeName, + Namespace: namespace, + Annotations: map[string]string{ + annotationName: "annotationValue", + }, + }, + Spec: gatewayapiv1.HTTPRouteSpec{ + CommonRouteSpec: gatewayapiv1.CommonRouteSpec{ + ParentRefs: []gatewayapiv1.ParentReference{ + { + Name: "gwName", + }, + }, + }, + }, + Status: gatewayapiv1.HTTPRouteStatus{ + RouteStatus: gatewayapiv1.RouteStatus{ + Parents: []gatewayapiv1.RouteParentStatus{ + { + ParentRef: gatewayapiv1.ParentReference{ + Name: "gwName", + }, + Conditions: []metav1.Condition{ + { + Type: "Accepted", + Status: metav1.ConditionTrue, + }, + }, + }, + }, + }, + }, + } + + // Objects to track in the fake client. + objs := []runtime.Object{existingRoute} + + // Create a fake client to mock API calls. + cl := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() + targetRefReconciler := TargetRefReconciler{ + Client: cl, + } + + err = targetRefReconciler.DeleteTargetBackReference(ctx, existingRoute, annotationName) + if err != nil { + t.Fatal(err) + } + + res := &gatewayapiv1.HTTPRoute{} + err = cl.Get(ctx, client.ObjectKey{Name: routeName, Namespace: namespace}, res) + if err != nil { + t.Fatal(err) + } + + if len(res.GetAnnotations()) > 0 { + _, ok := res.GetAnnotations()[annotationName] + if ok { + t.Fatal("expected annotation found and it should have been deleted") + } + } +} diff --git a/pkg/library/utils/domains.go b/pkg/library/utils/domains.go new file mode 100644 index 000000000..178a21d39 --- /dev/null +++ b/pkg/library/utils/domains.go @@ -0,0 +1,24 @@ +package utils + +// ValidSubdomains returns (true, "") when every single subdomains item +// is a subset of at least one of the domains. +// Domains and subdomains may be prefixed with a wildcard label (*.). +// The wildcard label must appear by itself as the first label. +// When one of the subdomains is not a subset of the domains, it returns false and +// the subdomain not being subset of the domains +func ValidSubdomains(domains, subdomains []string) (bool, string) { + for _, subdomain := range subdomains { + validSubdomain := false + for _, domain := range domains { + if Name(subdomain).SubsetOf(Name(domain)) { + validSubdomain = true + break + } + } + + if !validSubdomain { + return false, subdomain + } + } + return true, "" +} diff --git a/pkg/library/utils/domains_test.go b/pkg/library/utils/domains_test.go new file mode 100644 index 000000000..47a65f8d4 --- /dev/null +++ b/pkg/library/utils/domains_test.go @@ -0,0 +1,35 @@ +package utils + +import "testing" + +func TestValidSubdomains(t *testing.T) { + testCases := []struct { + name string + domains []string + subdomains []string + expected bool + expectedHostname string + }{ + {"nil", nil, nil, true, ""}, + {"nil subdomains", []string{"*.example.com"}, nil, true, ""}, + {"nil domains", nil, []string{"*.example.com"}, false, "*.example.com"}, + {"dot matters", []string{"*.example.com"}, []string{"example.com"}, false, "example.com"}, + {"dot matters2", []string{"example.com"}, []string{"*.example.com"}, false, "*.example.com"}, + {"happy path", []string{"*.example.com", "*.net"}, []string{"*.other.net", "test.example.com"}, true, ""}, + {"not all match", []string{"*.example.com", "*.net"}, []string{"*.other.com", "*.example.com"}, false, "*.other.com"}, + {"wildcard in subdomains does not match", []string{"*.example.com", "*.net"}, []string{"*", "*.example.com"}, false, "*"}, + {"wildcard in domains matches all", []string{"*", "*.net"}, []string{"*.net", "*.example.com"}, true, ""}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(subT *testing.T) { + valid, hostname := ValidSubdomains(tc.domains, tc.subdomains) + if valid != tc.expected { + subT.Errorf("expected (%t), got (%t)", tc.expected, valid) + } + if hostname != tc.expectedHostname { + subT.Errorf("expected hostname (%s), got (%s)", tc.expectedHostname, hostname) + } + }) + } +} diff --git a/pkg/common/hostname.go b/pkg/library/utils/hostname.go similarity index 72% rename from pkg/common/hostname.go rename to pkg/library/utils/hostname.go index c5a007c5c..f1781b087 100644 --- a/pkg/common/hostname.go +++ b/pkg/library/utils/hostname.go @@ -1,8 +1,13 @@ -package common +package utils // From https://github.com/istio/istio/blob/8af3aff0648fcf7ed3829e82ee0bd741bfc99a2e/pkg/config/host/name.go -import "strings" +import ( + "strings" + + "github.com/kuadrant/authorino/pkg/utils" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" +) // Name describes a (possibly wildcarded) hostname type Name string @@ -41,3 +46,10 @@ func (n Name) IsWildCarded() bool { func (n Name) String() string { return string(n) } + +// HostnamesToStrings converts []gatewayapiv1.Hostname to []string +func HostnamesToStrings(hostnames []gatewayapiv1.Hostname) []string { + return utils.Map(hostnames, func(hostname gatewayapiv1.Hostname) string { + return string(hostname) + }) +} diff --git a/pkg/common/hostname_test.go b/pkg/library/utils/hostname_test.go similarity index 61% rename from pkg/common/hostname_test.go rename to pkg/library/utils/hostname_test.go index 96e729096..5c0939890 100644 --- a/pkg/common/hostname_test.go +++ b/pkg/library/utils/hostname_test.go @@ -1,9 +1,12 @@ //go:build unit -package common +package utils import ( + "reflect" "testing" + + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" ) func TestNameSubsetOf(t *testing.T) { @@ -82,3 +85,46 @@ func TestString(t *testing.T) { }) } } + +func TestHostnamesToStrings(t *testing.T) { + testCases := []struct { + name string + inputHostnames []gatewayapiv1.Hostname + expectedOutput []string + }{ + { + name: "when input is empty then return empty output", + inputHostnames: []gatewayapiv1.Hostname{}, + expectedOutput: []string{}, + }, + { + name: "when input has a single precise hostname then return a single string", + inputHostnames: []gatewayapiv1.Hostname{"example.com"}, + expectedOutput: []string{"example.com"}, + }, + { + name: "when input has multiple precise hostnames then return the corresponding strings", + inputHostnames: []gatewayapiv1.Hostname{"example.com", "test.com", "localhost"}, + expectedOutput: []string{"example.com", "test.com", "localhost"}, + }, + { + name: "when input has a wildcard hostname then return the wildcard string", + inputHostnames: []gatewayapiv1.Hostname{"*.example.com"}, + expectedOutput: []string{"*.example.com"}, + }, + { + name: "when input has both precise and wildcard hostnames then return the corresponding strings", + inputHostnames: []gatewayapiv1.Hostname{"example.com", "*.test.com"}, + expectedOutput: []string{"example.com", "*.test.com"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + output := HostnamesToStrings(tc.inputHostnames) + if !reflect.DeepEqual(tc.expectedOutput, output) { + t.Errorf("Unexpected output. Expected %v but got %v", tc.expectedOutput, output) + } + }) + } +} diff --git a/pkg/library/utils/object_utils.go b/pkg/library/utils/object_utils.go new file mode 100644 index 000000000..950fb0870 --- /dev/null +++ b/pkg/library/utils/object_utils.go @@ -0,0 +1,26 @@ +package utils + +import ( + "strings" + + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// NamespacedNameToObjectKey converts format string to k8s object key. +// It's common for K8s to reference an object using this format. For e.g. gateways in VirtualService. +func NamespacedNameToObjectKey(namespacedName, defaultNamespace string) client.ObjectKey { + if i := strings.IndexRune(namespacedName, '/'); i >= 0 { + return client.ObjectKey{Namespace: namespacedName[:i], Name: namespacedName[i+1:]} + } + return client.ObjectKey{Namespace: defaultNamespace, Name: namespacedName} +} + +// ReadAnnotationsFromObject reads the annotations from a Kubernetes object +// and returns them as a map. If the object has no annotations, it returns an empty map. +func ReadAnnotationsFromObject(obj client.Object) map[string]string { + annotations := obj.GetAnnotations() + if annotations == nil { + annotations = map[string]string{} + } + return annotations +} diff --git a/pkg/library/utils/object_utils_test.go b/pkg/library/utils/object_utils_test.go new file mode 100644 index 000000000..b34d4583c --- /dev/null +++ b/pkg/library/utils/object_utils_test.go @@ -0,0 +1,88 @@ +package utils + +import ( + "reflect" + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestNamespacedNameToObjectKey(t *testing.T) { + t.Run("when a namespaced name is provided then return an ObjectKey with corresponding namespace and name", func(t *testing.T) { + namespacedName := "test-namespace/test-name" + defaultNamespace := "default" + + result := NamespacedNameToObjectKey(namespacedName, defaultNamespace) + + expected := client.ObjectKey{Name: "test-name", Namespace: "test-namespace"} + if !reflect.DeepEqual(result, expected) { + t.Errorf("Expected %v, but got %v", expected, result) + } + }) + + t.Run("when only a name is provided then return an ObjectKey with the default namespace and provided name", func(t *testing.T) { + namespacedName := "test-name" + defaultNamespace := "default" + + result := NamespacedNameToObjectKey(namespacedName, defaultNamespace) + + expected := client.ObjectKey{Name: "test-name", Namespace: "default"} + if !reflect.DeepEqual(result, expected) { + t.Errorf("Expected %v, but got %v", expected, result) + } + }) + + t.Run("when an empty string is provided, then return an ObjectKey with default namespace and empty name", func(t *testing.T) { + namespacedName := "" + defaultNamespace := "default" + + result := NamespacedNameToObjectKey(namespacedName, defaultNamespace) + + expected := client.ObjectKey{Name: "", Namespace: "default"} + if !reflect.DeepEqual(result, expected) { + t.Errorf("Expected %v, but got %v", expected, result) + } + }) +} + +func TestReadAnnotationsFromObject(t *testing.T) { + testCases := []struct { + name string + input client.Object + expected map[string]string + }{ + { + name: "when object has annotations then return the annotations", + input: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + }, + expected: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + { + name: "when object has no annotations then return an empty map", + input: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{}, + }, + expected: map[string]string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := ReadAnnotationsFromObject(tc.input) + if !reflect.DeepEqual(actual, tc.expected) { + t.Errorf("Expected annotations %v, but got %v", tc.expected, actual) + } + }) + } +} diff --git a/pkg/library/utils/service.go b/pkg/library/utils/service.go new file mode 100644 index 000000000..1073ec7e9 --- /dev/null +++ b/pkg/library/utils/service.go @@ -0,0 +1,50 @@ +package utils + +import ( + "context" + "fmt" + "strconv" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func GetServiceWorkloadSelector(ctx context.Context, k8sClient client.Client, serviceKey client.ObjectKey) (map[string]string, error) { + service, err := GetService(ctx, k8sClient, serviceKey) + if err != nil { + return nil, err + } + return service.Spec.Selector, nil +} + +func GetService(ctx context.Context, k8sClient client.Client, serviceKey client.ObjectKey) (*corev1.Service, error) { + service := &corev1.Service{} + if err := k8sClient.Get(ctx, serviceKey, service); err != nil { + return nil, err + } + return service, nil +} + +// GetServicePortNumber returns the port number from the referenced key and port info +// the port info can be named port or already a number. +func GetServicePortNumber(ctx context.Context, k8sClient client.Client, serviceKey client.ObjectKey, servicePort string) (int32, error) { + // check if the port is a number already. + if num, err := strconv.ParseInt(servicePort, 10, 32); err == nil { + return int32(num), nil + } + + // As the port is name, resolv the port from the service + service, err := GetService(ctx, k8sClient, serviceKey) + if err != nil { + // the service must exist + return 0, err + } + + for _, p := range service.Spec.Ports { + if p.Name == servicePort { + return int32(p.TargetPort.IntValue()), nil + } + } + + return 0, fmt.Errorf("service port %s was not found in %s", servicePort, serviceKey.String()) +} diff --git a/pkg/library/utils/service_test.go b/pkg/library/utils/service_test.go new file mode 100644 index 000000000..901a8a6ee --- /dev/null +++ b/pkg/library/utils/service_test.go @@ -0,0 +1,76 @@ +package utils + +import ( + "context" + "testing" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestGetService(t *testing.T) { + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "svc-ns", + Name: "my-svc", + Labels: map[string]string{ + "a-label": "irrelevant", + }, + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "a-selector": "what-we-are-looking-for", + }, + }, + } + + k8sClient := fake.NewClientBuilder().WithRuntimeObjects(service).Build() + + var svc *corev1.Service + var err error + + svc, err = GetService(context.TODO(), k8sClient, client.ObjectKey{Namespace: "svc-ns", Name: "my-svc"}) + if err != nil || svc == nil || svc.GetNamespace() != service.GetNamespace() || svc.GetName() != service.GetName() { + t.Error("should have gotten Service svc-ns/my-svc") + } + + svc, err = GetService(context.TODO(), k8sClient, client.ObjectKey{Namespace: "svc-ns", Name: "unknown"}) + if err == nil || !apierrors.IsNotFound(err) || svc != nil { + t.Error("should have gotten no Service") + } +} + +func TestGetServiceWorkloadSelector(t *testing.T) { + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "svc-ns", + Name: "my-svc", + Labels: map[string]string{ + "a-label": "irrelevant", + }, + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "a-selector": "what-we-are-looking-for", + }, + }, + } + + k8sClient := fake.NewClientBuilder().WithRuntimeObjects(service).Build() + + var selector map[string]string + var err error + + selector, err = GetServiceWorkloadSelector(context.TODO(), k8sClient, client.ObjectKey{Namespace: "svc-ns", Name: "my-svc"}) + if err != nil || len(selector) != 1 || selector["a-selector"] != "what-we-are-looking-for" { + t.Error("should not have failed to get the service workload selector") + } + + selector, err = GetServiceWorkloadSelector(context.TODO(), k8sClient, client.ObjectKey{Namespace: "svc-ns", Name: "unknown-svc"}) + if err == nil || !apierrors.IsNotFound(err) || selector != nil { + t.Error("should have failed to get the service workload selector") + } +} diff --git a/pkg/library/utils/slice_utils.go b/pkg/library/utils/slice_utils.go new file mode 100644 index 000000000..de5f8793f --- /dev/null +++ b/pkg/library/utils/slice_utils.go @@ -0,0 +1,78 @@ +package utils + +import "slices" + +// GetEmptySliceIfNil returns a provided slice, or an empty slice of the same type if the input slice is nil. +func GetEmptySliceIfNil[T any](val []T) []T { + if val == nil { + return make([]T, 0) + } + return val +} + +// SameElements checks if the two slices contain the exact same elements. Order does not matter. +func SameElements[T comparable](s1, s2 []T) bool { + if len(s1) != len(s2) { + return false + } + for _, v := range s1 { + if !slices.Contains(s2, v) { + return false + } + } + return true +} + +func Intersect[T comparable](slice1, slice2 []T) bool { + for _, item := range slice1 { + if slices.Contains(slice2, item) { + return true + } + } + return false +} + +func Intersection[T comparable](slice1, slice2 []T) []T { + smallerSlice := slice1 + largerSlice := slice2 + if len(slice1) > len(slice2) { + smallerSlice = slice2 + largerSlice = slice1 + } + var result []T + for _, item := range smallerSlice { + if slices.Contains(largerSlice, item) { + result = append(result, item) + } + } + return result +} + +func Find[T any](slice []T, match func(T) bool) (*T, bool) { + for _, item := range slice { + if match(item) { + return &item, true + } + } + return nil, false +} + +// Map applies the given mapper function to each element in the input slice and returns a new slice with the results. +func Map[T, U any](slice []T, f func(T) U) []U { + arr := make([]U, len(slice)) + for i, e := range slice { + arr[i] = f(e) + } + return arr +} + +// Filter filters the input slice using the given predicate function and returns a new slice with the results. +func Filter[T any](slice []T, f func(T) bool) []T { + arr := make([]T, 0) + for _, e := range slice { + if f(e) { + arr = append(arr, e) + } + } + return arr +} diff --git a/pkg/library/utils/slice_utils_test.go b/pkg/library/utils/slice_utils_test.go new file mode 100644 index 000000000..feb4063dc --- /dev/null +++ b/pkg/library/utils/slice_utils_test.go @@ -0,0 +1,318 @@ +package utils + +import ( + "reflect" + "testing" +) + +func TestFind(t *testing.T) { + s := []string{"a", "ab", "abc"} + + if r, found := Find(s, func(el string) bool { return el == "ab" }); !found || r == nil || *r != "ab" { + t.Error("should have found 'ab' in the slice") + } + + if r, found := Find(s, func(el string) bool { return len(el) <= 3 }); !found || r == nil || *r != "a" { + t.Error("should have found 'a' in the slice") + } + + if r, found := Find(s, func(el string) bool { return len(el) >= 3 }); !found || r == nil || *r != "abc" { + t.Error("should have found 'abc' in the slice") + } + + if r, found := Find(s, func(el string) bool { return len(el) == 4 }); found || r != nil { + t.Error("should not have found anything in the slice") + } + + i := []int{1, 2, 3} + + if r, found := Find(i, func(el int) bool { return el/3 == 1 }); !found || r == nil || *r != 3 { + t.Error("should have found 3 in the slice") + } + + if r, found := Find(i, func(el int) bool { return el == 75 }); found || r != nil { + t.Error("should not have found anything in the slice") + } +} + +func TestGetEmptySliceIfNil(t *testing.T) { + t.Run("when a non-nil slice is provided then return same slice", func(t *testing.T) { + value := []int{1, 2, 3} + expected := value + + result := GetEmptySliceIfNil(value) + + if !reflect.DeepEqual(result, expected) { + t.Errorf("Expected %v, but got %v", expected, result) + } + }) + + t.Run("when a nil slice is provided then return an empty slice of the same type", func(t *testing.T) { + var value []int + expected := []int{} + + result := GetEmptySliceIfNil(value) + + if !reflect.DeepEqual(result, expected) { + t.Errorf("Expected %v, but got %v", expected, result) + } + }) +} + +func TestSameElements(t *testing.T) { + testCases := []struct { + name string + slice1 []string + slice2 []string + expected bool + }{ + { + name: "when slice1 and slice2 contain the same elements then return true", + slice1: []string{"test-gw1", "test-gw2", "test-gw3"}, + slice2: []string{"test-gw1", "test-gw2", "test-gw3"}, + expected: true, + }, + { + name: "when slice1 and slice2 contain unique elements then return false", + slice1: []string{"test-gw1", "test-gw2"}, + slice2: []string{"test-gw1", "test-gw3"}, + expected: false, + }, + { + name: "when both slices are empty then return true", + slice1: []string{}, + slice2: []string{}, + expected: true, + }, + { + name: "when both slices are nil then return true", + expected: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if SameElements(tc.slice1, tc.slice2) != tc.expected { + t.Errorf("when slice1=%v and slice2=%v, expected=%v, but got=%v", tc.slice1, tc.slice2, tc.expected, !tc.expected) + } + }) + } +} + +func TestIntersect(t *testing.T) { + testCases := []struct { + name string + slice1 []string + slice2 []string + expected bool + }{ + { + name: "when slice1 and slice2 have one common item then return true", + slice1: []string{"test-gw1", "test-gw2"}, + slice2: []string{"test-gw1", "test-gw3", "test-gw4"}, + expected: true, + }, + { + name: "when slice1 and slice2 have no common item then return false", + slice1: []string{"test-gw1", "test-gw2"}, + slice2: []string{"test-gw3", "test-gw4"}, + expected: false, + }, + { + name: "when slice1 is empty then return false", + slice1: []string{}, + slice2: []string{"test-gw3", "test-gw4"}, + expected: false, + }, + { + name: "when slice2 is empty then return false", + slice1: []string{"test-gw1", "test-gw2"}, + slice2: []string{}, + expected: false, + }, + { + name: "when both slices are empty then return false", + slice1: []string{}, + slice2: []string{}, + expected: false, + }, + { + name: "when slice1 is nil then return false", + slice2: []string{"test-gw3", "test-gw4"}, + expected: false, + }, + { + name: "when slice2 is nil then return false", + slice1: []string{"test-gw1", "test-gw2"}, + expected: false, + }, + { + name: "when both slices are nil then return false", + expected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if Intersect(tc.slice1, tc.slice2) != tc.expected { + t.Errorf("when slice1=%v and slice2=%v, expected=%v, but got=%v", tc.slice1, tc.slice2, tc.expected, !tc.expected) + } + }) + } +} + +func TestIntersectWithInts(t *testing.T) { + testCases := []struct { + name string + slice1 []int + slice2 []int + expected bool + }{ + { + name: "when slice1 and slice2 have one common item then return true", + slice1: []int{1, 2}, + slice2: []int{1, 3, 4}, + expected: true, + }, + { + name: "when slice1 and slice2 have no common item then return false", + slice1: []int{1, 2}, + slice2: []int{3, 4}, + expected: false, + }, + { + name: "when slice1 is empty then return false", + slice1: []int{}, + slice2: []int{3, 4}, + expected: false, + }, + { + name: "when slice2 is empty then return false", + slice1: []int{1, 2}, + slice2: []int{}, + expected: false, + }, + { + name: "when both slices are empty then return false", + slice1: []int{}, + slice2: []int{}, + expected: false, + }, + { + name: "when slice1 is nil then return false", + slice2: []int{3, 4}, + expected: false, + }, + { + name: "when slice2 is nil then return false", + slice1: []int{1, 2}, + expected: false, + }, + { + name: "when both slices are nil then return false", + expected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if Intersect(tc.slice1, tc.slice2) != tc.expected { + t.Errorf("when slice1=%v and slice2=%v, expected=%v, but got=%v", tc.slice1, tc.slice2, tc.expected, !tc.expected) + } + }) + } +} + +func TestIntersection(t *testing.T) { + testCases := []struct { + name string + slice1 []string + slice2 []string + expected []string + }{ + { + name: "when slice1 and slice2 have one common item then return that item", + slice1: []string{"test-gw1", "test-gw2"}, + slice2: []string{"test-gw1", "test-gw3", "test-gw4"}, + expected: []string{"test-gw1"}, + }, + { + name: "when slice1 and slice2 have no common item then return nil", + slice1: []string{"test-gw1", "test-gw2"}, + slice2: []string{"test-gw3", "test-gw4"}, + expected: nil, + }, + { + name: "when slice1 is empty then return nil", + slice1: []string{}, + slice2: []string{"test-gw3", "test-gw4"}, + expected: nil, + }, + { + name: "when slice2 is empty then return nil", + slice1: []string{"test-gw1", "test-gw2"}, + slice2: []string{}, + expected: nil, + }, + { + name: "when both slices are empty then return nil", + slice1: []string{}, + slice2: []string{}, + expected: nil, + }, + { + name: "when slice1 is nil then return nil", + slice2: []string{"test-gw3", "test-gw4"}, + expected: nil, + }, + { + name: "when slice2 is nil then return nil", + slice1: []string{"test-gw1", "test-gw2"}, + expected: nil, + }, + { + name: "when both slices are nil then return nil", + expected: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if r := Intersection(tc.slice1, tc.slice2); !reflect.DeepEqual(r, tc.expected) { + t.Errorf("expected=%v; got=%v", tc.expected, r) + } + }) + } +} + +func TestMap(t *testing.T) { + slice1 := []int{1, 2, 3, 4} + f1 := func(x int) int { return x + 1 } + expected1 := []int{2, 3, 4, 5} + result1 := Map(slice1, f1) + t.Run("when mapping an int slice with an increment function then return new slice with the incremented values", func(t *testing.T) { + if !reflect.DeepEqual(result1, expected1) { + t.Errorf("result1 = %v; expected %v", result1, expected1) + } + }) + + slice2 := []string{"hello", "world", "buz", "a"} + f2 := func(s string) int { return len(s) } + expected2 := []int{5, 5, 3, 1} + result2 := Map(slice2, f2) + t.Run("when mapping a string slice with string->int mapping then return new slice with the mapped values", func(t *testing.T) { + if !reflect.DeepEqual(result2, expected2) { + t.Errorf("result2 = %v; expected %v", result2, expected2) + } + }) + + slice3 := []int{} + f3 := func(x int) float32 { return float32(x) / 2 } + expected3 := []float32{} + result3 := Map(slice3, f3) + t.Run("when mapping an empty int slice then return an empty slice", func(t *testing.T) { + if !reflect.DeepEqual(result3, expected3) { + t.Errorf("result3 = %v; expected %v", result3, expected3) + } + }) +} diff --git a/pkg/reconcilers/targetref_reconciler_test.go b/pkg/reconcilers/targetref_reconciler_test.go deleted file mode 100644 index 532395afb..000000000 --- a/pkg/reconcilers/targetref_reconciler_test.go +++ /dev/null @@ -1,561 +0,0 @@ -//go:build unit - -/* -Copyright 2021 Red Hat, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package reconcilers - -import ( - "context" - "reflect" - "testing" - - "github.com/go-logr/logr" - "github.com/kuadrant/kuadrant-operator/api/v1beta2" - appsv1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/tools/record" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - - "github.com/kuadrant/kuadrant-operator/pkg/log" -) - -func TestFetchValidGateway(t *testing.T) { - var ( - namespace = "operator-unittest" - gwName = "my-gateway" - ) - baseCtx := context.Background() - ctx := logr.NewContext(baseCtx, log.Log) - - s := scheme.Scheme - err := appsv1.AddToScheme(s) - if err != nil { - t.Fatal(err) - } - err = gatewayapiv1.AddToScheme(s) - if err != nil { - t.Fatal(err) - } - - existingGateway := &gatewayapiv1.Gateway{ - TypeMeta: metav1.TypeMeta{ - APIVersion: gatewayapiv1.GroupName, - Kind: "Gateway", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: gwName, - Namespace: namespace, - }, - Spec: gatewayapiv1.GatewaySpec{ - GatewayClassName: "istio", - }, - Status: gatewayapiv1.GatewayStatus{ - Conditions: []metav1.Condition{ - { - Type: "Ready", - Status: metav1.ConditionTrue, - }, - }, - }, - } - - // Objects to track in the fake client. - objs := []runtime.Object{existingGateway} - - // Create a fake client to mock API calls. - cl := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() - clientAPIReader := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() - recorder := record.NewFakeRecorder(1000) - - baseReconciler := NewBaseReconciler(cl, s, clientAPIReader, log.Log, recorder) - targetRefReconciler := TargetRefReconciler{ - BaseReconciler: baseReconciler, - } - - key := client.ObjectKey{Name: gwName, Namespace: namespace} - - res, err := targetRefReconciler.FetchValidGateway(ctx, key) - if err != nil { - t.Fatal(err) - } - - if res == nil { - t.Fatal("res is nil") - } - - if !reflect.DeepEqual(res.Spec, existingGateway.Spec) { - t.Fatal("res spec not as expected") - } -} - -func TestFetchValidHTTPRoute(t *testing.T) { - var ( - namespace = "operator-unittest" - routeName = "my-route" - ) - baseCtx := context.Background() - ctx := logr.NewContext(baseCtx, log.Log) - - s := scheme.Scheme - err := appsv1.AddToScheme(s) - if err != nil { - t.Fatal(err) - } - err = gatewayapiv1.AddToScheme(s) - if err != nil { - t.Fatal(err) - } - - existingRoute := &gatewayapiv1.HTTPRoute{ - TypeMeta: metav1.TypeMeta{ - APIVersion: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: routeName, - Namespace: namespace, - }, - Spec: gatewayapiv1.HTTPRouteSpec{ - CommonRouteSpec: gatewayapiv1.CommonRouteSpec{ - ParentRefs: []gatewayapiv1.ParentReference{ - { - Name: "gwName", - }, - }, - }, - }, - Status: gatewayapiv1.HTTPRouteStatus{ - RouteStatus: gatewayapiv1.RouteStatus{ - Parents: []gatewayapiv1.RouteParentStatus{ - { - ParentRef: gatewayapiv1.ParentReference{ - Name: "gwName", - }, - Conditions: []metav1.Condition{ - { - Type: "Accepted", - Status: metav1.ConditionTrue, - }, - }, - }, - }, - }, - }, - } - - // Objects to track in the fake client. - objs := []runtime.Object{existingRoute} - - // Create a fake client to mock API calls. - cl := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() - clientAPIReader := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() - recorder := record.NewFakeRecorder(1000) - - baseReconciler := NewBaseReconciler(cl, s, clientAPIReader, log.Log, recorder) - targetRefReconciler := TargetRefReconciler{ - BaseReconciler: baseReconciler, - } - - key := client.ObjectKey{Name: routeName, Namespace: namespace} - - res, err := targetRefReconciler.FetchValidHTTPRoute(ctx, key) - if err != nil { - t.Fatal(err) - } - - if res == nil { - t.Fatal("res is nil") - } - - if !reflect.DeepEqual(res.Spec, existingRoute.Spec) { - t.Fatal("res spec not as expected") - } -} - -func TestFetchValidTargetRef(t *testing.T) { - var ( - namespace = "operator-unittest" - routeName = "my-route" - ) - baseCtx := context.Background() - ctx := logr.NewContext(baseCtx, log.Log) - - s := scheme.Scheme - err := appsv1.AddToScheme(s) - if err != nil { - t.Fatal(err) - } - err = gatewayapiv1.AddToScheme(s) - if err != nil { - t.Fatal(err) - } - - targetRef := gatewayapiv1alpha2.PolicyTargetReference{ - Group: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - Name: gatewayapiv1.ObjectName(routeName), - } - - existingRoute := &gatewayapiv1.HTTPRoute{ - TypeMeta: metav1.TypeMeta{ - APIVersion: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: routeName, - Namespace: namespace, - }, - Spec: gatewayapiv1.HTTPRouteSpec{ - CommonRouteSpec: gatewayapiv1.CommonRouteSpec{ - ParentRefs: []gatewayapiv1.ParentReference{ - { - Name: "gwName", - }, - }, - }, - }, - Status: gatewayapiv1.HTTPRouteStatus{ - RouteStatus: gatewayapiv1.RouteStatus{ - Parents: []gatewayapiv1.RouteParentStatus{ - { - ParentRef: gatewayapiv1.ParentReference{ - Name: "gwName", - }, - Conditions: []metav1.Condition{ - { - Type: "Accepted", - Status: metav1.ConditionTrue, - }, - }, - }, - }, - }, - }, - } - - // Objects to track in the fake client. - objs := []runtime.Object{existingRoute} - - // Create a fake client to mock API calls. - cl := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() - clientAPIReader := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() - recorder := record.NewFakeRecorder(1000) - - baseReconciler := NewBaseReconciler(cl, s, clientAPIReader, log.Log, recorder) - targetRefReconciler := TargetRefReconciler{ - BaseReconciler: baseReconciler, - } - - res, err := targetRefReconciler.FetchValidTargetRef(ctx, targetRef, namespace) - if err != nil { - t.Fatal(err) - } - - if res == nil { - t.Fatal("res is nil") - } - - switch obj := res.(type) { - case *gatewayapiv1.HTTPRoute: - if !reflect.DeepEqual(obj.Spec, existingRoute.Spec) { - t.Fatal("res spec not as expected") - } - default: - t.Fatal("res type not known") - } -} - -func TestReconcileTargetBackReference(t *testing.T) { - var ( - namespace = "operator-unittest" - routeName = "my-route" - annotationName = "some-annotation" - ) - baseCtx := context.Background() - ctx := logr.NewContext(baseCtx, log.Log) - - s := scheme.Scheme - err := appsv1.AddToScheme(s) - if err != nil { - t.Fatal(err) - } - err = gatewayapiv1.AddToScheme(s) - if err != nil { - t.Fatal(err) - } - - policy := &v1beta2.RateLimitPolicy{ObjectMeta: metav1.ObjectMeta{Name: "someName", Namespace: "someNamespace"}} - - existingRoute := &gatewayapiv1.HTTPRoute{ - TypeMeta: metav1.TypeMeta{ - APIVersion: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: routeName, - Namespace: namespace, - }, - Spec: gatewayapiv1.HTTPRouteSpec{ - CommonRouteSpec: gatewayapiv1.CommonRouteSpec{ - ParentRefs: []gatewayapiv1.ParentReference{ - { - Name: "gwName", - }, - }, - }, - }, - Status: gatewayapiv1.HTTPRouteStatus{ - RouteStatus: gatewayapiv1.RouteStatus{ - Parents: []gatewayapiv1.RouteParentStatus{ - { - ParentRef: gatewayapiv1.ParentReference{ - Name: "gwName", - }, - Conditions: []metav1.Condition{ - { - Type: "Accepted", - Status: metav1.ConditionTrue, - }, - }, - }, - }, - }, - }, - } - - // Objects to track in the fake client. - objs := []runtime.Object{existingRoute} - - // Create a fake client to mock API calls. - cl := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() - clientAPIReader := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() - recorder := record.NewFakeRecorder(1000) - - baseReconciler := NewBaseReconciler(cl, s, clientAPIReader, log.Log, recorder) - targetRefReconciler := TargetRefReconciler{ - BaseReconciler: baseReconciler, - } - - err = targetRefReconciler.ReconcileTargetBackReference(ctx, policy, existingRoute, annotationName) - if err != nil { - t.Fatal(err) - } - - res := &gatewayapiv1.HTTPRoute{} - err = cl.Get(ctx, client.ObjectKey{Name: routeName, Namespace: namespace}, res) - if err != nil { - t.Fatal(err) - } - - if res == nil { - t.Fatal("res is nil") - } - - if len(res.GetAnnotations()) == 0 { - t.Fatal("annotations are empty") - } - - val, ok := res.GetAnnotations()[annotationName] - if !ok { - t.Fatal("expected annotation not found") - } - - if val != client.ObjectKeyFromObject(policy).String() { - t.Fatalf("annotation value (%s) does not match expected (%s)", val, client.ObjectKeyFromObject(policy).String()) - } -} - -func TestTargetedGatewayKeys(t *testing.T) { - var ( - namespace = "operator-unittest" - routeName = "my-route" - ) - baseCtx := context.Background() - ctx := logr.NewContext(baseCtx, log.Log) - - s := scheme.Scheme - err := appsv1.AddToScheme(s) - if err != nil { - t.Fatal(err) - } - err = gatewayapiv1.AddToScheme(s) - if err != nil { - t.Fatal(err) - } - - existingRoute := &gatewayapiv1.HTTPRoute{ - TypeMeta: metav1.TypeMeta{ - APIVersion: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: routeName, - Namespace: namespace, - }, - Spec: gatewayapiv1.HTTPRouteSpec{ - CommonRouteSpec: gatewayapiv1.CommonRouteSpec{ - ParentRefs: []gatewayapiv1.ParentReference{ - { - Name: "gwName", - }, - }, - }, - }, - Status: gatewayapiv1.HTTPRouteStatus{ - RouteStatus: gatewayapiv1.RouteStatus{ - Parents: []gatewayapiv1.RouteParentStatus{ - { - ParentRef: gatewayapiv1.ParentReference{ - Name: "gwName", - }, - Conditions: []metav1.Condition{ - { - Type: "Accepted", - Status: metav1.ConditionTrue, - }, - }, - }, - }, - }, - }, - } - - // Objects to track in the fake client. - objs := []runtime.Object{existingRoute} - - // Create a fake client to mock API calls. - cl := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() - clientAPIReader := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() - recorder := record.NewFakeRecorder(1000) - - baseReconciler := NewBaseReconciler(cl, s, clientAPIReader, log.Log, recorder) - targetRefReconciler := TargetRefReconciler{ - BaseReconciler: baseReconciler, - } - - keys := targetRefReconciler.TargetedGatewayKeys(ctx, existingRoute) - - if len(keys) != 1 { - t.Fatalf("gateway key slice length is %d and it was expected to be 1", len(keys)) - } - - expectedKey := client.ObjectKey{Name: "gwName", Namespace: namespace} - - if keys[0] != expectedKey { - t.Fatalf("gwKey value (%+v) does not match expected (%+v)", keys[0], expectedKey) - } -} - -func TestDeleteTargetBackReference(t *testing.T) { - var ( - namespace = "operator-unittest" - routeName = "my-route" - annotationName = "some-annotation" - ) - baseCtx := context.Background() - ctx := logr.NewContext(baseCtx, log.Log) - - s := scheme.Scheme - err := appsv1.AddToScheme(s) - if err != nil { - t.Fatal(err) - } - err = gatewayapiv1.AddToScheme(s) - if err != nil { - t.Fatal(err) - } - - existingRoute := &gatewayapiv1.HTTPRoute{ - TypeMeta: metav1.TypeMeta{ - APIVersion: gatewayapiv1.GroupName, - Kind: "HTTPRoute", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: routeName, - Namespace: namespace, - Annotations: map[string]string{ - annotationName: "annotationValue", - }, - }, - Spec: gatewayapiv1.HTTPRouteSpec{ - CommonRouteSpec: gatewayapiv1.CommonRouteSpec{ - ParentRefs: []gatewayapiv1.ParentReference{ - { - Name: "gwName", - }, - }, - }, - }, - Status: gatewayapiv1.HTTPRouteStatus{ - RouteStatus: gatewayapiv1.RouteStatus{ - Parents: []gatewayapiv1.RouteParentStatus{ - { - ParentRef: gatewayapiv1.ParentReference{ - Name: "gwName", - }, - Conditions: []metav1.Condition{ - { - Type: "Accepted", - Status: metav1.ConditionTrue, - }, - }, - }, - }, - }, - }, - } - - // Objects to track in the fake client. - objs := []runtime.Object{existingRoute} - - // Create a fake client to mock API calls. - cl := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() - clientAPIReader := fake.NewClientBuilder().WithRuntimeObjects(objs...).Build() - recorder := record.NewFakeRecorder(1000) - - baseReconciler := NewBaseReconciler(cl, s, clientAPIReader, log.Log, recorder) - targetRefReconciler := TargetRefReconciler{ - BaseReconciler: baseReconciler, - } - - err = targetRefReconciler.DeleteTargetBackReference(ctx, existingRoute, annotationName) - if err != nil { - t.Fatal(err) - } - - res := &gatewayapiv1.HTTPRoute{} - err = cl.Get(ctx, client.ObjectKey{Name: routeName, Namespace: namespace}, res) - if err != nil { - t.Fatal(err) - } - - if res == nil { - t.Fatal("res is nil") - } - - if len(res.GetAnnotations()) > 0 { - _, ok := res.GetAnnotations()[annotationName] - if ok { - t.Fatal("expected annotation found and it should have been deleted") - } - } -} diff --git a/pkg/rlptools/rate_limit_index.go b/pkg/rlptools/rate_limit_index.go index a14d8cd93..82e38412b 100644 --- a/pkg/rlptools/rate_limit_index.go +++ b/pkg/rlptools/rate_limit_index.go @@ -6,10 +6,9 @@ import ( "strings" "github.com/elliotchance/orderedmap/v2" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/kuadrant/kuadrant-operator/pkg/common" ) type RateLimitIndexKey = client.ObjectKey @@ -108,18 +107,18 @@ func Equal(a, b RateLimitList) bool { // two limits with reordered conditions/variables are effectively the same // For comparison purposes, nil equals the empty array for conditions and variables for idx := range aCopy { - aCopy[idx].Conditions = common.GetEmptySliceIfNil(aCopy[idx].Conditions) + aCopy[idx].Conditions = utils.GetEmptySliceIfNil(aCopy[idx].Conditions) sort.Strings(aCopy[idx].Conditions) - aCopy[idx].Variables = common.GetEmptySliceIfNil(aCopy[idx].Variables) + aCopy[idx].Variables = utils.GetEmptySliceIfNil(aCopy[idx].Variables) sort.Strings(aCopy[idx].Variables) } for idx := range bCopy { - bCopy[idx].Conditions = common.GetEmptySliceIfNil(bCopy[idx].Conditions) + bCopy[idx].Conditions = utils.GetEmptySliceIfNil(bCopy[idx].Conditions) sort.Strings(bCopy[idx].Conditions) - bCopy[idx].Variables = common.GetEmptySliceIfNil(bCopy[idx].Variables) + bCopy[idx].Variables = utils.GetEmptySliceIfNil(bCopy[idx].Variables) sort.Strings(bCopy[idx].Variables) } diff --git a/pkg/rlptools/utils.go b/pkg/rlptools/utils.go index 18118b19f..30348b240 100644 --- a/pkg/rlptools/utils.go +++ b/pkg/rlptools/utils.go @@ -6,10 +6,10 @@ import ( "fmt" "unicode" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" kuadrantv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" - "github.com/kuadrant/kuadrant-operator/pkg/common" ) const ( @@ -50,7 +50,7 @@ func LimitadorRateLimitsFromRLP(rlp *kuadrantv1beta2.RateLimitPolicy) []limitado MaxValue: maxValue, Seconds: seconds, Conditions: []string{fmt.Sprintf("%s == \"1\"", limitIdentifier)}, - Variables: common.GetEmptySliceIfNil(limit.CountersAsStringList()), + Variables: utils.GetEmptySliceIfNil(limit.CountersAsStringList()), Name: LimitsNameFromRLP(rlp), }) } diff --git a/pkg/rlptools/utils_test.go b/pkg/rlptools/utils_test.go index c20db49ee..5db768bbd 100644 --- a/pkg/rlptools/utils_test.go +++ b/pkg/rlptools/utils_test.go @@ -7,11 +7,11 @@ import ( "regexp" "testing" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kuadrantv1beta2 "github.com/kuadrant/kuadrant-operator/api/v1beta2" - "github.com/kuadrant/kuadrant-operator/pkg/common" ) func testRLP_1Limit_1Rate(ns, name string) *kuadrantv1beta2.RateLimitPolicy { @@ -266,7 +266,7 @@ func TestLimitadorRateLimitsFromRLP(t *testing.T) { } // When both slices have equal length, items can be checked one by one. for _, rl := range rateLimits { - if _, found := common.Find(tc.expected, func(expectedRateLimit limitadorv1alpha1.RateLimit) bool { + if _, found := utils.Find(tc.expected, func(expectedRateLimit limitadorv1alpha1.RateLimit) bool { return reflect.DeepEqual(rl, expectedRateLimit) }); !found { subT.Errorf("returned rate limit (%+v) not within expected ones, expected: %v", rl, tc.expected)