diff --git a/api/v1beta1/topology.go b/api/v1beta1/topology.go index a2cf74945..69cf954d9 100644 --- a/api/v1beta1/topology.go +++ b/api/v1beta1/topology.go @@ -1,23 +1,28 @@ package v1beta1 import ( - authorinov1beta1 "github.com/kuadrant/authorino-operator/api/v1beta1" + authorinooperatorv1beta1 "github.com/kuadrant/authorino-operator/api/v1beta1" + authorinov1beta2 "github.com/kuadrant/authorino/api/v1beta2" limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" "github.com/kuadrant/policy-machinery/controller" "github.com/kuadrant/policy-machinery/machinery" "github.com/samber/lo" "k8s.io/apimachinery/pkg/runtime/schema" - gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" ) var ( - AuthorinoGroupKind = schema.GroupKind{Group: authorinov1beta1.GroupVersion.Group, Kind: "Authorino"} - KuadrantGroupKind = schema.GroupKind{Group: GroupVersion.Group, Kind: "Kuadrant"} - LimitadorGroupKind = schema.GroupKind{Group: limitadorv1alpha1.GroupVersion.Group, Kind: "Limitador"} + KuadrantGroupKind = schema.GroupKind{Group: GroupVersion.Group, Kind: "Kuadrant"} + LimitadorGroupKind = schema.GroupKind{Group: limitadorv1alpha1.GroupVersion.Group, Kind: "Limitador"} + AuthorinoGroupKind = schema.GroupKind{Group: authorinooperatorv1beta1.GroupVersion.Group, Kind: "Authorino"} + AuthConfigGroupKind = schema.GroupKind{Group: authorinov1beta2.GroupVersion.Group, Kind: "AuthConfig"} - AuthorinosResource = authorinov1beta1.GroupVersion.WithResource("authorinos") - KuadrantsResource = GroupVersion.WithResource("kuadrants") - LimitadorsResource = limitadorv1alpha1.GroupVersion.WithResource("limitadors") + KuadrantsResource = GroupVersion.WithResource("kuadrants") + LimitadorsResource = limitadorv1alpha1.GroupVersion.WithResource("limitadors") + AuthorinosResource = authorinooperatorv1beta1.GroupVersion.WithResource("authorinos") + AuthConfigsResource = authorinov1beta2.GroupVersion.WithResource("authconfigs") + + AuthConfigHTTPRouteRuleAnnotation = machinery.HTTPRouteRuleGroupKind.String() ) var _ machinery.Object = &Kuadrant{} @@ -31,7 +36,7 @@ func LinkKuadrantToGatewayClasses(objs controller.Store) machinery.LinkFunc { return machinery.LinkFunc{ From: KuadrantGroupKind, - To: schema.GroupKind{Group: gwapiv1.GroupVersion.Group, Kind: "GatewayClass"}, + To: schema.GroupKind{Group: gatewayapiv1.GroupVersion.Group, Kind: "GatewayClass"}, Func: func(_ machinery.Object) []machinery.Object { parents := make([]machinery.Object, len(kuadrants)) for _, parent := range kuadrants { @@ -69,3 +74,22 @@ func LinkKuadrantToAuthorino(objs controller.Store) machinery.LinkFunc { }, } } + +func LinkHTTPRouteRuleToAuthConfig(objs controller.Store) machinery.LinkFunc { + httpRoutes := lo.Map(objs.FilterByGroupKind(machinery.HTTPRouteGroupKind), controller.ObjectAs[*gatewayapiv1.HTTPRoute]) + httpRouteRules := lo.FlatMap(lo.Map(httpRoutes, func(r *gatewayapiv1.HTTPRoute, _ int) *machinery.HTTPRoute { + return &machinery.HTTPRoute{HTTPRoute: r} + }), machinery.HTTPRouteRulesFromHTTPRouteFunc) + + return machinery.LinkFunc{ + From: machinery.HTTPRouteRuleGroupKind, + To: AuthConfigGroupKind, + Func: func(child machinery.Object) []machinery.Object { + return lo.FilterMap(httpRouteRules, func(httpRouteRule *machinery.HTTPRouteRule, _ int) (machinery.Object, bool) { + authConfig := child.(*controller.RuntimeObject).Object.(*authorinov1beta2.AuthConfig) + annotations := authConfig.GetAnnotations() + return httpRouteRule, annotations != nil && annotations[AuthConfigHTTPRouteRuleAnnotation] == httpRouteRule.GetLocator() + }) + }, + } +} diff --git a/api/v1beta3/authpolicy_types.go b/api/v1beta3/authpolicy_types.go index f93f71ed5..b2d0724cf 100644 --- a/api/v1beta3/authpolicy_types.go +++ b/api/v1beta3/authpolicy_types.go @@ -24,6 +24,7 @@ import ( "github.com/go-logr/logr" "github.com/google/go-cmp/cmp" authorinov1beta2 "github.com/kuadrant/authorino/api/v1beta2" + "github.com/samber/lo" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" @@ -489,6 +490,39 @@ func (r *MergeablePatternExpressionOrRef) WithSource(source string) kuadrantv1.M r.Source = source return r } +func (r *MergeablePatternExpressionOrRef) ToWhenConditions(namedPatterns map[string]MergeablePatternExpressions) []WhenCondition { + if ref := r.PatternRef.Name; ref != "" { + if pattern, ok := namedPatterns[ref]; ok { + return lo.Map(pattern.PatternExpressions, func(p authorinov1beta2.PatternExpression, _ int) WhenCondition { + return WhenCondition{ + Selector: ContextSelector(p.Selector), + Operator: WhenConditionOperator(p.Operator), + Value: p.Value, + } + }) + } + } + + if allOf := r.All; len(allOf) > 0 { + return lo.Map(allOf, func(p authorinov1beta2.UnstructuredPatternExpressionOrRef, _ int) WhenCondition { + return WhenCondition{ + Selector: ContextSelector(p.Selector), + Operator: WhenConditionOperator(p.Operator), + Value: p.Value, + } + }) + } + + // FIXME: anyOf cannot be represented in the current schema of the wasm config + + return []WhenCondition{ + { + Selector: ContextSelector(r.Selector), + Operator: WhenConditionOperator(r.Operator), + Value: r.Value, + }, + } +} type MergeableAuthenticationSpec struct { authorinov1beta2.AuthenticationSpec `json:",inline"` @@ -501,6 +535,14 @@ func (r *MergeableAuthenticationSpec) WithSource(source string) kuadrantv1.Merge r.Source = source return r } +func (r *MergeableAuthenticationSpec) UnmarshalJSON(j []byte) error { + spec := authorinov1beta2.AuthenticationSpec{} + if err := json.Unmarshal(j, &spec); err != nil { + return err + } + r.AuthenticationSpec = spec + return nil +} type MergeableMetadataSpec struct { authorinov1beta2.MetadataSpec `json:",inline"` diff --git a/controllers/auth_policies_validator.go b/controllers/auth_policies_validator.go new file mode 100644 index 000000000..69c72be83 --- /dev/null +++ b/controllers/auth_policies_validator.go @@ -0,0 +1,60 @@ +package controllers + +import ( + "context" + "sync" + + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/ptr" + + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + kuadrant "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" +) + +type AuthPolicyValidator struct{} + +// AuthPolicyValidator subscribes to events with potential to flip the validity of auth policies +func (r *AuthPolicyValidator) Subscription() controller.Subscription { + return controller.Subscription{ + ReconcileFunc: r.Validate, + Events: []controller.ResourceEventMatcher{ + {Kind: &machinery.GatewayGroupKind}, + {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.AuthPolicyGroupKind, EventType: ptr.To(controller.CreateEvent)}, + {Kind: &kuadrantv1beta3.AuthPolicyGroupKind, EventType: ptr.To(controller.UpdateEvent)}, + }, + } +} + +func (r *AuthPolicyValidator) Validate(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("AuthPolicyValidator") + + policies := topology.Policies().Items(func(o machinery.Object) bool { + return o.GroupVersionKind().GroupKind() == kuadrantv1beta3.AuthPolicyGroupKind + }) + + logger.V(1).Info("validating auth policies", "policies", len(policies)) + defer logger.V(1).Info("finished validating auth policies") + + state.Store(StateAuthPolicyValid, lo.SliceToMap(policies, func(policy machinery.Policy) (string, error) { + var err error + if len(policy.GetTargetRefs()) > 0 && len(topology.Targetables().Children(policy)) == 0 { + ref := policy.GetTargetRefs()[0] + var res schema.GroupResource + switch ref.GroupVersionKind().Kind { + case machinery.GatewayGroupKind.Kind: + res = controller.GatewaysResource.GroupResource() + case machinery.HTTPRouteGroupKind.Kind: + res = controller.HTTPRoutesResource.GroupResource() + } + err = kuadrant.NewErrPolicyTargetNotFound(kuadrantv1beta3.AuthPolicyGroupKind.Kind, ref, apierrors.NewNotFound(res, ref.GetName())) + } + return policy.GetLocator(), err + })) + + return nil +} diff --git a/controllers/auth_policy_status_updater.go b/controllers/auth_policy_status_updater.go new file mode 100644 index 000000000..81715210e --- /dev/null +++ b/controllers/auth_policy_status_updater.go @@ -0,0 +1,241 @@ +package controllers + +import ( + "context" + "fmt" + "slices" + "sync" + + envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + authorinooperatorv1beta1 "github.com/kuadrant/authorino-operator/api/v1beta1" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8stypes "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "k8s.io/utils/ptr" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + kuadrantv1 "github.com/kuadrant/kuadrant-operator/api/v1" + kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + "github.com/kuadrant/kuadrant-operator/pkg/common" + kuadrantenvoygateway "github.com/kuadrant/kuadrant-operator/pkg/envoygateway" + kuadrantistio "github.com/kuadrant/kuadrant-operator/pkg/istio" + kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" +) + +type AuthPolicyStatusUpdater struct { + client *dynamic.DynamicClient +} + +// AuthPolicyStatusUpdater reconciles to events with impact to change the status of AuthPolicy resources +func (r *AuthPolicyStatusUpdater) Subscription() controller.Subscription { + return controller.Subscription{ + ReconcileFunc: r.UpdateStatus, + Events: []controller.ResourceEventMatcher{ + {Kind: &kuadrantv1beta1.KuadrantGroupKind}, + {Kind: &machinery.GatewayClassGroupKind}, + {Kind: &machinery.GatewayGroupKind}, + {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.AuthPolicyGroupKind}, + {Kind: &kuadrantv1beta1.AuthConfigGroupKind}, + {Kind: &kuadrantistio.EnvoyFilterGroupKind}, + {Kind: &kuadrantistio.WasmPluginGroupKind}, + {Kind: &kuadrantenvoygateway.EnvoyPatchPolicyGroupKind}, + {Kind: &kuadrantenvoygateway.EnvoyExtensionPolicyGroupKind}, + }, + } +} + +func (r *AuthPolicyStatusUpdater) UpdateStatus(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("AuthPolicyStatusUpdater") + + policies := lo.FilterMap(topology.Policies().Items(), func(item machinery.Policy, index int) (*kuadrantv1beta3.AuthPolicy, bool) { + p, ok := item.(*kuadrantv1beta3.AuthPolicy) + return p, ok + }) + + policyAcceptedFunc := authPolicyAcceptedStatusFunc(state) + + logger.V(1).Info("updating authpolicy statuses", "policies", len(policies)) + defer logger.V(1).Info("finished updating authpolicy statuses") + + for _, policy := range policies { + if policy.GetDeletionTimestamp() != nil { + logger.V(1).Info("authpolicy is marked for deletion, skipping", "name", policy.Name, "namespace", policy.Namespace) + continue + } + + // copy initial conditions, otherwise status will always be updated + newStatus := &kuadrantv1beta3.AuthPolicyStatus{ + Conditions: slices.Clone(policy.Status.Conditions), + ObservedGeneration: policy.Status.ObservedGeneration, + } + + accepted, err := policyAcceptedFunc(policy) + meta.SetStatusCondition(&newStatus.Conditions, *kuadrant.AcceptedCondition(policy, err)) + + // do not set enforced condition if Accepted condition is false + if !accepted { + meta.RemoveStatusCondition(&newStatus.Conditions, string(kuadrant.PolicyConditionEnforced)) + } else { + enforcedCond := r.enforcedCondition(policy, topology, state) + meta.SetStatusCondition(&newStatus.Conditions, *enforcedCond) + } + + equalStatus := equality.Semantic.DeepEqual(newStatus, policy.Status) + if equalStatus && policy.Generation == policy.Status.ObservedGeneration { + logger.V(1).Info("policy status unchanged, skipping update") + continue + } + newStatus.ObservedGeneration = policy.Generation + policy.Status = *newStatus + + obj, err := controller.Destruct(policy) + if err != nil { + logger.Error(err, "unable to destruct policy") // should never happen + continue + } + + _, err = r.client.Resource(kuadrantv1beta3.AuthPoliciesResource).Namespace(policy.GetNamespace()).UpdateStatus(ctx, obj, metav1.UpdateOptions{}) + if err != nil { + logger.Error(err, "unable to update status for authpolicy", "name", policy.GetName(), "namespace", policy.GetNamespace()) + // TODO: handle error + } + } + + return nil +} + +func (r *AuthPolicyStatusUpdater) enforcedCondition(policy *kuadrantv1beta3.AuthPolicy, topology *machinery.Topology, state *sync.Map) *metav1.Condition { + policyKind := kuadrantv1beta3.AuthPolicyGroupKind.Kind + + effectivePolicies, ok := state.Load(StateEffectiveAuthPolicies) + if !ok { + return kuadrant.EnforcedCondition(policy, kuadrant.NewErrUnknown(policyKind, ErrMissingStateEffectiveAuthPolicies), false) + } + + // check the state of the rules of the policy in the effective policies + policyRuleKeys := lo.Keys(policy.Rules()) + affectedPaths := map[string][][]machinery.Targetable{} // policyRuleKey → topological paths affected by the policy rule + overridingPolicies := map[string][]string{} // policyRuleKey → locators of policies overriding the policy rule + for _, effectivePolicy := range effectivePolicies.(EffectiveAuthPolicies) { + if len(kuadrantv1.PoliciesInPath(effectivePolicy.Path, func(p machinery.Policy) bool { return p.GetLocator() == policy.GetLocator() })) == 0 { + continue + } + gatewayClass, gateway, listener, httpRoute, _, _ := common.ObjectsInRequestPath(effectivePolicy.Path) + if !kuadrantgatewayapi.IsListenerReady(listener.Listener, gateway.Gateway) || !kuadrantgatewayapi.IsHTTPRouteReady(httpRoute.HTTPRoute, gateway.Gateway, gatewayClass.GatewayClass.Spec.ControllerName) { + continue + } + effectivePolicyRules := effectivePolicy.Spec.Rules() + for _, policyRuleKey := range policyRuleKeys { + if effectivePolicyRule, ok := effectivePolicyRules[policyRuleKey]; !ok || (ok && effectivePolicyRule.GetSource() != policy.GetLocator()) { + var overriddenBy string + if ok { // TODO(guicassolato): !ok → we cannot tell which policy is overriding the rule, this information is lost when the policy rule is dropped during an atomic override + overriddenBy = effectivePolicyRule.GetSource() + } + overridingPolicies[policyRuleKey] = append(overridingPolicies[policyRuleKey], overriddenBy) + continue + } + if affectedPaths[policyRuleKey] == nil { + affectedPaths[policyRuleKey] = [][]machinery.Targetable{} + } + affectedPaths[policyRuleKey] = append(affectedPaths[policyRuleKey], effectivePolicy.Path) + } + } + + // no rules of the policy found in the effective policies + if len(affectedPaths) == 0 { + // no rules of the policy have been overridden by any other policy + if len(overridingPolicies) == 0 { + return kuadrant.EnforcedCondition(policy, kuadrant.NewErrNoRoutes(policyKind), false) + } + // all rules of the policy have been overridden by at least one other policy + overridingPoliciesKeys := lo.FilterMap(lo.Uniq(lo.Flatten(lo.Values(overridingPolicies))), func(policyLocator string, _ int) (k8stypes.NamespacedName, bool) { + policyKey, err := common.NamespacedNameFromLocator(policyLocator) + return policyKey, err == nil + }) + return kuadrant.EnforcedCondition(policy, kuadrant.NewErrOverridden(policyKind, overridingPoliciesKeys), false) + } + + var componentsToSync []string + + // check the status of Authorino + authorino, err := GetAuthorinoFromTopology(topology) + if err != nil { + return kuadrant.EnforcedCondition(policy, kuadrant.NewErrUnknown(policyKind, err), false) + } + if !meta.IsStatusConditionTrue(lo.Map(authorino.Status.Conditions, authorinoConditionToProperConditionFunc), string(authorinooperatorv1beta1.ConditionReady)) { + componentsToSync = append(componentsToSync, kuadrantv1beta1.AuthorinoGroupKind.Kind) + } + + // TODO: check status of the authconfig + + type affectedGateway struct { + gateway *machinery.Gateway + gatewayClass *machinery.GatewayClass + } + + // check the status of the gateways' configuration resources + affectedGateways := lo.UniqBy(lo.Map(lo.Flatten(lo.Values(affectedPaths)), func(path []machinery.Targetable, _ int) affectedGateway { + gatewayClass, gateway, _, _, _, _ := common.ObjectsInRequestPath(path) + return affectedGateway{ + gateway: gateway, + gatewayClass: gatewayClass, + } + }), func(g affectedGateway) string { + return g.gateway.GetLocator() + }) + for _, g := range affectedGateways { + switch g.gatewayClass.Spec.ControllerName { + case istioGatewayControllerName: + // EnvoyFilter + istioAuthClustersModifiedGateways, _ := state.Load(StateIstioAuthClustersModified) + componentsToSync = append(componentsToSync, gatewayComponentsToSync(g.gateway, kuadrantistio.EnvoyFilterGroupKind, istioAuthClustersModifiedGateways, topology, func(obj machinery.Object) bool { + // return meta.IsStatusConditionTrue(lo.Map(obj.(*controller.RuntimeObject).Object.(*istioclientgonetworkingv1alpha3.EnvoyFilter).Status.Conditions, kuadrantistio.ConditionToProperConditionFunc), "Ready") + return true // Istio won't ever populate the status stanza of EnvoyFilter resources, so we cannot expect to find a given a condition there + })...) + // WasmPlugin + istioExtensionsModifiedGateways, _ := state.Load(StateIstioExtensionsModified) + componentsToSync = append(componentsToSync, gatewayComponentsToSync(g.gateway, kuadrantistio.WasmPluginGroupKind, istioExtensionsModifiedGateways, topology, func(obj machinery.Object) bool { + // return meta.IsStatusConditionTrue(lo.Map(obj.(*controller.RuntimeObject).Object.(*istioclientgoextensionv1alpha1.WasmPlugin).Status.Conditions, kuadrantistio.ConditionToProperConditionFunc), "Ready") + return true // Istio won't ever populate the status stanza of WasmPlugin resources, so we cannot expect to find a given a condition there + })...) + case envoyGatewayGatewayControllerName: + gatewayAncestor := gatewayapiv1.ParentReference{Name: gatewayapiv1.ObjectName(g.gateway.GetName()), Namespace: ptr.To(gatewayapiv1.Namespace(g.gateway.GetNamespace()))} + // EnvoyPatchPolicy + envoyGatewayAuthClustersModifiedGateways, _ := state.Load(StateEnvoyGatewayAuthClustersModified) + componentsToSync = append(componentsToSync, gatewayComponentsToSync(g.gateway, kuadrantenvoygateway.EnvoyPatchPolicyGroupKind, envoyGatewayAuthClustersModifiedGateways, topology, func(obj machinery.Object) bool { + return meta.IsStatusConditionTrue(kuadrantgatewayapi.PolicyStatusConditionsFromAncestor(obj.(*controller.RuntimeObject).Object.(*envoygatewayv1alpha1.EnvoyPatchPolicy).Status, envoyGatewayGatewayControllerName, gatewayAncestor, gatewayapiv1.Namespace(obj.GetNamespace())), string(envoygatewayv1alpha1.PolicyConditionProgrammed)) + })...) + // EnvoyExtensionPolicy + envoyGatewayExtensionsModifiedGateways, _ := state.Load(StateEnvoyGatewayExtensionsModified) + componentsToSync = append(componentsToSync, gatewayComponentsToSync(g.gateway, kuadrantenvoygateway.EnvoyExtensionPolicyGroupKind, envoyGatewayExtensionsModifiedGateways, topology, func(obj machinery.Object) bool { + return meta.IsStatusConditionTrue(kuadrantgatewayapi.PolicyStatusConditionsFromAncestor(obj.(*controller.RuntimeObject).Object.(*envoygatewayv1alpha1.EnvoyExtensionPolicy).Status, envoyGatewayGatewayControllerName, gatewayAncestor, gatewayapiv1.Namespace(obj.GetNamespace())), string(gatewayapiv1alpha2.PolicyConditionAccepted)) + })...) + default: + componentsToSync = append(componentsToSync, fmt.Sprintf("%s (%s/%s)", machinery.GatewayGroupKind.Kind, g.gateway.GetNamespace(), g.gateway.GetName())) + } + } + + if len(componentsToSync) > 0 { + return kuadrant.EnforcedCondition(policy, kuadrant.NewErrOutOfSync(policyKind, componentsToSync), false) + } + + return kuadrant.EnforcedCondition(policy, nil, len(overridingPolicies) == 0) +} + +func authorinoConditionToProperConditionFunc(condition authorinooperatorv1beta1.Condition, _ int) metav1.Condition { + return metav1.Condition{ + Type: string(condition.Type), + Status: metav1.ConditionStatus(condition.Status), + Reason: condition.Reason, + Message: condition.Message, + } +} diff --git a/controllers/auth_workflow.go b/controllers/auth_workflow.go deleted file mode 100644 index 8eaf5f251..000000000 --- a/controllers/auth_workflow.go +++ /dev/null @@ -1,7 +0,0 @@ -package controllers - -import "github.com/kuadrant/policy-machinery/controller" - -func NewAuthWorkflow() *controller.Workflow { - return &controller.Workflow{} -} diff --git a/controllers/auth_workflow_helpers.go b/controllers/auth_workflow_helpers.go new file mode 100644 index 000000000..f248cedc3 --- /dev/null +++ b/controllers/auth_workflow_helpers.go @@ -0,0 +1,184 @@ +package controllers + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "sync" + + authorinooperatorv1beta1 "github.com/kuadrant/authorino-operator/api/v1beta1" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + k8stypes "k8s.io/apimachinery/pkg/types" + gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/wasm" +) + +const authObjectLabelKey = "kuadrant.io/auth" + +var ( + StateAuthPolicyValid = "AuthPolicyValid" + StateEffectiveAuthPolicies = "EffectiveAuthPolicies" + StateIstioAuthClustersModified = "IstioAuthClustersModified" + StateEnvoyGatewayAuthClustersModified = "EnvoyGatewayAuthClustersModified" + + ErrMissingAuthorino = fmt.Errorf("missing authorino object in the topology") + ErrMissingStateEffectiveAuthPolicies = fmt.Errorf("missing auth effective policies stored in the reconciliation state") +) + +//+kubebuilder:rbac:groups=kuadrant.io,resources=authpolicies,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=kuadrant.io,resources=authpolicies/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=kuadrant.io,resources=authpolicies/finalizers,verbs=update +//+kubebuilder:rbac:groups=authorino.kuadrant.io,resources=authconfigs,verbs=get;list;watch;create;update;patch;delete + +func GetAuthorinoFromTopology(topology *machinery.Topology) (*authorinooperatorv1beta1.Authorino, error) { + kuadrant, err := GetKuadrantFromTopology(topology) + if err != nil { + return nil, err + } + + authorinoObj, found := lo.Find(topology.Objects().Children(kuadrant), func(child machinery.Object) bool { + return child.GroupVersionKind().GroupKind() == kuadrantv1beta1.AuthorinoGroupKind + }) + if !found { + return nil, ErrMissingAuthorino + } + + authorino := authorinoObj.(*controller.RuntimeObject).Object.(*authorinooperatorv1beta1.Authorino) + return authorino, nil +} + +func AuthObjectLabels() labels.Set { + m := KuadrantManagedObjectLabels() + m[authObjectLabelKey] = "true" + return m +} + +func AuthClusterName(gatewayName string) string { + return fmt.Sprintf("kuadrant-auth-%s", gatewayName) +} + +func authClusterPatch(host string, port int) map[string]any { + return map[string]any{ + "name": common.KuadrantAuthClusterName, + "type": "STRICT_DNS", + "connect_timeout": "1s", + "lb_policy": "ROUND_ROBIN", + "http2_protocol_options": map[string]any{}, + "load_assignment": map[string]any{ + "cluster_name": common.KuadrantAuthClusterName, + "endpoints": []map[string]any{ + { + "lb_endpoints": []map[string]any{ + { + "endpoint": map[string]any{ + "address": map[string]any{ + "socket_address": map[string]any{ + "address": host, + "port_value": port, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +type authorinoServiceInfo struct { + Host string + Port int32 +} + +func authorinoServiceInfoFromAuthorino(authorino *authorinooperatorv1beta1.Authorino) authorinoServiceInfo { + info := authorinoServiceInfo{ + Host: fmt.Sprintf("%s-authorino-authorization.%s.svc.cluster.local", authorino.GetName(), authorino.GetNamespace()), + Port: int32(50051), // default authorino grpc authorization service port + } + if p := authorino.Spec.Listener.Ports.GRPC; p != nil { + info.Port = int32(*p) + } else if p := authorino.Spec.Listener.Port; p != nil { + info.Port = int32(*p) + } + return info +} + +func authConfigNameForPath(pathID string) string { + hash := sha256.Sum256([]byte(pathID)) + return hex.EncodeToString(hash[:]) +} + +func buildWasmActionsForAuth(pathID string, effectivePolicy EffectiveAuthPolicy) []wasm.Action { + action := wasm.Action{ + ServiceName: wasm.AuthServiceName, + Scope: authConfigNameForPath(pathID), + } + spec := effectivePolicy.Spec.Spec.Proper() + if conditions := wasm.PredicatesFromWhenConditions(lo.FlatMap(spec.Conditions, func(pattern kuadrantv1beta3.MergeablePatternExpressionOrRef, _ int) []kuadrantv1beta3.WhenCondition { + return pattern.ToWhenConditions(spec.NamedPatterns) + })...); len(conditions) > 0 { + action.Conditions = conditions + } + return []wasm.Action{action} +} + +func isAuthPolicyAcceptedAndNotDeletedFunc(state *sync.Map) func(machinery.Policy) bool { + f := isAuthPolicyAcceptedFunc(state) + return func(policy machinery.Policy) bool { + p, object := policy.(metav1.Object) + return object && f(policy) && p.GetDeletionTimestamp() == nil + } +} + +func isAuthPolicyAcceptedFunc(state *sync.Map) func(machinery.Policy) bool { + f := authPolicyAcceptedStatusFunc(state) + return func(policy machinery.Policy) bool { + accepted, _ := f(policy) + return accepted + } +} + +func authPolicyAcceptedStatusFunc(state *sync.Map) func(policy machinery.Policy) (bool, error) { + validatedPolicies, validated := state.Load(StateAuthPolicyValid) + if !validated { + return authPolicyAcceptedStatus + } + validatedPoliciesMap := validatedPolicies.(map[string]error) + return func(policy machinery.Policy) (bool, error) { + err, validated := validatedPoliciesMap[policy.GetLocator()] + if validated { + return err == nil, err + } + return authPolicyAcceptedStatus(policy) + } +} + +func authPolicyAcceptedStatus(policy machinery.Policy) (accepted bool, err error) { + p, ok := policy.(*kuadrantv1beta3.AuthPolicy) + if !ok { + return + } + if condition := meta.FindStatusCondition(p.Status.Conditions, string(gatewayapiv1alpha2.PolicyConditionAccepted)); condition != nil { + accepted = condition.Status == metav1.ConditionTrue + if !accepted { + err = fmt.Errorf(condition.Message) + } + return + } + return +} + +// TODO: remove this function and replace all calls with the actual config name +func AuthConfigName(_ k8stypes.NamespacedName) string { + return "FIXME" +} diff --git a/controllers/authconfigs_reconciler.go b/controllers/authconfigs_reconciler.go new file mode 100644 index 000000000..e874e9a13 --- /dev/null +++ b/controllers/authconfigs_reconciler.go @@ -0,0 +1,479 @@ +package controllers + +import ( + "context" + "errors" + "fmt" + "reflect" + "strings" + "sync" + + authorinov1beta2 "github.com/kuadrant/authorino/api/v1beta2" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + k8stypes "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + + kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + "github.com/kuadrant/kuadrant-operator/pkg/common" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" +) + +type AuthConfigsReconciler struct { + client *dynamic.DynamicClient +} + +// AuthConfigsReconciler subscribes to events with potential to change Authorino AuthConfig custom resources +func (r *AuthConfigsReconciler) Subscription() controller.Subscription { + return controller.Subscription{ + ReconcileFunc: r.Reconcile, + Events: []controller.ResourceEventMatcher{ + {Kind: &kuadrantv1beta1.KuadrantGroupKind}, + {Kind: &machinery.GatewayClassGroupKind}, + {Kind: &machinery.GatewayGroupKind}, + {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.AuthPolicyGroupKind}, + {Kind: &kuadrantv1beta1.AuthConfigGroupKind}, + }, + } +} + +func (r *AuthConfigsReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("AuthConfigsReconciler") + + authorino, err := GetAuthorinoFromTopology(topology) + if err != nil { + if errors.Is(err, ErrMissingKuadrant) || errors.Is(err, ErrMissingAuthorino) { + logger.V(1).Info(err.Error()) + return nil + } + return err + } + authConfigsNamespace := authorino.GetNamespace() + + effectivePolicies, ok := state.Load(StateEffectiveAuthPolicies) + if !ok { + logger.Error(ErrMissingStateEffectiveAuthPolicies, "failed to build limitador limits") + return nil + } + effectivePoliciesMap := effectivePolicies.(EffectiveAuthPolicies) + + logger.V(1).Info("reconciling authconfig objects", "effectivePolicies", len(effectivePoliciesMap)) + defer logger.V(1).Info("finished reconciling authconfig objects") + + desiredAuthConfigs := make(map[k8stypes.NamespacedName]struct{}) + var modifiedAuthConfigs []string + + for pathID, effectivePolicy := range effectivePoliciesMap { + _, _, _, httpRoute, httpRouteRule, _ := common.ObjectsInRequestPath(effectivePolicy.Path) + httpRouteKey := k8stypes.NamespacedName{Name: httpRoute.GetName(), Namespace: httpRoute.GetNamespace()} + httpRouteRuleKey := httpRouteRule.Name + + authConfigName := authConfigNameForPath(pathID) + desiredAuthConfig, err := r.buildDesiredAuthConfig(effectivePolicy, authConfigName, authConfigsNamespace) + if err != nil { + logger.Error(err, "failed to build desired envoy filter") + continue + } + desiredAuthConfigs[k8stypes.NamespacedName{Name: desiredAuthConfig.GetName(), Namespace: desiredAuthConfig.GetNamespace()}] = struct{}{} + + resource := r.client.Resource(kuadrantv1beta1.AuthConfigsResource).Namespace(desiredAuthConfig.GetNamespace()) + + existingAuthConfigObj, found := lo.Find(topology.Objects().Children(httpRouteRule), func(child machinery.Object) bool { + return child.GroupVersionKind().GroupKind() == kuadrantv1beta1.AuthConfigGroupKind && child.GetName() == authConfigName && labels.Set(child.(*controller.RuntimeObject).GetLabels()).AsSelector().Matches(labels.Set(desiredAuthConfig.GetLabels())) + }) + + // create + if !found { + modifiedAuthConfigs = append(modifiedAuthConfigs, authConfigName) + desiredAuthConfigUnstructured, err := common.Destruct(desiredAuthConfig) + if err != nil { + logger.Error(err, "failed to destruct authconfig object", "httpRoute", httpRouteKey.String(), "httpRouteRule", httpRouteRuleKey, "authconfig", desiredAuthConfig) + continue + } + + if _, err = resource.Create(ctx, desiredAuthConfigUnstructured, metav1.CreateOptions{}); err != nil { + logger.Error(err, "failed to create authconfig object", "httpRoute", httpRouteKey.String(), "httpRouteRule", httpRouteRuleKey, "authconfig", desiredAuthConfigUnstructured.Object) + // TODO: handle error + } + continue + } + + existingAuthConfig := existingAuthConfigObj.(*controller.RuntimeObject).Object.(*authorinov1beta2.AuthConfig) + + if equalAuthConfigs(existingAuthConfig, desiredAuthConfig) { + logger.V(1).Info("authconfig object is up to date, nothing to do") + continue + } + + modifiedAuthConfigs = append(modifiedAuthConfigs, authConfigName) + + // delete + if utils.IsObjectTaggedToDelete(desiredAuthConfig) && !utils.IsObjectTaggedToDelete(existingAuthConfig) { + if err := resource.Delete(ctx, existingAuthConfig.GetName(), metav1.DeleteOptions{}); err != nil { + logger.Error(err, "failed to delete wasmplugin object", "httpRoute", httpRouteKey.String(), "httpRouteRule", httpRouteRuleKey, "authconfig", fmt.Sprintf("%s/%s", existingAuthConfig.GetNamespace(), existingAuthConfig.GetName())) + // TODO: handle error + } + continue + } + + // update + existingAuthConfig.Spec = desiredAuthConfig.Spec + + existingAuthConfigUnstructured, err := common.Destruct(existingAuthConfig) + if err != nil { + logger.Error(err, "failed to destruct authconfig object", "httpRoute", httpRouteKey.String(), "httpRouteRule", httpRouteRuleKey, "authconfig", existingAuthConfig) + continue + } + if _, err = resource.Update(ctx, existingAuthConfigUnstructured, metav1.UpdateOptions{}); err != nil { + logger.Error(err, "failed to update authconfig object", "httpRoute", httpRouteKey.String(), "httpRouteRule", httpRouteRuleKey, "authconfig", existingAuthConfigUnstructured.Object) + // TODO: handle error + } + } + + // cleanup authconfigs that are not in the effective policies + staleAuthConfigs := topology.Objects().Items(func(o machinery.Object) bool { + _, desired := desiredAuthConfigs[k8stypes.NamespacedName{Name: o.GetName(), Namespace: o.GetNamespace()}] + return o.GroupVersionKind().GroupKind() == kuadrantv1beta1.AuthConfigGroupKind && labels.Set(o.(*controller.RuntimeObject).GetLabels()).AsSelector().Matches(AuthObjectLabels()) && !desired + }) + for _, authConfig := range staleAuthConfigs { + if err := r.client.Resource(kuadrantv1beta1.AuthConfigsResource).Namespace(authConfig.GetNamespace()).Delete(ctx, authConfig.GetName(), metav1.DeleteOptions{}); err != nil { + logger.Error(err, "failed to delete authconfig object", "authconfig", fmt.Sprintf("%s/%s", authConfig.GetNamespace(), authConfig.GetName())) + // TODO: handle error + } + } + + return nil +} + +func (r *AuthConfigsReconciler) buildDesiredAuthConfig(effectivePolicy EffectiveAuthPolicy, name, namespace string) (*authorinov1beta2.AuthConfig, error) { + _, _, _, _, httpRouteRule, _ := common.ObjectsInRequestPath(effectivePolicy.Path) + + authConfig := &authorinov1beta2.AuthConfig{ + TypeMeta: metav1.TypeMeta{ + Kind: "AuthConfig", + APIVersion: authorinov1beta2.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: map[string]string{ + kuadrantv1beta1.AuthConfigHTTPRouteRuleAnnotation: httpRouteRule.GetLocator(), + }, + Labels: AuthObjectLabels(), + }, + Spec: authorinov1beta2.AuthConfigSpec{ + Hosts: []string{name}, + }, + } + + spec := effectivePolicy.Spec.Spec.Proper() + + // named patterns + if namedPatterns := spec.NamedPatterns; namedPatterns != nil { + authConfig.Spec.NamedPatterns = lo.MapValues(spec.NamedPatterns, func(v kuadrantv1beta3.MergeablePatternExpressions, _ string) authorinov1beta2.PatternExpressions { + return v.PatternExpressions + }) + } + + // top-level conditions + if conditions := spec.Conditions; conditions != nil { + authConfig.Spec.Conditions = lo.Map(spec.Conditions, func(v kuadrantv1beta3.MergeablePatternExpressionOrRef, _ int) authorinov1beta2.PatternExpressionOrRef { + return v.PatternExpressionOrRef + }) + } + + // return early if authScheme is nil + authScheme := spec.AuthScheme + if authScheme == nil { + return authConfig, nil + } + + // authentication + if authentication := authScheme.Authentication; authentication != nil { + authConfig.Spec.Authentication = lo.MapValues(authentication, func(v kuadrantv1beta3.MergeableAuthenticationSpec, _ string) authorinov1beta2.AuthenticationSpec { + return v.AuthenticationSpec + }) + } + + // metadata + if metadata := authScheme.Metadata; metadata != nil { + authConfig.Spec.Metadata = lo.MapValues(metadata, func(v kuadrantv1beta3.MergeableMetadataSpec, _ string) authorinov1beta2.MetadataSpec { + return v.MetadataSpec + }) + } + + // authorization + if authorization := authScheme.Authorization; authorization != nil { + authConfig.Spec.Authorization = lo.MapValues(authorization, func(v kuadrantv1beta3.MergeableAuthorizationSpec, _ string) authorinov1beta2.AuthorizationSpec { + return v.AuthorizationSpec + }) + } + + // response + if response := authScheme.Response; response != nil { + var unauthenticated *authorinov1beta2.DenyWithSpec + if response.Unauthenticated != nil { + unauthenticated = &response.Unauthenticated.DenyWithSpec + } + + var unauthorized *authorinov1beta2.DenyWithSpec + if response.Unauthorized != nil { + unauthorized = &response.Unauthorized.DenyWithSpec + } + + authConfig.Spec.Response = &authorinov1beta2.ResponseSpec{ + Unauthenticated: unauthenticated, + Unauthorized: unauthorized, + Success: authorinov1beta2.WrappedSuccessResponseSpec{ + Headers: authorinoSpecsFromConfigs(response.Success.Headers, func(config kuadrantv1beta3.MergeableHeaderSuccessResponseSpec) authorinov1beta2.HeaderSuccessResponseSpec { + return authorinov1beta2.HeaderSuccessResponseSpec{SuccessResponseSpec: config.SuccessResponseSpec} + }), + DynamicMetadata: authorinoSpecsFromConfigs(response.Success.DynamicMetadata, func(config kuadrantv1beta3.MergeableSuccessResponseSpec) authorinov1beta2.SuccessResponseSpec { + return config.SuccessResponseSpec + }), + }, + } + } + + // callbacks + if callbacks := authScheme.Callbacks; callbacks != nil { + authConfig.Spec.Callbacks = lo.MapValues(callbacks, func(v kuadrantv1beta3.MergeableCallbackSpec, _ string) authorinov1beta2.CallbackSpec { + return v.CallbackSpec + }) + } + + return authConfig, nil +} + +func authorinoSpecsFromConfigs[T, U any](configs map[string]U, extractAuthorinoSpec func(U) T) map[string]T { + specs := make(map[string]T, len(configs)) + for name, config := range configs { + authorinoConfig := extractAuthorinoSpec(config) + specs[name] = authorinoConfig + } + + if len(specs) == 0 { + return nil + } + + return specs +} + +func equalAuthConfigs(existing, desired *authorinov1beta2.AuthConfig) bool { + // httprouterule back ref annotation + existingAnnotations := existing.GetAnnotations() + desiredAnnotations := desired.GetAnnotations() + if existingAnnotations == nil || desiredAnnotations == nil || existingAnnotations[kuadrantv1beta1.AuthConfigHTTPRouteRuleAnnotation] != desiredAnnotations[kuadrantv1beta1.AuthConfigHTTPRouteRuleAnnotation] { + return false + } + + // labels + existingLabels := existing.GetLabels() + desiredLabels := desired.GetLabels() + if len(existingLabels) != len(desiredLabels) || !labels.Set(existingLabels).AsSelector().Matches(labels.Set(desiredLabels)) { + return false + } + + // spec + return reflect.DeepEqual(existing.Spec, desired.Spec) +} + +// TODO(guicassolato): remove these functions below if we decide not to build conditions from the HTTPRouteRule + hostnames + +// authorinoConditionsFromHTTPRouteRule builds a list of Authorino conditions from a HTTPRouteRule and a list of hostnames +// * Each combination of HTTPRouteMatch and hostname yields one condition. +// * Rules that specify no explicit HTTPRouteMatch are assumed to match all requests (i.e. implicit catch-all rule.) +// * Empty list of hostnames yields a condition without a hostname pattern expression. +func authorinoConditionsFromHTTPRouteRule(rule gatewayapiv1.HTTPRouteRule, hostnames []gatewayapiv1.Hostname) []authorinov1beta2.PatternExpressionOrRef { + hosts := []string{} + for _, hostname := range hostnames { + if hostname == "*" { + continue + } + hosts = append(hosts, string(hostname)) + } + + // no http route matches → we only need one simple authorino condition or even no condition at all + if len(rule.Matches) == 0 { + if len(hosts) == 0 { + return nil + } + return []authorinov1beta2.PatternExpressionOrRef{hostnameRuleToAuthorinoCondition(hosts)} + } + + var oneOf []authorinov1beta2.PatternExpressionOrRef + + // http route matches and possibly hostnames → we need one authorino rule per http route match + for _, match := range rule.Matches { + var allOf []authorinov1beta2.PatternExpressionOrRef + + // hosts + if len(hosts) > 0 { + allOf = append(allOf, hostnameRuleToAuthorinoCondition(hosts)) + } + + // method + if method := match.Method; method != nil { + allOf = append(allOf, httpMethodRuleToAuthorinoCondition(*method)) + } + + // path + if path := match.Path; path != nil { + allOf = append(allOf, httpPathRuleToAuthorinoCondition(*path)) + } + + // headers + if headers := match.Headers; len(headers) > 0 { + allOf = append(allOf, httpHeadersRuleToAuthorinoConditions(headers)...) + } + + // query params + if queryParams := match.QueryParams; len(queryParams) > 0 { + allOf = append(allOf, httpQueryParamsRuleToAuthorinoConditions(queryParams)...) + } + + if len(allOf) > 0 { + oneOf = append(oneOf, authorinov1beta2.PatternExpressionOrRef{ + All: utils.Map(allOf, toAuthorinoUnstructuredPatternExpressionOrRef), + }) + } + } + return toAuthorinoOneOfPatternExpressionsOrRefs(oneOf) +} + +func hostnameRuleToAuthorinoCondition(hostnames []string) authorinov1beta2.PatternExpressionOrRef { + return authorinov1beta2.PatternExpressionOrRef{ + PatternExpression: authorinov1beta2.PatternExpression{ + Selector: "request.host", + Operator: "matches", + Value: hostnamesToRegex(hostnames), + }, + } +} + +func hostnamesToRegex(hostnames []string) string { + return strings.Join(utils.Map(hostnames, func(hostname string) string { + return strings.ReplaceAll(strings.ReplaceAll(hostname, ".", `\.`), "*", ".*") + }), "|") +} + +func httpMethodRuleToAuthorinoCondition(method gatewayapiv1.HTTPMethod) authorinov1beta2.PatternExpressionOrRef { + return authorinov1beta2.PatternExpressionOrRef{ + PatternExpression: authorinov1beta2.PatternExpression{ + Selector: "request.method", + Operator: "eq", + Value: string(method), + }, + } +} + +func httpPathRuleToAuthorinoCondition(path gatewayapiv1.HTTPPathMatch) authorinov1beta2.PatternExpressionOrRef { + value := "/" + if path.Value != nil { + value = *path.Value + } + var operator string + + matchType := path.Type + if matchType == nil { + p := gatewayapiv1.PathMatchPathPrefix + matchType = &p // gateway api defaults to PathMatchPathPrefix + } + + switch *matchType { + case gatewayapiv1.PathMatchExact: + operator = "eq" + case gatewayapiv1.PathMatchPathPrefix: + operator = "matches" + value += ".*" + case gatewayapiv1.PathMatchRegularExpression: + operator = "matches" + } + + return authorinov1beta2.PatternExpressionOrRef{ + PatternExpression: authorinov1beta2.PatternExpression{ + Selector: `request.url_path`, + Operator: authorinov1beta2.PatternExpressionOperator(operator), + Value: value, + }, + } +} + +func httpHeadersRuleToAuthorinoConditions(headers []gatewayapiv1.HTTPHeaderMatch) []authorinov1beta2.PatternExpressionOrRef { + conditions := make([]authorinov1beta2.PatternExpressionOrRef, 0, len(headers)) + for _, header := range headers { + condition := httpHeaderRuleToAuthorinoCondition(header) + conditions = append(conditions, condition) + } + return conditions +} + +func httpHeaderRuleToAuthorinoCondition(header gatewayapiv1.HTTPHeaderMatch) authorinov1beta2.PatternExpressionOrRef { + operator := "eq" // gateway api defaults to HeaderMatchExact + if header.Type != nil && *header.Type == gatewayapiv1.HeaderMatchRegularExpression { + operator = "matches" + } + return authorinov1beta2.PatternExpressionOrRef{ + PatternExpression: authorinov1beta2.PatternExpression{ + Selector: fmt.Sprintf("request.headers.%s", strings.ToLower(string(header.Name))), + Operator: authorinov1beta2.PatternExpressionOperator(operator), + Value: header.Value, + }, + } +} + +func httpQueryParamsRuleToAuthorinoConditions(queryParams []gatewayapiv1.HTTPQueryParamMatch) []authorinov1beta2.PatternExpressionOrRef { + conditions := make([]authorinov1beta2.PatternExpressionOrRef, 0, len(queryParams)) + for _, queryParam := range queryParams { + condition := httpQueryParamRuleToAuthorinoCondition(queryParam) + conditions = append(conditions, condition) + } + return conditions +} + +func httpQueryParamRuleToAuthorinoCondition(queryParam gatewayapiv1.HTTPQueryParamMatch) authorinov1beta2.PatternExpressionOrRef { + operator := "eq" // gateway api defaults to QueryParamMatchExact + if queryParam.Type != nil && *queryParam.Type == gatewayapiv1.QueryParamMatchRegularExpression { + operator = "matches" + } + return authorinov1beta2.PatternExpressionOrRef{ + Any: []authorinov1beta2.UnstructuredPatternExpressionOrRef{ + { + PatternExpressionOrRef: authorinov1beta2.PatternExpressionOrRef{ + PatternExpression: authorinov1beta2.PatternExpression{ + Selector: fmt.Sprintf(`request.path.@extract:{"sep":"?%s=","pos":1}|@extract:{"sep":"&"}`, queryParam.Name), + Operator: authorinov1beta2.PatternExpressionOperator(operator), + Value: queryParam.Value, + }, + }, + }, + { + PatternExpressionOrRef: authorinov1beta2.PatternExpressionOrRef{ + PatternExpression: authorinov1beta2.PatternExpression{ + Selector: fmt.Sprintf(`request.path.@extract:{"sep":"&%s=","pos":1}|@extract:{"sep":"&"}`, queryParam.Name), + Operator: authorinov1beta2.PatternExpressionOperator(operator), + Value: queryParam.Value, + }, + }, + }, + }, + } +} + +func toAuthorinoUnstructuredPatternExpressionOrRef(patternExpressionOrRef authorinov1beta2.PatternExpressionOrRef) authorinov1beta2.UnstructuredPatternExpressionOrRef { + return authorinov1beta2.UnstructuredPatternExpressionOrRef{PatternExpressionOrRef: patternExpressionOrRef} +} + +func toAuthorinoOneOfPatternExpressionsOrRefs(oneOf []authorinov1beta2.PatternExpressionOrRef) []authorinov1beta2.PatternExpressionOrRef { + return []authorinov1beta2.PatternExpressionOrRef{ + { + Any: utils.Map(oneOf, toAuthorinoUnstructuredPatternExpressionOrRef), + }, + } +} diff --git a/controllers/authpolicy_authconfig.go b/controllers/authpolicy_authconfig.go deleted file mode 100644 index bd408ffba..000000000 --- a/controllers/authpolicy_authconfig.go +++ /dev/null @@ -1,492 +0,0 @@ -package controllers - -import ( - "context" - "fmt" - "reflect" - "slices" - "strings" - - "github.com/go-logr/logr" - authorinoapi "github.com/kuadrant/authorino/api/v1beta2" - "github.com/samber/lo" - 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" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - "github.com/kuadrant/kuadrant-operator/pkg/common" - kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" - "github.com/kuadrant/kuadrant-operator/pkg/library/utils" -) - -func (r *AuthPolicyReconciler) reconcileAuthConfigs(ctx context.Context, ap *kuadrantv1beta3.AuthPolicy, targetNetworkObject client.Object) error { - logger, err := logr.FromContext(ctx) - if err != nil { - return err - } - - authConfig, err := r.desiredAuthConfig(ctx, ap, targetNetworkObject) - if err != nil { - return err - } - - err = r.SetOwnerReference(ap, authConfig) - if err != nil { - return err - } - - err = r.ReconcileResource(ctx, &authorinoapi.AuthConfig{}, authConfig, authConfigBasicMutator) - if err != nil && !apierrors.IsAlreadyExists(err) { - logger.Error(err, "ReconcileResource failed to create/update AuthConfig resource") - return err - } - return nil -} - -func (r *AuthPolicyReconciler) desiredAuthConfig(ctx context.Context, ap *kuadrantv1beta3.AuthPolicy, targetNetworkObject client.Object) (*authorinoapi.AuthConfig, error) { - logger, _ := logr.FromContext(ctx) - logger = logger.WithName("desiredAuthConfig") - - authConfig := &authorinoapi.AuthConfig{ - TypeMeta: metav1.TypeMeta{ - Kind: "AuthConfig", - APIVersion: authorinoapi.GroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: AuthConfigName(client.ObjectKeyFromObject(ap)), - Namespace: ap.Namespace, - }, - Spec: authorinoapi.AuthConfigSpec{}, - } - - var route *gatewayapiv1.HTTPRoute - var hosts []string - - switch obj := targetNetworkObject.(type) { - case *gatewayapiv1.HTTPRoute: - t, err := r.generateTopology(ctx) - if err != nil { - logger.V(1).Info("Failed to generate topology", "error", err) - return nil, err - } - - overrides := routeGatewayAuthOverrides(t, ap) - if len(overrides) != 0 { - logger.V(1).Info("targeted gateway has authpolicy with atomic overrides, skipping authorino authconfig for the HTTPRoute authpolicy") - utils.TagObjectToDelete(authConfig) - r.AffectedPolicyMap.SetAffectedPolicy(ap, overrides) - return authConfig, nil - } - route = obj - 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 := kuadrant.GatewayWrapper{Gateway: obj} - gwHostnames := gw.Hostnames() - if len(gwHostnames) == 0 { - gwHostnames = []gatewayapiv1.Hostname{"*"} - } - hosts = utils.HostnamesToStrings(gwHostnames) - - rules := make([]gatewayapiv1.HTTPRouteRule, 0) - routes := r.TargetRefReconciler.FetchAcceptedGatewayHTTPRoutes(ctx, obj) - for idx := range routes { - route := routes[idx] - // skip routes that have an authpolicy of its own and Gateway authpolicy does not define atomic overrides - if route.GetAnnotations()[common.AuthPolicyBackRefAnnotation] != "" && !ap.IsAtomicOverride() { - continue - } - rules = append(rules, route.Spec.Rules...) - } - if len(rules) == 0 { - logger.V(1).Info("no httproutes attached to the targeted gateway, skipping authorino authconfig for the gateway authpolicy") - utils.TagObjectToDelete(authConfig) - obj := targetNetworkObject.(*gatewayapiv1.Gateway) - gatewayWrapper := kuadrant.GatewayWrapper{Gateway: obj, Referrer: ap} - refs := gatewayWrapper.PolicyRefs() - filteredRef := utils.Filter(refs, func(key client.ObjectKey) bool { - return key != client.ObjectKeyFromObject(ap) - }) - - r.AffectedPolicyMap.SetAffectedPolicy(ap, filteredRef) - return authConfig, nil - } - route = &gatewayapiv1.HTTPRoute{ - Spec: gatewayapiv1.HTTPRouteSpec{ - Hostnames: gwHostnames, - Rules: rules, - }, - } - } - - // AuthPolicy is not Affected if we still need to create an AuthConfig for it - r.AffectedPolicyMap.RemoveAffectedPolicy(ap) - - // hosts - authConfig.Spec.Hosts = hosts - - commonSpec := ap.Spec.Proper() - - // named patterns - if namedPatterns := commonSpec.NamedPatterns; len(namedPatterns) > 0 { - authConfig.Spec.NamedPatterns = make(map[string]authorinoapi.PatternExpressions, len(namedPatterns)) - for name, pattern := range namedPatterns { - authConfig.Spec.NamedPatterns[name] = pattern.PatternExpressions - } - } - - conditionsFromHTTPRoute := authorinoConditionsFromHTTPRoute(route) - if len(conditionsFromHTTPRoute) > 0 || len(commonSpec.Conditions) > 0 { - authConfig.Spec.Conditions = append(lo.Map(commonSpec.Conditions, func(c kuadrantv1beta3.MergeablePatternExpressionOrRef, _ int) authorinoapi.PatternExpressionOrRef { - return c.PatternExpressionOrRef - }), conditionsFromHTTPRoute...) - } - - // return early if authScheme is nil - if commonSpec.AuthScheme == nil { - return authConfig, nil - } - - // authentication - if authentication := commonSpec.AuthScheme.Authentication; len(authentication) > 0 { - authConfig.Spec.Authentication = authorinoSpecsFromConfigs(authentication, func(config kuadrantv1beta3.MergeableAuthenticationSpec) authorinoapi.AuthenticationSpec { - return config.AuthenticationSpec - }) - } - - // metadata - if metadata := commonSpec.AuthScheme.Metadata; len(metadata) > 0 { - authConfig.Spec.Metadata = authorinoSpecsFromConfigs(metadata, func(config kuadrantv1beta3.MergeableMetadataSpec) authorinoapi.MetadataSpec { - return config.MetadataSpec - }) - } - - // authorization - if authorization := commonSpec.AuthScheme.Authorization; len(authorization) > 0 { - authConfig.Spec.Authorization = authorinoSpecsFromConfigs(authorization, func(config kuadrantv1beta3.MergeableAuthorizationSpec) authorinoapi.AuthorizationSpec { - return config.AuthorizationSpec - }) - } - - // response - if response := commonSpec.AuthScheme.Response; response != nil { - var unauthenticated *authorinoapi.DenyWithSpec - if response.Unauthenticated != nil { - unauthenticated = &response.Unauthenticated.DenyWithSpec - } - - var unauthorized *authorinoapi.DenyWithSpec - if response.Unauthorized != nil { - unauthorized = &response.Unauthorized.DenyWithSpec - } - - authConfig.Spec.Response = &authorinoapi.ResponseSpec{ - Unauthenticated: unauthenticated, - Unauthorized: unauthorized, - Success: authorinoapi.WrappedSuccessResponseSpec{ - Headers: authorinoSpecsFromConfigs(response.Success.Headers, func(config kuadrantv1beta3.MergeableHeaderSuccessResponseSpec) authorinoapi.HeaderSuccessResponseSpec { - return authorinoapi.HeaderSuccessResponseSpec{SuccessResponseSpec: config.SuccessResponseSpec} - }), - DynamicMetadata: authorinoSpecsFromConfigs(response.Success.DynamicMetadata, func(config kuadrantv1beta3.MergeableSuccessResponseSpec) authorinoapi.SuccessResponseSpec { - return config.SuccessResponseSpec - }), - }, - } - } - - // callbacks - if callbacks := commonSpec.AuthScheme.Callbacks; len(callbacks) > 0 { - authConfig.Spec.Callbacks = authorinoSpecsFromConfigs(callbacks, func(config kuadrantv1beta3.MergeableCallbackSpec) authorinoapi.CallbackSpec { - return config.CallbackSpec - }) - } - - return authConfig, nil -} - -// routeGatewayAuthOverrides returns the GW auth policies that has an override field set -func routeGatewayAuthOverrides(t *kuadrantgatewayapi.Topology, ap *kuadrantv1beta3.AuthPolicy) []client.ObjectKey { - affectedPolicies := getAffectedPolicies(t, ap) - - // Filter the policies where: - // 1. targets a gateway - // 2. is not the current AP that is being assessed - // 3. is an overriding policy - // 4. is not marked for deletion - affectedPolicies = utils.Filter(affectedPolicies, func(policy kuadrantgatewayapi.Policy) bool { - p, ok := policy.(*kuadrantv1beta3.AuthPolicy) - return ok && - p.DeletionTimestamp == nil && - kuadrantgatewayapi.IsTargetRefGateway(policy.GetTargetRef()) && - ap.GetUID() != policy.GetUID() && - p.IsAtomicOverride() - }) - - return utils.Map(affectedPolicies, func(policy kuadrantgatewayapi.Policy) client.ObjectKey { - return client.ObjectKeyFromObject(policy) - }) -} - -func getAffectedPolicies(t *kuadrantgatewayapi.Topology, ap *kuadrantv1beta3.AuthPolicy) []kuadrantgatewayapi.Policy { - topologyIndexes := kuadrantgatewayapi.NewTopologyIndexes(t) - var affectedPolicies []kuadrantgatewayapi.Policy - - // If AP is listed within the policies from gateway, it potentially can be overridden by it - for _, gw := range t.Gateways() { - policyList := topologyIndexes.PoliciesFromGateway(gw.Gateway) - if slices.Contains(utils.Map(policyList, func(p kuadrantgatewayapi.Policy) client.ObjectKey { - return client.ObjectKeyFromObject(p) - }), client.ObjectKeyFromObject(ap)) { - affectedPolicies = append(affectedPolicies, policyList...) - } - } - - return affectedPolicies -} - -// AuthConfigName returns the name of Authorino AuthConfig CR. -func AuthConfigName(apKey client.ObjectKey) string { - return fmt.Sprintf("ap-%s-%s", apKey.Namespace, apKey.Name) -} - -func authorinoSpecsFromConfigs[T, U any](configs map[string]U, extractAuthorinoSpec func(U) T) map[string]T { - specs := make(map[string]T, len(configs)) - for name, config := range configs { - authorinoConfig := extractAuthorinoSpec(config) - specs[name] = authorinoConfig - } - - if len(specs) == 0 { - return nil - } - - return specs -} - -// authorinoConditionsFromHTTPRoute builds a list of Authorino conditions from an HTTPRoute, without using route selectors. -func authorinoConditionsFromHTTPRoute(route *gatewayapiv1.HTTPRoute) []authorinoapi.PatternExpressionOrRef { - conditions := []authorinoapi.PatternExpressionOrRef{} - hostnamesForConditions := []gatewayapiv1.Hostname{"*"} - for _, rule := range route.Spec.Rules { - conditions = append(conditions, authorinoConditionsFromHTTPRouteRule(rule, hostnamesForConditions)...) - } - return toAuthorinoOneOfPatternExpressionsOrRefs(conditions) -} - -// authorinoConditionsFromHTTPRouteRule builds a list of Authorino conditions from a HTTPRouteRule and a list of hostnames -// * Each combination of HTTPRouteMatch and hostname yields one condition. -// * Rules that specify no explicit HTTPRouteMatch are assumed to match all requests (i.e. implicit catch-all rule.) -// * Empty list of hostnames yields a condition without a hostname pattern expression. -func authorinoConditionsFromHTTPRouteRule(rule gatewayapiv1.HTTPRouteRule, hostnames []gatewayapiv1.Hostname) []authorinoapi.PatternExpressionOrRef { - hosts := []string{} - for _, hostname := range hostnames { - if hostname == "*" { - continue - } - hosts = append(hosts, string(hostname)) - } - - // no http route matches → we only need one simple authorino condition or even no condition at all - if len(rule.Matches) == 0 { - if len(hosts) == 0 { - return nil - } - return []authorinoapi.PatternExpressionOrRef{hostnameRuleToAuthorinoCondition(hosts)} - } - - var oneOf []authorinoapi.PatternExpressionOrRef - - // http route matches and possibly hostnames → we need one authorino rule per http route match - for _, match := range rule.Matches { - var allOf []authorinoapi.PatternExpressionOrRef - - // hosts - if len(hosts) > 0 { - allOf = append(allOf, hostnameRuleToAuthorinoCondition(hosts)) - } - - // method - if method := match.Method; method != nil { - allOf = append(allOf, httpMethodRuleToAuthorinoCondition(*method)) - } - - // path - if path := match.Path; path != nil { - allOf = append(allOf, httpPathRuleToAuthorinoCondition(*path)) - } - - // headers - if headers := match.Headers; len(headers) > 0 { - allOf = append(allOf, httpHeadersRuleToAuthorinoConditions(headers)...) - } - - // query params - if queryParams := match.QueryParams; len(queryParams) > 0 { - allOf = append(allOf, httpQueryParamsRuleToAuthorinoConditions(queryParams)...) - } - - if len(allOf) > 0 { - oneOf = append(oneOf, authorinoapi.PatternExpressionOrRef{ - All: utils.Map(allOf, toAuthorinoUnstructuredPatternExpressionOrRef), - }) - } - } - return toAuthorinoOneOfPatternExpressionsOrRefs(oneOf) -} - -func hostnameRuleToAuthorinoCondition(hostnames []string) authorinoapi.PatternExpressionOrRef { - return authorinoapi.PatternExpressionOrRef{ - PatternExpression: authorinoapi.PatternExpression{ - Selector: "request.host", - Operator: "matches", - Value: hostnamesToRegex(hostnames), - }, - } -} - -func hostnamesToRegex(hostnames []string) string { - return strings.Join(utils.Map(hostnames, func(hostname string) string { - return strings.ReplaceAll(strings.ReplaceAll(hostname, ".", `\.`), "*", ".*") - }), "|") -} - -func httpMethodRuleToAuthorinoCondition(method gatewayapiv1.HTTPMethod) authorinoapi.PatternExpressionOrRef { - return authorinoapi.PatternExpressionOrRef{ - PatternExpression: authorinoapi.PatternExpression{ - Selector: "request.method", - Operator: "eq", - Value: string(method), - }, - } -} - -func httpPathRuleToAuthorinoCondition(path gatewayapiv1.HTTPPathMatch) authorinoapi.PatternExpressionOrRef { - value := "/" - if path.Value != nil { - value = *path.Value - } - var operator string - - matchType := path.Type - if matchType == nil { - p := gatewayapiv1.PathMatchPathPrefix - matchType = &p // gateway api defaults to PathMatchPathPrefix - } - - switch *matchType { - case gatewayapiv1.PathMatchExact: - operator = "eq" - case gatewayapiv1.PathMatchPathPrefix: - operator = "matches" - value += ".*" - case gatewayapiv1.PathMatchRegularExpression: - operator = "matches" - } - - return authorinoapi.PatternExpressionOrRef{ - PatternExpression: authorinoapi.PatternExpression{ - Selector: `request.url_path`, - Operator: authorinoapi.PatternExpressionOperator(operator), - Value: value, - }, - } -} - -func httpHeadersRuleToAuthorinoConditions(headers []gatewayapiv1.HTTPHeaderMatch) []authorinoapi.PatternExpressionOrRef { - conditions := make([]authorinoapi.PatternExpressionOrRef, 0, len(headers)) - for _, header := range headers { - condition := httpHeaderRuleToAuthorinoCondition(header) - conditions = append(conditions, condition) - } - return conditions -} - -func httpHeaderRuleToAuthorinoCondition(header gatewayapiv1.HTTPHeaderMatch) authorinoapi.PatternExpressionOrRef { - operator := "eq" // gateway api defaults to HeaderMatchExact - if header.Type != nil && *header.Type == gatewayapiv1.HeaderMatchRegularExpression { - operator = "matches" - } - return authorinoapi.PatternExpressionOrRef{ - PatternExpression: authorinoapi.PatternExpression{ - Selector: fmt.Sprintf("request.headers.%s", strings.ToLower(string(header.Name))), - Operator: authorinoapi.PatternExpressionOperator(operator), - Value: header.Value, - }, - } -} - -func httpQueryParamsRuleToAuthorinoConditions(queryParams []gatewayapiv1.HTTPQueryParamMatch) []authorinoapi.PatternExpressionOrRef { - conditions := make([]authorinoapi.PatternExpressionOrRef, 0, len(queryParams)) - for _, queryParam := range queryParams { - condition := httpQueryParamRuleToAuthorinoCondition(queryParam) - conditions = append(conditions, condition) - } - return conditions -} - -func httpQueryParamRuleToAuthorinoCondition(queryParam gatewayapiv1.HTTPQueryParamMatch) authorinoapi.PatternExpressionOrRef { - operator := "eq" // gateway api defaults to QueryParamMatchExact - if queryParam.Type != nil && *queryParam.Type == gatewayapiv1.QueryParamMatchRegularExpression { - operator = "matches" - } - return authorinoapi.PatternExpressionOrRef{ - Any: []authorinoapi.UnstructuredPatternExpressionOrRef{ - { - PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ - PatternExpression: authorinoapi.PatternExpression{ - Selector: fmt.Sprintf(`request.path.@extract:{"sep":"?%s=","pos":1}|@extract:{"sep":"&"}`, queryParam.Name), - Operator: authorinoapi.PatternExpressionOperator(operator), - Value: queryParam.Value, - }, - }, - }, - { - PatternExpressionOrRef: authorinoapi.PatternExpressionOrRef{ - PatternExpression: authorinoapi.PatternExpression{ - Selector: fmt.Sprintf(`request.path.@extract:{"sep":"&%s=","pos":1}|@extract:{"sep":"&"}`, queryParam.Name), - Operator: authorinoapi.PatternExpressionOperator(operator), - Value: queryParam.Value, - }, - }, - }, - }, - } -} - -func toAuthorinoUnstructuredPatternExpressionOrRef(patternExpressionOrRef authorinoapi.PatternExpressionOrRef) authorinoapi.UnstructuredPatternExpressionOrRef { - return authorinoapi.UnstructuredPatternExpressionOrRef{PatternExpressionOrRef: patternExpressionOrRef} -} - -func toAuthorinoOneOfPatternExpressionsOrRefs(oneOf []authorinoapi.PatternExpressionOrRef) []authorinoapi.PatternExpressionOrRef { - return []authorinoapi.PatternExpressionOrRef{ - { - Any: utils.Map(oneOf, toAuthorinoUnstructuredPatternExpressionOrRef), - }, - } -} - -func authConfigBasicMutator(existingObj, desiredObj client.Object) (bool, error) { - existing, ok := existingObj.(*authorinoapi.AuthConfig) - if !ok { - return false, fmt.Errorf("%T is not an *authorinoapi.AuthConfig", existingObj) - } - desired, ok := desiredObj.(*authorinoapi.AuthConfig) - if !ok { - return false, fmt.Errorf("%T is not an *authorinoapi.AuthConfig", desiredObj) - } - - if reflect.DeepEqual(existing.Spec, desired.Spec) { - return false, nil - } - - existing.Spec = desired.Spec - - return true, nil -} diff --git a/controllers/authpolicy_controller.go b/controllers/authpolicy_controller.go deleted file mode 100644 index b0c7e5580..000000000 --- a/controllers/authpolicy_controller.go +++ /dev/null @@ -1,242 +0,0 @@ -package controllers - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/go-logr/logr" - 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" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" - "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" -) - -const authPolicyFinalizer = "authpolicy.kuadrant.io/finalizer" - -// AuthPolicyReconciler reconciles a AuthPolicy object -type AuthPolicyReconciler struct { - *reconcilers.BaseReconciler - TargetRefReconciler reconcilers.TargetRefReconciler - // AffectedPolicyMap tracks the affected policies to report their status. - AffectedPolicyMap *kuadrant.AffectedPolicyMap -} - -//+kubebuilder:rbac:groups=kuadrant.io,resources=authpolicies,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=kuadrant.io,resources=authpolicies/finalizers,verbs=update -//+kubebuilder:rbac:groups=kuadrant.io,resources=authpolicies/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=authorino.kuadrant.io,resources=authconfigs,verbs=get;list;watch;create;update;patch;delete - -func (r *AuthPolicyReconciler) Reconcile(eventCtx context.Context, req ctrl.Request) (ctrl.Result, error) { - logger := r.Logger().WithValues("AuthPolicy", req.NamespacedName) - logger.Info("Reconciling AuthPolicy") - ctx := logr.NewContext(eventCtx, logger) - - // fetch the authpolicy - ap := &kuadrantv1beta3.AuthPolicy{} - if err := r.Client().Get(ctx, req.NamespacedName, ap); err != nil { - if apierrors.IsNotFound(err) { - logger.Info("no AuthPolicy found") - return ctrl.Result{}, nil - } - logger.Error(err, "failed to get AuthPolicy") - return ctrl.Result{}, err - } - - if logger.V(1).Enabled() { - jsonData, err := json.MarshalIndent(ap, "", " ") - if err != nil { - return ctrl.Result{}, err - } - logger.V(1).Info(string(jsonData)) - } - - markedForDeletion := ap.GetDeletionTimestamp() != nil - - // fetch the target network object - targetNetworkObject, err := reconcilers.FetchTargetRefObject(ctx, r.Client(), ap.GetTargetRef(), ap.Namespace, ap.TargetProgrammedGatewaysOnly()) - if err != nil { - if !markedForDeletion { - if apierrors.IsNotFound(err) { - logger.V(1).Info("Network object not found. Cleaning up") - delResErr := r.deleteResources(ctx, ap, nil) - if delResErr == nil { - delResErr = err - } - return r.reconcileStatus(ctx, ap, kuadrant.NewErrTargetNotFound(ap.Kind(), ap.GetTargetRef(), delResErr)) - } - return ctrl.Result{}, err - } - targetNetworkObject = nil // we need the object set to nil when there's an error, otherwise deleting the resources (when marked for deletion) will panic - } - - // handle authpolicy marked for deletion - if markedForDeletion { - if controllerutil.ContainsFinalizer(ap, authPolicyFinalizer) { - logger.V(1).Info("Handling removal of authpolicy object") - - if err := r.deleteResources(ctx, ap, targetNetworkObject); err != nil { - return ctrl.Result{}, err - } - - logger.Info("removing finalizer") - if err := r.RemoveFinalizer(ctx, ap, authPolicyFinalizer); err != nil { - return ctrl.Result{}, err - } - } - - return ctrl.Result{}, nil - } - - // add finalizer to the authpolicy - if !controllerutil.ContainsFinalizer(ap, authPolicyFinalizer) { - if err := r.AddFinalizer(ctx, ap, authPolicyFinalizer); client.IgnoreNotFound(err) != nil { - return ctrl.Result{Requeue: true}, err - } - } - - // reconcile the authpolicy spec - specErr := r.reconcileResources(ctx, ap, targetNetworkObject) - - // reconcile authpolicy status - statusResult, statusErr := r.reconcileStatus(ctx, ap, specErr) - - if specErr != nil { - return ctrl.Result{}, specErr - } - - if statusErr != nil { - return ctrl.Result{}, statusErr - } - - if statusResult.Requeue { - logger.V(1).Info("Reconciling status not finished. Requeueing.") - return statusResult, nil - } - - // trigger concurrent reconciliations of possibly affected gateway policies - switch route := targetNetworkObject.(type) { - case *gatewayapiv1.HTTPRoute: - if err := r.reconcileRouteParentGatewayPolicies(ctx, route); err != nil { - return ctrl.Result{}, err - } - } - - logger.Info("AuthPolicy reconciled successfully") - return ctrl.Result{}, nil -} - -// validate performs validation before proceeding with the reconcile loop, returning a common.ErrInvalid on any failing validation -func (r *AuthPolicyReconciler) validate(ap *kuadrantv1beta3.AuthPolicy, targetNetworkObject client.Object) error { - if err := kuadrant.ValidateHierarchicalRules(ap, targetNetworkObject); err != nil { - return kuadrant.NewErrInvalid(ap.Kind(), err) - } - - return nil -} - -func (r *AuthPolicyReconciler) reconcileResources(ctx context.Context, ap *kuadrantv1beta3.AuthPolicy, targetNetworkObject client.Object) error { - if err := r.validate(ap, targetNetworkObject); err != nil { - return err - } - - // reconcile based on gateway diffs - gatewayDiffObj, err := reconcilers.ComputeGatewayDiffs(ctx, r.Client(), ap, targetNetworkObject) - if err != nil { - return err - } - - if err := r.reconcileAuthConfigs(ctx, ap, targetNetworkObject); err != nil { - return fmt.Errorf("reconcile AuthConfig error %w", err) - } - - // if the AuthPolicy(ap) targets a Gateway then all policies attached to that Gateway need to be checked. - // this is due to not knowing if the Gateway AuthPolicy was updated to include or remove the overrides section. - switch obj := targetNetworkObject.(type) { - case *gatewayapiv1.Gateway: - gw := kuadrant.GatewayWrapper{Gateway: obj, Referrer: ap} - apKey := client.ObjectKeyFromObject(ap) - for _, policyKey := range gw.PolicyRefs() { - if policyKey == apKey { - continue - } - - ref := &kuadrantv1beta3.AuthPolicy{} - err = r.Client().Get(ctx, policyKey, ref) - if err != nil { - return err - } - - refNetworkObject, err := reconcilers.FetchTargetRefObject(ctx, r.Client(), ref.GetTargetRef(), ref.Namespace, ap.TargetProgrammedGatewaysOnly()) - if err != nil { - return err - } - - if err = r.reconcileAuthConfigs(ctx, ref, refNetworkObject); err != nil { - return err - } - } - } - - // set direct back ref - i.e. claim the target network object as taken asap - if err := r.reconcileNetworkResourceDirectBackReference(ctx, ap, targetNetworkObject); err != nil { - return fmt.Errorf("reconcile TargetBackReference error %w", err) - } - - // set annotation of policies affecting the gateway - should be the last step, only when all the reconciliation steps succeed - if err := r.TargetRefReconciler.ReconcileGatewayPolicyReferences(ctx, ap, gatewayDiffObj); err != nil { - return fmt.Errorf("ReconcileGatewayPolicyReferences error %w", err) - } - - return nil -} - -func (r *AuthPolicyReconciler) deleteResources(ctx context.Context, ap *kuadrantv1beta3.AuthPolicy, targetNetworkObject client.Object) error { - // delete based on gateway diffs - gatewayDiffObj, err := reconcilers.ComputeGatewayDiffs(ctx, r.Client(), ap, targetNetworkObject) - if err != nil { - return err - } - - // remove direct back ref - if targetNetworkObject != nil { - if err := r.deleteNetworkResourceDirectBackReference(ctx, targetNetworkObject, ap); err != nil { - return err - } - } - - // update annotation of policies affecting the gateway - return r.TargetRefReconciler.ReconcileGatewayPolicyReferences(ctx, ap, gatewayDiffObj) -} - -// Ensures only one RLP targets the network resource -func (r *AuthPolicyReconciler) reconcileNetworkResourceDirectBackReference(ctx context.Context, ap *kuadrantv1beta3.AuthPolicy, targetNetworkObject client.Object) error { - return r.TargetRefReconciler.ReconcileTargetBackReference(ctx, ap, targetNetworkObject, ap.DirectReferenceAnnotationName()) -} - -func (r *AuthPolicyReconciler) deleteNetworkResourceDirectBackReference(ctx context.Context, targetNetworkObject client.Object, ap *kuadrantv1beta3.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 -func (r *AuthPolicyReconciler) reconcileRouteParentGatewayPolicies(ctx context.Context, route *gatewayapiv1.HTTPRoute) error { - logger, err := logr.FromContext(ctx) - if err != nil { - return err - } - mapper := HTTPRouteParentRefsEventMapper{ - Logger: logger, - Client: r.Client(), - } - requests := mapper.MapToAuthPolicy(route) - for i := range requests { - request := requests[i] - go r.Reconcile(context.Background(), request) - } - return nil -} diff --git a/controllers/authpolicy_status.go b/controllers/authpolicy_status.go deleted file mode 100644 index 7df00f53b..000000000 --- a/controllers/authpolicy_status.go +++ /dev/null @@ -1,179 +0,0 @@ -package controllers - -import ( - "context" - "errors" - "fmt" - "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" - "k8s.io/utils/ptr" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" - "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. -func (r *AuthPolicyReconciler) reconcileStatus(ctx context.Context, ap *kuadrantv1beta3.AuthPolicy, specErr error) (ctrl.Result, error) { - logger, _ := logr.FromContext(ctx) - logger.V(1).Info("Reconciling AuthPolicy status", "spec error", specErr) - - newStatus := r.calculateStatus(ctx, ap, specErr) - - equalStatus := ap.Status.Equals(newStatus, logger) - logger.V(1).Info("Status", "status is different", !equalStatus) - logger.V(1).Info("Status", "generation is different", ap.Generation != ap.Status.ObservedGeneration) - if equalStatus && ap.Generation == ap.Status.ObservedGeneration { - logger.V(1).Info("Status up-to-date. No changes required.") - return ctrl.Result{}, nil - } - - // Save the generation number we acted on, otherwise we might wrongfully indicate - // that we've seen a spec update when we retry. - // TODO: This can clobber an update if we allow multiple agents to write to the - // same status. - newStatus.ObservedGeneration = ap.Generation - - logger.V(1).Info("Updating Status", "sequence no:", fmt.Sprintf("sequence No: %v->%v", ap.Status.ObservedGeneration, newStatus.ObservedGeneration)) - - ap.Status = *newStatus - updateErr := r.Client().Status().Update(ctx, ap) - if updateErr != nil { - // Ignore conflicts, resource might just be outdated. - if apierrors.IsConflict(updateErr) { - logger.Info("Failed to update status: resource might just be outdated") - return ctrl.Result{Requeue: true}, nil - } - - return ctrl.Result{}, fmt.Errorf("failed to update status: %w", updateErr) - } - return ctrl.Result{}, nil -} - -func (r *AuthPolicyReconciler) calculateStatus(ctx context.Context, ap *kuadrantv1beta3.AuthPolicy, specErr error) *kuadrantv1beta3.AuthPolicyStatus { - newStatus := &kuadrantv1beta3.AuthPolicyStatus{ - Conditions: slices.Clone(ap.Status.Conditions), - ObservedGeneration: ap.Status.ObservedGeneration, - } - - acceptedCond := r.acceptedCondition(ap, specErr) - meta.SetStatusCondition(&newStatus.Conditions, *acceptedCond) - - // Do not set enforced condition if Accepted condition is false - if meta.IsStatusConditionFalse(newStatus.Conditions, string(gatewayapiv1alpha2.PolicyReasonAccepted)) { - meta.RemoveStatusCondition(&newStatus.Conditions, string(kuadrant.PolicyConditionEnforced)) - return newStatus - } - - enforcedCond := r.enforcedCondition(ctx, ap) - meta.SetStatusCondition(&newStatus.Conditions, *enforcedCond) - - return newStatus -} - -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 -// on the status of the associated AuthConfig and Gateway. -func (r *AuthPolicyReconciler) enforcedCondition(ctx context.Context, policy *kuadrantv1beta3.AuthPolicy) *metav1.Condition { - logger, _ := logr.FromContext(ctx) - - // Check if the policy is Affected - // Note: This logic assumes synchronous processing, where computing the desired AuthConfig, marking the AuthPolicy - // as Affected, and calculating the Enforced condition happen sequentially. - // Introducing a goroutine in this flow could break this assumption and lead to unexpected behavior. - if r.AffectedPolicyMap.IsPolicyAffected(policy) { - logger.V(1).Info("Gateway Policy is overridden") - return r.handlePolicyOverride(policy) - } - - // Check if the AuthConfig is ready - authConfigReady, err := r.isAuthConfigReady(ctx, policy) - if err != nil { - logger.Error(err, "Failed to check AuthConfig and Gateway") - return kuadrant.EnforcedCondition(policy, kuadrant.NewErrUnknown(policy.Kind(), err), false) - } - - if !authConfigReady { - logger.V(1).Info("AuthConfig is not ready") - return kuadrant.EnforcedCondition(policy, kuadrant.NewErrUnknown(policy.Kind(), errors.New("AuthScheme is not ready yet")), false) - } - - logger.V(1).Info("AuthPolicy is enforced") - return kuadrant.EnforcedCondition(policy, nil, true) -} - -// isAuthConfigReady checks if the AuthConfig is ready. -func (r *AuthPolicyReconciler) isAuthConfigReady(ctx context.Context, policy *kuadrantv1beta3.AuthPolicy) (bool, error) { - apKey := client.ObjectKeyFromObject(policy) - authConfigKey := client.ObjectKey{ - Namespace: policy.Namespace, - Name: AuthConfigName(apKey), - } - authConfig := &authorinoapi.AuthConfig{} - err := r.GetResource(ctx, authConfigKey, authConfig) - if err != nil { - if !apierrors.IsNotFound(err) { - return false, fmt.Errorf("failed to get AuthConfig: %w", err) - } - } - return authConfig.Status.Ready(), nil -} - -func (r *AuthPolicyReconciler) handlePolicyOverride(policy *kuadrantv1beta3.AuthPolicy) *metav1.Condition { - if !r.AffectedPolicyMap.IsPolicyOverridden(policy) { - return kuadrant.EnforcedCondition(policy, kuadrant.NewErrUnknown(policy.Kind(), errors.New("no free routes to enforce policy")), false) // Maybe this should be a standard condition rather than an unknown condition - } - - return kuadrant.EnforcedCondition(policy, kuadrant.NewErrOverridden(policy.Kind(), r.AffectedPolicyMap.PolicyAffectedBy(policy)), false) -} - -func (r *AuthPolicyReconciler) generateTopology(ctx context.Context) (*kuadrantgatewayapi.Topology, error) { - logger, _ := logr.FromContext(ctx) - - gwList := &gatewayapiv1.GatewayList{} - err := r.Client().List(ctx, gwList) - logger.V(1).Info("topology: list gateways", "#Gateways", len(gwList.Items), "err", err) - if err != nil { - return nil, err - } - - routeList := &gatewayapiv1.HTTPRouteList{} - err = r.Client().List(ctx, routeList) - logger.V(1).Info("topology: list httproutes", "#HTTPRoutes", len(routeList.Items), "err", err) - if err != nil { - return nil, err - } - - aplist := &kuadrantv1beta3.AuthPolicyList{} - err = r.Client().List(ctx, aplist) - logger.V(1).Info("topology: list rate limit policies", "#RLPS", len(aplist.Items), "err", err) - if err != nil { - return nil, err - } - - policies := utils.Map(aplist.Items, func(p kuadrantv1beta3.AuthPolicy) kuadrantgatewayapi.Policy { - return &p - }) - - return kuadrantgatewayapi.NewTopology( - kuadrantgatewayapi.WithAcceptedRoutesLinkedOnly(), - kuadrantgatewayapi.WithProgrammedGatewaysOnly(), - kuadrantgatewayapi.WithGateways(utils.Map(gwList.Items, ptr.To[gatewayapiv1.Gateway])), - kuadrantgatewayapi.WithRoutes(utils.Map(routeList.Items, ptr.To[gatewayapiv1.HTTPRoute])), - kuadrantgatewayapi.WithPolicies(policies), - kuadrantgatewayapi.WithLogger(logger), - ) -} diff --git a/controllers/authpolicy_status_test.go b/controllers/authpolicy_status_test.go deleted file mode 100644 index 783225891..000000000 --- a/controllers/authpolicy_status_test.go +++ /dev/null @@ -1,72 +0,0 @@ -//go:build unit - -package controllers - -import ( - "context" - "errors" - "reflect" - "testing" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - - kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" -) - -func TestAuthPolicyReconciler_calculateStatus(t *testing.T) { - type args struct { - ctx context.Context - ap *kuadrantv1beta3.AuthPolicy - specErr error - } - tests := []struct { - name string - args args - want *kuadrantv1beta3.AuthPolicyStatus - }{ - { - name: "Enforced status block removed if policy not Accepted. (Regression test)", // https://github.com/Kuadrant/kuadrant-operator/issues/588 - args: args{ - ap: &kuadrantv1beta3.AuthPolicy{ - Status: kuadrantv1beta3.AuthPolicyStatus{ - Conditions: []metav1.Condition{ - { - Message: "not accepted", - Type: string(gatewayapiv1alpha2.PolicyConditionAccepted), - Status: metav1.ConditionFalse, - Reason: string(gatewayapiv1alpha2.PolicyReasonTargetNotFound), - }, - { - Message: "AuthPolicy has been successfully enforced", - Type: string(kuadrant.PolicyConditionEnforced), - Status: metav1.ConditionTrue, - Reason: string(kuadrant.PolicyConditionEnforced), - }, - }, - }, - }, - specErr: kuadrant.NewErrInvalid("AuthPolicy", errors.New("policy Error")), - }, - want: &kuadrantv1beta3.AuthPolicyStatus{ - Conditions: []metav1.Condition{ - { - Message: "AuthPolicy target is invalid: policy Error", - Type: string(gatewayapiv1alpha2.PolicyConditionAccepted), - Status: metav1.ConditionFalse, - Reason: string(gatewayapiv1alpha2.PolicyReasonInvalid), - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := &AuthPolicyReconciler{} - if got := r.calculateStatus(tt.args.ctx, tt.args.ap, tt.args.specErr); !reflect.DeepEqual(got, tt.want) { - t.Errorf("calculateStatus() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/controllers/data_plane_policies_workflow.go b/controllers/data_plane_policies_workflow.go new file mode 100644 index 000000000..256504c84 --- /dev/null +++ b/controllers/data_plane_policies_workflow.go @@ -0,0 +1,105 @@ +package controllers + +import ( + "fmt" + + kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + kuadrantenvoygateway "github.com/kuadrant/kuadrant-operator/pkg/envoygateway" + kuadrantistio "github.com/kuadrant/kuadrant-operator/pkg/istio" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/utils/env" +) + +const ( + // make these configurable? + istioGatewayControllerName = "istio.io/gateway-controller" + envoyGatewayGatewayControllerName = "gateway.envoyproxy.io/gatewayclass-controller" +) + +var ( + WASMFilterImageURL = env.GetString("RELATED_IMAGE_WASMSHIM", "oci://quay.io/kuadrant/wasm-shim:latest") + + StateIstioExtensionsModified = "IstioExtensionsModified" + StateEnvoyGatewayExtensionsModified = "EnvoyGatewayExtensionsModified" + + // Event matchers to match events with potential impact on effective data plane policies (auth or rate limit) + dataPlaneEffectivePoliciesEventMatchers = []controller.ResourceEventMatcher{ + {Kind: &kuadrantv1beta1.KuadrantGroupKind}, + {Kind: &machinery.GatewayClassGroupKind}, + {Kind: &machinery.GatewayGroupKind}, + {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.RateLimitPolicyGroupKind}, + {Kind: &kuadrantv1beta1.LimitadorGroupKind}, + {Kind: &kuadrantv1beta3.AuthPolicyGroupKind}, + {Kind: &kuadrantv1beta1.AuthConfigGroupKind}, + {Kind: &kuadrantistio.EnvoyFilterGroupKind}, + {Kind: &kuadrantistio.WasmPluginGroupKind}, + {Kind: &kuadrantenvoygateway.EnvoyPatchPolicyGroupKind}, + {Kind: &kuadrantenvoygateway.EnvoyExtensionPolicyGroupKind}, + } +) + +func NewDataPlanePoliciesWorkflow(client *dynamic.DynamicClient, isIstioInstalled, isEnvoyGatewayInstalled bool) *controller.Workflow { + dataPlanePoliciesValidation := &controller.Workflow{ + Tasks: []controller.ReconcileFunc{ + (&AuthPolicyValidator{}).Subscription().Reconcile, + (&RateLimitPolicyValidator{}).Subscription().Reconcile, + }, + } + + effectiveDataPlanePoliciesWorkflow := &controller.Workflow{ + Precondition: (&controller.Workflow{ + Tasks: []controller.ReconcileFunc{ + (&EffectiveAuthPolicyReconciler{client: client}).Subscription().Reconcile, + (&EffectiveRateLimitPolicyReconciler{client: client}).Subscription().Reconcile, + }, + }).Run, + Tasks: []controller.ReconcileFunc{ + (&AuthConfigsReconciler{client: client}).Subscription().Reconcile, + (&LimitadorLimitsReconciler{client: client}).Subscription().Reconcile, + }, + } + + if isIstioInstalled { + effectiveDataPlanePoliciesWorkflow.Tasks = append(effectiveDataPlanePoliciesWorkflow.Tasks, (&IstioAuthClusterReconciler{client: client}).Subscription().Reconcile) + effectiveDataPlanePoliciesWorkflow.Tasks = append(effectiveDataPlanePoliciesWorkflow.Tasks, (&IstioRateLimitClusterReconciler{client: client}).Subscription().Reconcile) + effectiveDataPlanePoliciesWorkflow.Tasks = append(effectiveDataPlanePoliciesWorkflow.Tasks, (&IstioExtensionReconciler{client: client}).Subscription().Reconcile) + } + + if isEnvoyGatewayInstalled { + effectiveDataPlanePoliciesWorkflow.Tasks = append(effectiveDataPlanePoliciesWorkflow.Tasks, (&EnvoyGatewayAuthClusterReconciler{client: client}).Subscription().Reconcile) + effectiveDataPlanePoliciesWorkflow.Tasks = append(effectiveDataPlanePoliciesWorkflow.Tasks, (&EnvoyGatewayRateLimitClusterReconciler{client: client}).Subscription().Reconcile) + effectiveDataPlanePoliciesWorkflow.Tasks = append(effectiveDataPlanePoliciesWorkflow.Tasks, (&EnvoyGatewayExtensionReconciler{client: client}).Subscription().Reconcile) + } + + dataPlanePoliciesStatus := &controller.Workflow{ + Tasks: []controller.ReconcileFunc{ + (&AuthPolicyStatusUpdater{client: client}).Subscription().Reconcile, + (&RateLimitPolicyStatusUpdater{client: client}).Subscription().Reconcile, + }, + } + + return &controller.Workflow{ + Precondition: dataPlanePoliciesValidation.Run, + Tasks: []controller.ReconcileFunc{effectiveDataPlanePoliciesWorkflow.Run}, + Postcondition: dataPlanePoliciesStatus.Run, + } +} + +func gatewayComponentsToSync(gateway *machinery.Gateway, componentGroupKind schema.GroupKind, modifiedGatewayLocators any, topology *machinery.Topology, requiredCondition func(machinery.Object) bool) []string { + missingConditionInTopologyFunc := func() bool { + obj, found := lo.Find(topology.Objects().Children(gateway), func(child machinery.Object) bool { + return child.GroupVersionKind().GroupKind() == componentGroupKind + }) + return !found || !requiredCondition(obj) + } + if (modifiedGatewayLocators != nil && lo.Contains(modifiedGatewayLocators.([]string), gateway.GetLocator())) || missingConditionInTopologyFunc() { + return []string{fmt.Sprintf("%s (%s/%s)", componentGroupKind.Kind, gateway.GetNamespace(), gateway.GetName())} + } + return nil +} diff --git a/controllers/effective_auth_policies_reconciler.go b/controllers/effective_auth_policies_reconciler.go new file mode 100644 index 000000000..d444373d2 --- /dev/null +++ b/controllers/effective_auth_policies_reconciler.go @@ -0,0 +1,93 @@ +package controllers + +import ( + "context" + "encoding/json" + "errors" + "sync" + + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + "k8s.io/client-go/dynamic" + + kuadrantv1 "github.com/kuadrant/kuadrant-operator/api/v1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" +) + +type EffectiveAuthPolicy struct { + Path []machinery.Targetable + Spec kuadrantv1beta3.AuthPolicy +} + +type EffectiveAuthPolicies map[string]EffectiveAuthPolicy + +type EffectiveAuthPolicyReconciler struct { + client *dynamic.DynamicClient +} + +// EffectiveAuthPolicyReconciler subscribe to the same events as rate limit because they are used together to compose gateway extension resources +func (r *EffectiveAuthPolicyReconciler) Subscription() controller.Subscription { + return controller.Subscription{ + ReconcileFunc: r.Reconcile, + Events: dataPlaneEffectivePoliciesEventMatchers, + } +} + +func (r *EffectiveAuthPolicyReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("EffectiveAuthPolicyReconciler") + + kuadrant, err := GetKuadrantFromTopology(topology) + if err != nil { + if errors.Is(err, ErrMissingKuadrant) { + logger.V(1).Info(err.Error()) + return nil + } + return err + } + + effectivePolicies := r.calculateEffectivePolicies(ctx, topology, kuadrant, state) + + state.Store(StateEffectiveAuthPolicies, effectivePolicies) + + return nil +} + +func (r *EffectiveAuthPolicyReconciler) calculateEffectivePolicies(ctx context.Context, topology *machinery.Topology, kuadrant machinery.Object, state *sync.Map) EffectiveAuthPolicies { + logger := controller.LoggerFromContext(ctx).WithName("EffectiveAuthPolicyReconciler").WithName("calculateEffectivePolicies") + + targetables := topology.Targetables() + gatewayClasses := targetables.Children(kuadrant) // assumes only and all valid gateway classes are linked to kuadrant in the topology + httpRouteRules := targetables.Items(func(o machinery.Object) bool { + _, ok := o.(*machinery.HTTPRouteRule) + return ok + }) + + logger.V(1).Info("calculating effective auth policies", "httpRouteRules", len(httpRouteRules)) + + effectivePolicies := EffectiveAuthPolicies{} + + for _, gatewayClass := range gatewayClasses { + for _, httpRouteRule := range httpRouteRules { + paths := targetables.Paths(gatewayClass, httpRouteRule) // this may be expensive in clusters with many gateway classes - an alternative is to deep search the topology for httprouterules from each gatewayclass, keeping record of the paths + for i := range paths { + if effectivePolicy := kuadrantv1.EffectivePolicyForPath[*kuadrantv1beta3.AuthPolicy](paths[i], isAuthPolicyAcceptedAndNotDeletedFunc(state)); effectivePolicy != nil { + pathID := kuadrantv1.PathID(paths[i]) + effectivePolicies[pathID] = EffectiveAuthPolicy{ + Path: paths[i], + Spec: **effectivePolicy, + } + if logger.V(1).Enabled() { + jsonEffectivePolicy, _ := json.Marshal(effectivePolicy) + pathLocators := lo.Map(paths[i], machinery.MapTargetableToLocatorFunc) + logger.V(1).Info("effective policy", "kind", kuadrantv1beta3.AuthPolicyGroupKind.Kind, "pathID", pathID, "path", pathLocators, "effectivePolicy", string(jsonEffectivePolicy)) + } + } + } + } + } + + logger.V(1).Info("finished calculating effective auth policies", "effectivePolicies", len(effectivePolicies)) + + return effectivePolicies +} diff --git a/controllers/effective_ratelimitpolicies_reconciler.go b/controllers/effective_ratelimit_policies_reconciler.go similarity index 84% rename from controllers/effective_ratelimitpolicies_reconciler.go rename to controllers/effective_ratelimit_policies_reconciler.go index 641d0bbb0..0976d57bf 100644 --- a/controllers/effective_ratelimitpolicies_reconciler.go +++ b/controllers/effective_ratelimit_policies_reconciler.go @@ -22,19 +22,20 @@ type EffectiveRateLimitPolicy struct { type EffectiveRateLimitPolicies map[string]EffectiveRateLimitPolicy -type effectiveRateLimitPolicyReconciler struct { +type EffectiveRateLimitPolicyReconciler struct { client *dynamic.DynamicClient } -func (r *effectiveRateLimitPolicyReconciler) Subscription() controller.Subscription { +// EffectiveRateLimitPolicyReconciler subscribe to the same events as auth because they are used together to compose gateway extension resources +func (r *EffectiveRateLimitPolicyReconciler) Subscription() controller.Subscription { return controller.Subscription{ ReconcileFunc: r.Reconcile, - Events: rateLimitEventMatchers, + Events: dataPlaneEffectivePoliciesEventMatchers, } } -func (r *effectiveRateLimitPolicyReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { - logger := controller.LoggerFromContext(ctx).WithName("effectiveRateLimitPolicyReconciler") +func (r *EffectiveRateLimitPolicyReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("EffectiveRateLimitPolicyReconciler") kuadrant, err := GetKuadrantFromTopology(topology) if err != nil { @@ -52,8 +53,8 @@ func (r *effectiveRateLimitPolicyReconciler) Reconcile(ctx context.Context, _ [] return nil } -func (r *effectiveRateLimitPolicyReconciler) calculateEffectivePolicies(ctx context.Context, topology *machinery.Topology, kuadrant machinery.Object, state *sync.Map) EffectiveRateLimitPolicies { - logger := controller.LoggerFromContext(ctx).WithName("effectiveRateLimitPolicyReconciler").WithName("calculateEffectivePolicies") +func (r *EffectiveRateLimitPolicyReconciler) calculateEffectivePolicies(ctx context.Context, topology *machinery.Topology, kuadrant machinery.Object, state *sync.Map) EffectiveRateLimitPolicies { + logger := controller.LoggerFromContext(ctx).WithName("EffectiveRateLimitPolicyReconciler").WithName("calculateEffectivePolicies") targetables := topology.Targetables() gatewayClasses := targetables.Children(kuadrant) // assumes only and all valid gateway classes are linked to kuadrant in the topology diff --git a/controllers/envoy_gateway_auth_cluster_reconciler.go b/controllers/envoy_gateway_auth_cluster_reconciler.go new file mode 100644 index 000000000..7cd77d867 --- /dev/null +++ b/controllers/envoy_gateway_auth_cluster_reconciler.go @@ -0,0 +1,202 @@ +package controllers + +import ( + "context" + "errors" + "fmt" + "sync" + + envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + authorinooperatorv1beta1 "github.com/kuadrant/authorino-operator/api/v1beta1" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + k8stypes "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "k8s.io/utils/ptr" + gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + "github.com/kuadrant/kuadrant-operator/pkg/common" + kuadrantenvoygateway "github.com/kuadrant/kuadrant-operator/pkg/envoygateway" +) + +// EnvoyGatewayAuthClusterReconciler reconciles Envoy Gateway EnvoyPatchPolicy custom resources for auth +type EnvoyGatewayAuthClusterReconciler struct { + client *dynamic.DynamicClient +} + +// EnvoyGatewayAuthClusterReconciler subscribes to events with potential impact on the Envoy Gateway EnvoyPatchPolicy custom resources for auth +func (r *EnvoyGatewayAuthClusterReconciler) Subscription() controller.Subscription { + return controller.Subscription{ + ReconcileFunc: r.Reconcile, + Events: []controller.ResourceEventMatcher{ + {Kind: &kuadrantv1beta1.KuadrantGroupKind}, + {Kind: &machinery.GatewayClassGroupKind}, + {Kind: &machinery.GatewayGroupKind}, + {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.AuthPolicyGroupKind}, + {Kind: &kuadrantenvoygateway.EnvoyPatchPolicyGroupKind}, + }, + } +} + +func (r *EnvoyGatewayAuthClusterReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("EnvoyGatewayAuthClusterReconciler") + + logger.V(1).Info("building envoy gateway auth clusters") + defer logger.V(1).Info("finished building envoy gateway auth clusters") + + kuadrant, err := GetKuadrantFromTopology(topology) + if err != nil { + if errors.Is(err, ErrMissingKuadrant) { + logger.V(1).Info(err.Error()) + return nil + } + return err + } + + authorinoObj, found := lo.Find(topology.Objects().Children(kuadrant), func(child machinery.Object) bool { + return child.GroupVersionKind().GroupKind() == kuadrantv1beta1.AuthorinoGroupKind + }) + if !found { + logger.V(1).Info(ErrMissingAuthorino.Error()) + return nil + } + authorino := authorinoObj.(*controller.RuntimeObject).Object.(*authorinooperatorv1beta1.Authorino) + + effectivePolicies, ok := state.Load(StateEffectiveAuthPolicies) + if !ok { + logger.Error(ErrMissingStateEffectiveAuthPolicies, "failed to get effective auth policies from state") + return nil + } + + gateways := lo.UniqBy(lo.FilterMap(lo.Values(effectivePolicies.(EffectiveAuthPolicies)), func(effectivePolicy EffectiveAuthPolicy, _ int) (*machinery.Gateway, bool) { + gatewayClass, gateway, _, _, _, _ := common.ObjectsInRequestPath(effectivePolicy.Path) + return gateway, gatewayClass.Spec.ControllerName == envoyGatewayGatewayControllerName + }), func(gateway *machinery.Gateway) string { + return gateway.GetLocator() + }) + + desiredEnvoyPatchPolicies := make(map[k8stypes.NamespacedName]struct{}) + var modifiedGateways []string + + // reconcile envoy gateway cluster for gateway + for _, gateway := range gateways { + gatewayKey := k8stypes.NamespacedName{Name: gateway.GetName(), Namespace: gateway.GetNamespace()} + + desiredEnvoyPatchPolicy, err := r.buildDesiredEnvoyPatchPolicy(authorino, gateway) + if err != nil { + logger.Error(err, "failed to build desired envoy patch policy") + continue + } + desiredEnvoyPatchPolicies[k8stypes.NamespacedName{Name: desiredEnvoyPatchPolicy.GetName(), Namespace: desiredEnvoyPatchPolicy.GetNamespace()}] = struct{}{} + + resource := r.client.Resource(kuadrantenvoygateway.EnvoyPatchPoliciesResource).Namespace(desiredEnvoyPatchPolicy.GetNamespace()) + + existingEnvoyPatchPolicyObj, found := lo.Find(topology.Objects().Children(gateway), func(child machinery.Object) bool { + return child.GroupVersionKind().GroupKind() == kuadrantenvoygateway.EnvoyPatchPolicyGroupKind && child.GetName() == desiredEnvoyPatchPolicy.GetName() && child.GetNamespace() == desiredEnvoyPatchPolicy.GetNamespace() && labels.Set(child.(*controller.RuntimeObject).GetLabels()).AsSelector().Matches(labels.Set(desiredEnvoyPatchPolicy.GetLabels())) + }) + + // create + if !found { + modifiedGateways = append(modifiedGateways, gateway.GetLocator()) // we only signal the gateway as modified when an envoypatchpolicy is created, because updates won't change the status + desiredEnvoyPatchPolicyUnstructured, err := controller.Destruct(desiredEnvoyPatchPolicy) + if err != nil { + logger.Error(err, "failed to destruct envoypatchpolicy object", "gateway", gatewayKey.String(), "envoypatchpolicy", desiredEnvoyPatchPolicy) + continue + } + if _, err = resource.Create(ctx, desiredEnvoyPatchPolicyUnstructured, metav1.CreateOptions{}); err != nil { + logger.Error(err, "failed to create envoypatchpolicy object", "gateway", gatewayKey.String(), "envoypatchpolicy", desiredEnvoyPatchPolicyUnstructured.Object) + // TODO: handle error + } + continue + } + + existingEnvoyPatchPolicy := existingEnvoyPatchPolicyObj.(*controller.RuntimeObject).Object.(*envoygatewayv1alpha1.EnvoyPatchPolicy) + + if kuadrantenvoygateway.EqualEnvoyPatchPolicies(existingEnvoyPatchPolicy, desiredEnvoyPatchPolicy) { + logger.V(1).Info("envoypatchpolicy object is up to date, nothing to do") + continue + } + + // update + existingEnvoyPatchPolicy.Spec = envoygatewayv1alpha1.EnvoyPatchPolicySpec{ + TargetRef: desiredEnvoyPatchPolicy.Spec.TargetRef, + Type: desiredEnvoyPatchPolicy.Spec.Type, + JSONPatches: desiredEnvoyPatchPolicy.Spec.JSONPatches, + Priority: desiredEnvoyPatchPolicy.Spec.Priority, + } + + existingEnvoyPatchPolicyUnstructured, err := controller.Destruct(existingEnvoyPatchPolicy) + if err != nil { + logger.Error(err, "failed to destruct envoypatchpolicy object", "gateway", gatewayKey.String(), "envoypatchpolicy", existingEnvoyPatchPolicy) + continue + } + if _, err = resource.Update(ctx, existingEnvoyPatchPolicyUnstructured, metav1.UpdateOptions{}); err != nil { + logger.Error(err, "failed to update envoypatchpolicy object", "gateway", gatewayKey.String(), "envoypatchpolicy", existingEnvoyPatchPolicyUnstructured.Object) + // TODO: handle error + } + } + + state.Store(StateEnvoyGatewayAuthClustersModified, modifiedGateways) + + // cleanup envoy gateway clusters for gateways that are not in the effective policies + staleEnvoyPatchPolicies := topology.Objects().Items(func(o machinery.Object) bool { + _, desired := desiredEnvoyPatchPolicies[k8stypes.NamespacedName{Name: o.GetName(), Namespace: o.GetNamespace()}] + return o.GroupVersionKind().GroupKind() == kuadrantenvoygateway.EnvoyPatchPolicyGroupKind && labels.Set(o.(*controller.RuntimeObject).GetLabels()).AsSelector().Matches(AuthObjectLabels()) && !desired + }) + + for _, envoyPatchPolicy := range staleEnvoyPatchPolicies { + if err := r.client.Resource(kuadrantenvoygateway.EnvoyPatchPoliciesResource).Namespace(envoyPatchPolicy.GetNamespace()).Delete(ctx, envoyPatchPolicy.GetName(), metav1.DeleteOptions{}); err != nil { + logger.Error(err, "failed to delete envoypatchpolicy object", "envoypatchpolicy", fmt.Sprintf("%s/%s", envoyPatchPolicy.GetNamespace(), envoyPatchPolicy.GetName())) + // TODO: handle error + } + } + + return nil +} + +func (r *EnvoyGatewayAuthClusterReconciler) buildDesiredEnvoyPatchPolicy(authorino *authorinooperatorv1beta1.Authorino, gateway *machinery.Gateway) (*envoygatewayv1alpha1.EnvoyPatchPolicy, error) { + envoyPatchPolicy := &envoygatewayv1alpha1.EnvoyPatchPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: kuadrantenvoygateway.EnvoyPatchPolicyGroupKind.Kind, + APIVersion: envoygatewayv1alpha1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: AuthClusterName(gateway.GetName()), + Namespace: gateway.GetNamespace(), + Labels: AuthObjectLabels(), + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: gateway.GroupVersionKind().GroupVersion().String(), + Kind: gateway.GroupVersionKind().Kind, + Name: gateway.Name, + UID: gateway.UID, + BlockOwnerDeletion: ptr.To(true), + Controller: ptr.To(true), + }, + }, + }, + Spec: envoygatewayv1alpha1.EnvoyPatchPolicySpec{ + TargetRef: gatewayapiv1alpha2.LocalPolicyTargetReference{ + Group: gatewayapiv1alpha2.Group(machinery.GatewayGroupKind.Group), + Kind: gatewayapiv1alpha2.Kind(machinery.GatewayGroupKind.Kind), + Name: gatewayapiv1alpha2.ObjectName(gateway.GetName()), + }, + Type: envoygatewayv1alpha1.JSONPatchEnvoyPatchType, + }, + } + + authorinoServiceInfo := authorinoServiceInfoFromAuthorino(authorino) + jsonPatches, err := kuadrantenvoygateway.BuildEnvoyPatchPolicyClusterPatch(authorinoServiceInfo.Host, int(authorinoServiceInfo.Port), authClusterPatch) + if err != nil { + return nil, err + } + envoyPatchPolicy.Spec.JSONPatches = jsonPatches + + return envoyPatchPolicy, nil +} diff --git a/controllers/envoy_gateway_extension_reconciler.go b/controllers/envoy_gateway_extension_reconciler.go index 9e8ad2fdf..2f8a6648e 100644 --- a/controllers/envoy_gateway_extension_reconciler.go +++ b/controllers/envoy_gateway_extension_reconciler.go @@ -27,27 +27,29 @@ import ( "github.com/kuadrant/kuadrant-operator/pkg/wasm" ) -// envoyGatewayExtensionReconciler reconciles Envoy Gateway EnvoyExtensionPolicy custom resources -type envoyGatewayExtensionReconciler struct { +// EnvoyGatewayExtensionReconciler reconciles Envoy Gateway EnvoyExtensionPolicy custom resources +type EnvoyGatewayExtensionReconciler struct { client *dynamic.DynamicClient } -func (r *envoyGatewayExtensionReconciler) Subscription() controller.Subscription { +// EnvoyGatewayExtensionReconciler subscribes to events with potential impact on the Envoy Gateway EnvoyExtensionPolicy custom resources +func (r *EnvoyGatewayExtensionReconciler) Subscription() controller.Subscription { return controller.Subscription{ ReconcileFunc: r.Reconcile, - Events: []controller.ResourceEventMatcher{ // matches reconciliation events that change the rate limit definitions or status of rate limit policies + Events: []controller.ResourceEventMatcher{ {Kind: &kuadrantv1beta1.KuadrantGroupKind}, {Kind: &machinery.GatewayClassGroupKind}, {Kind: &machinery.GatewayGroupKind}, {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.AuthPolicyGroupKind}, {Kind: &kuadrantv1beta3.RateLimitPolicyGroupKind}, {Kind: &kuadrantenvoygateway.EnvoyExtensionPolicyGroupKind}, }, } } -func (r *envoyGatewayExtensionReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { - logger := controller.LoggerFromContext(ctx).WithName("envoyGatewayExtensionReconciler") +func (r *EnvoyGatewayExtensionReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("EnvoyGatewayExtensionReconciler") logger.V(1).Info("building envoy gateway extension") defer logger.V(1).Info("finished building envoy gateway extension") @@ -55,7 +57,7 @@ func (r *envoyGatewayExtensionReconciler) Reconcile(ctx context.Context, _ []con // build wasm plugin configs for each gateway wasmConfigs, err := r.buildWasmConfigs(ctx, state) if err != nil { - if errors.Is(err, ErrMissingStateEffectiveRateLimitPolicies) { + if errors.Is(err, ErrMissingStateEffectiveAuthPolicies) || errors.Is(err, ErrMissingStateEffectiveRateLimitPolicies) { logger.V(1).Info(err.Error()) } else { return err @@ -137,28 +139,59 @@ func (r *envoyGatewayExtensionReconciler) Reconcile(ctx context.Context, _ []con } // buildWasmConfigs returns a map of envoy gateway gateway locators to an ordered list of corresponding wasm policies -func (r *envoyGatewayExtensionReconciler) buildWasmConfigs(ctx context.Context, state *sync.Map) (map[string]wasm.Config, error) { - logger := controller.LoggerFromContext(ctx).WithName("envoyGatewayExtensionReconciler").WithName("buildWasmConfigs") +func (r *EnvoyGatewayExtensionReconciler) buildWasmConfigs(ctx context.Context, state *sync.Map) (map[string]wasm.Config, error) { + logger := controller.LoggerFromContext(ctx).WithName("EnvoyGatewayExtensionReconciler").WithName("buildWasmConfigs") - effectivePolicies, ok := state.Load(StateEffectiveRateLimitPolicies) + effectiveAuthPolicies, ok := state.Load(StateEffectiveAuthPolicies) + if !ok { + return nil, ErrMissingStateEffectiveAuthPolicies + } + effectiveAuthPoliciesMap := effectiveAuthPolicies.(EffectiveAuthPolicies) + + effectiveRateLimitPolicies, ok := state.Load(StateEffectiveRateLimitPolicies) if !ok { return nil, ErrMissingStateEffectiveRateLimitPolicies } + effectiveRateLimitPoliciesMap := effectiveRateLimitPolicies.(EffectiveRateLimitPolicies) + + logger.V(1).Info("building wasm configs for envoy gateway extension", "effectiveRateLimitPolicies", len(effectiveRateLimitPoliciesMap)) - logger.V(1).Info("building wasm configs for envoy gateway extension", "effectivePolicies", len(effectivePolicies.(EffectiveRateLimitPolicies))) + paths := lo.UniqBy(append( + lo.Entries(lo.MapValues(effectiveAuthPoliciesMap, func(p EffectiveAuthPolicy, _ string) []machinery.Targetable { return p.Path })), + lo.Entries(lo.MapValues(effectiveRateLimitPoliciesMap, func(p EffectiveRateLimitPolicy, _ string) []machinery.Targetable { return p.Path }))..., + ), func(e lo.Entry[string, []machinery.Targetable]) string { return e.Key }) wasmActionSets := kuadrantgatewayapi.GrouppedHTTPRouteMatchConfigs{} // build the wasm policies for each topological path that contains an effective rate limit policy affecting an envoy gateway gateway - for pathID, effectivePolicy := range effectivePolicies.(EffectiveRateLimitPolicies) { - gatewayClass, gateway, _, _, _, _ := common.ObjectsInRequestPath(effectivePolicy.Path) + for i := range paths { + pathID := paths[i].Key + path := paths[i].Value + + gatewayClass, gateway, _, _, _, _ := common.ObjectsInRequestPath(path) // ignore if not an envoy gateway gateway if gatewayClass.Spec.ControllerName != envoyGatewayGatewayControllerName { continue } - wasmActionSetsForPath, err := wasm.BuildActionSetsForPath(pathID, effectivePolicy.Path, effectivePolicy.Spec.Rules(), rateLimitWasmActionBuilder(pathID, effectivePolicy, state)) + var actions []wasm.Action + + // auth + if effectivePolicy, ok := effectiveAuthPoliciesMap[pathID]; ok { + actions = append(actions, buildWasmActionsForAuth(pathID, effectivePolicy)...) + } + + // rate limit + if effectivePolicy, ok := effectiveRateLimitPoliciesMap[pathID]; ok { + actions = append(actions, buildWasmActionsForRateLimit(effectivePolicy, state)...) + } + + if len(actions) == 0 { + continue + } + + wasmActionSetsForPath, err := wasm.BuildActionSetsForPath(pathID, path, actions) if err != nil { logger.Error(err, "failed to build wasm policies for path", "pathID", pathID) continue diff --git a/controllers/envoy_gateway_rate_limit_cluster_reconciler.go b/controllers/envoy_gateway_ratelimit_cluster_reconciler.go similarity index 77% rename from controllers/envoy_gateway_rate_limit_cluster_reconciler.go rename to controllers/envoy_gateway_ratelimit_cluster_reconciler.go index b61eab34a..4940d63f4 100644 --- a/controllers/envoy_gateway_rate_limit_cluster_reconciler.go +++ b/controllers/envoy_gateway_ratelimit_cluster_reconciler.go @@ -2,7 +2,6 @@ package controllers import ( "context" - "encoding/json" "errors" "fmt" "sync" @@ -12,7 +11,6 @@ import ( "github.com/kuadrant/policy-machinery/controller" "github.com/kuadrant/policy-machinery/machinery" "github.com/samber/lo" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" k8stypes "k8s.io/apimachinery/pkg/types" @@ -26,15 +24,16 @@ import ( kuadrantenvoygateway "github.com/kuadrant/kuadrant-operator/pkg/envoygateway" ) -// envoyGatewayRateLimitClusterReconciler reconciles Envoy Gateway EnvoyPatchPolicy custom resources -type envoyGatewayRateLimitClusterReconciler struct { +// EnvoyGatewayRateLimitClusterReconciler reconciles Envoy Gateway EnvoyPatchPolicy custom resources for rate limiting +type EnvoyGatewayRateLimitClusterReconciler struct { client *dynamic.DynamicClient } -func (r *envoyGatewayRateLimitClusterReconciler) Subscription() controller.Subscription { +// EnvoyGatewayRateLimitClusterReconciler subscribes to events with potential impact on the Envoy Gateway EnvoyPatchPolicy custom resources for rate limiting +func (r *EnvoyGatewayRateLimitClusterReconciler) Subscription() controller.Subscription { return controller.Subscription{ ReconcileFunc: r.Reconcile, - Events: []controller.ResourceEventMatcher{ // matches reconciliation events that change the rate limit definitions or status of rate limit policies + Events: []controller.ResourceEventMatcher{ {Kind: &kuadrantv1beta1.KuadrantGroupKind}, {Kind: &machinery.GatewayClassGroupKind}, {Kind: &machinery.GatewayGroupKind}, @@ -45,8 +44,8 @@ func (r *envoyGatewayRateLimitClusterReconciler) Subscription() controller.Subsc } } -func (r *envoyGatewayRateLimitClusterReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { - logger := controller.LoggerFromContext(ctx).WithName("envoyGatewayRateLimitClusterReconciler") +func (r *EnvoyGatewayRateLimitClusterReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("EnvoyGatewayRateLimitClusterReconciler") logger.V(1).Info("building envoy gateway rate limit clusters") defer logger.V(1).Info("finished building envoy gateway rate limit clusters") @@ -119,7 +118,7 @@ func (r *envoyGatewayRateLimitClusterReconciler) Reconcile(ctx context.Context, existingEnvoyPatchPolicy := existingEnvoyPatchPolicyObj.(*controller.RuntimeObject).Object.(*envoygatewayv1alpha1.EnvoyPatchPolicy) - if equalEnvoyPatchPolicies(existingEnvoyPatchPolicy, desiredEnvoyPatchPolicy) { + if kuadrantenvoygateway.EqualEnvoyPatchPolicies(existingEnvoyPatchPolicy, desiredEnvoyPatchPolicy) { logger.V(1).Info("envoypatchpolicy object is up to date, nothing to do") continue } @@ -161,7 +160,7 @@ func (r *envoyGatewayRateLimitClusterReconciler) Reconcile(ctx context.Context, return nil } -func (r *envoyGatewayRateLimitClusterReconciler) buildDesiredEnvoyPatchPolicy(limitador *limitadorv1alpha1.Limitador, gateway *machinery.Gateway) (*envoygatewayv1alpha1.EnvoyPatchPolicy, error) { +func (r *EnvoyGatewayRateLimitClusterReconciler) buildDesiredEnvoyPatchPolicy(limitador *limitadorv1alpha1.Limitador, gateway *machinery.Gateway) (*envoygatewayv1alpha1.EnvoyPatchPolicy, error) { envoyPatchPolicy := &envoygatewayv1alpha1.EnvoyPatchPolicy{ TypeMeta: metav1.TypeMeta{ Kind: kuadrantenvoygateway.EnvoyPatchPolicyGroupKind.Kind, @@ -192,7 +191,7 @@ func (r *envoyGatewayRateLimitClusterReconciler) buildDesiredEnvoyPatchPolicy(li }, } - jsonPatches, err := envoyGatewayEnvoyPatchPolicyClusterPatch(limitador.Status.Service.Host, int(limitador.Status.Service.Ports.GRPC)) + jsonPatches, err := kuadrantenvoygateway.BuildEnvoyPatchPolicyClusterPatch(limitador.Status.Service.Host, int(limitador.Status.Service.Ports.GRPC), rateLimitClusterPatch) if err != nil { return nil, err } @@ -200,42 +199,3 @@ func (r *envoyGatewayRateLimitClusterReconciler) buildDesiredEnvoyPatchPolicy(li return envoyPatchPolicy, nil } - -// envoyGatewayEnvoyPatchPolicyClusterPatch returns a set envoy config patch that defines the rate limit cluster for the gateway. -// The rate limit cluster configures the endpoint of the external rate limit service. -func envoyGatewayEnvoyPatchPolicyClusterPatch(host string, port int) ([]envoygatewayv1alpha1.EnvoyJSONPatchConfig, error) { - patchRaw, _ := json.Marshal(rateLimitClusterPatch(host, port)) - patch := &apiextensionsv1.JSON{} - if err := patch.UnmarshalJSON(patchRaw); err != nil { - return nil, err - } - - return []envoygatewayv1alpha1.EnvoyJSONPatchConfig{ - { - Type: envoygatewayv1alpha1.ClusterEnvoyResourceType, - Name: common.KuadrantRateLimitClusterName, - Operation: envoygatewayv1alpha1.JSONPatchOperation{ - Op: envoygatewayv1alpha1.JSONPatchOperationType("add"), - Path: "", - Value: patch, - }, - }, - }, nil -} - -func equalEnvoyPatchPolicies(a, b *envoygatewayv1alpha1.EnvoyPatchPolicy) bool { - if a.Spec.Priority != b.Spec.Priority || a.Spec.TargetRef != b.Spec.TargetRef { - return false - } - - aJSONPatches := a.Spec.JSONPatches - bJSONPatches := b.Spec.JSONPatches - if len(aJSONPatches) != len(bJSONPatches) { - return false - } - return lo.EveryBy(aJSONPatches, func(aJSONPatch envoygatewayv1alpha1.EnvoyJSONPatchConfig) bool { - return lo.SomeBy(bJSONPatches, func(bJSONPatch envoygatewayv1alpha1.EnvoyJSONPatchConfig) bool { - return aJSONPatch.Type == bJSONPatch.Type && aJSONPatch.Name == bJSONPatch.Name && aJSONPatch.Operation == bJSONPatch.Operation - }) - }) -} diff --git a/controllers/istio_auth_cluster_reconciler.go b/controllers/istio_auth_cluster_reconciler.go new file mode 100644 index 000000000..53847a9dd --- /dev/null +++ b/controllers/istio_auth_cluster_reconciler.go @@ -0,0 +1,202 @@ +package controllers + +import ( + "context" + "errors" + "fmt" + "sync" + + authorinooperatorv1beta1 "github.com/kuadrant/authorino-operator/api/v1beta1" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + istioapinetworkingv1alpha3 "istio.io/api/networking/v1alpha3" + istiov1beta1 "istio.io/api/type/v1beta1" + istioclientgonetworkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + k8stypes "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "k8s.io/utils/ptr" + + kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" + kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" + "github.com/kuadrant/kuadrant-operator/pkg/common" + kuadrantistio "github.com/kuadrant/kuadrant-operator/pkg/istio" +) + +// IstioAuthClusterReconciler reconciles Istio EnvoyFilter custom resources for auth +type IstioAuthClusterReconciler struct { + client *dynamic.DynamicClient +} + +// IstioAuthClusterReconciler subscribes to events with potential impact on the Istio EnvoyFilter custom resources for auth +func (r *IstioAuthClusterReconciler) Subscription() controller.Subscription { + return controller.Subscription{ + ReconcileFunc: r.Reconcile, + Events: []controller.ResourceEventMatcher{ + {Kind: &kuadrantv1beta1.KuadrantGroupKind}, + {Kind: &machinery.GatewayClassGroupKind}, + {Kind: &machinery.GatewayGroupKind}, + {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.AuthPolicyGroupKind}, + {Kind: &kuadrantistio.EnvoyFilterGroupKind}, + }, + } +} + +func (r *IstioAuthClusterReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("IstioAuthClusterReconciler") + + logger.V(1).Info("building istio auth clusters") + defer logger.V(1).Info("finished building istio auth clusters") + + kuadrant, err := GetKuadrantFromTopology(topology) + if err != nil { + if errors.Is(err, ErrMissingKuadrant) { + logger.V(1).Info(err.Error()) + return nil + } + return err + } + + authorinoObj, found := lo.Find(topology.Objects().Children(kuadrant), func(child machinery.Object) bool { + return child.GroupVersionKind().GroupKind() == kuadrantv1beta1.AuthorinoGroupKind + }) + if !found { + logger.V(1).Info(ErrMissingAuthorino.Error()) + return nil + } + authorino := authorinoObj.(*controller.RuntimeObject).Object.(*authorinooperatorv1beta1.Authorino) + + effectivePolicies, ok := state.Load(StateEffectiveAuthPolicies) + if !ok { + logger.Error(ErrMissingStateEffectiveAuthPolicies, "failed to get effective auth policies from state") + return nil + } + + gateways := lo.UniqBy(lo.FilterMap(lo.Values(effectivePolicies.(EffectiveAuthPolicies)), func(effectivePolicy EffectiveAuthPolicy, _ int) (*machinery.Gateway, bool) { + gatewayClass, gateway, _, _, _, _ := common.ObjectsInRequestPath(effectivePolicy.Path) + return gateway, gatewayClass.Spec.ControllerName == istioGatewayControllerName + }), func(gateway *machinery.Gateway) string { + return gateway.GetLocator() + }) + + desiredEnvoyFilters := make(map[k8stypes.NamespacedName]struct{}) + var modifiedGateways []string + + // reconcile istio cluster for gateway + for _, gateway := range gateways { + gatewayKey := k8stypes.NamespacedName{Name: gateway.GetName(), Namespace: gateway.GetNamespace()} + + desiredEnvoyFilter, err := r.buildDesiredEnvoyFilter(authorino, gateway) + if err != nil { + logger.Error(err, "failed to build desired envoy filter") + continue + } + desiredEnvoyFilters[k8stypes.NamespacedName{Name: desiredEnvoyFilter.GetName(), Namespace: desiredEnvoyFilter.GetNamespace()}] = struct{}{} + + resource := r.client.Resource(kuadrantistio.EnvoyFiltersResource).Namespace(desiredEnvoyFilter.GetNamespace()) + + existingEnvoyFilterObj, found := lo.Find(topology.Objects().Children(gateway), func(child machinery.Object) bool { + return child.GroupVersionKind().GroupKind() == kuadrantistio.EnvoyFilterGroupKind && child.GetName() == desiredEnvoyFilter.GetName() && child.GetNamespace() == desiredEnvoyFilter.GetNamespace() && labels.Set(child.(*controller.RuntimeObject).GetLabels()).AsSelector().Matches(labels.Set(desiredEnvoyFilter.GetLabels())) + }) + + // create + if !found { + modifiedGateways = append(modifiedGateways, gateway.GetLocator()) // we only signal the gateway as modified when an envoyfilter is created, because updates won't change the status + desiredEnvoyFilterUnstructured, err := controller.Destruct(desiredEnvoyFilter) + if err != nil { + logger.Error(err, "failed to destruct envoyfilter object", "gateway", gatewayKey.String(), "envoyfilter", desiredEnvoyFilter) + continue + } + if _, err = resource.Create(ctx, desiredEnvoyFilterUnstructured, metav1.CreateOptions{}); err != nil { + logger.Error(err, "failed to create envoyfilter object", "gateway", gatewayKey.String(), "envoyfilter", desiredEnvoyFilterUnstructured.Object) + // TODO: handle error + } + continue + } + + existingEnvoyFilter := existingEnvoyFilterObj.(*controller.RuntimeObject).Object.(*istioclientgonetworkingv1alpha3.EnvoyFilter) + + if kuadrantistio.EqualEnvoyFilters(existingEnvoyFilter, desiredEnvoyFilter) { + logger.V(1).Info("envoyfilter object is up to date, nothing to do") + continue + } + + // update + existingEnvoyFilter.Spec = istioapinetworkingv1alpha3.EnvoyFilter{ + TargetRefs: desiredEnvoyFilter.Spec.TargetRefs, + ConfigPatches: desiredEnvoyFilter.Spec.ConfigPatches, + Priority: desiredEnvoyFilter.Spec.Priority, + } + + existingEnvoyFilterUnstructured, err := controller.Destruct(existingEnvoyFilter) + if err != nil { + logger.Error(err, "failed to destruct envoyfilter object", "gateway", gatewayKey.String(), "envoyfilter", existingEnvoyFilter) + continue + } + if _, err = resource.Update(ctx, existingEnvoyFilterUnstructured, metav1.UpdateOptions{}); err != nil { + logger.Error(err, "failed to update envoyfilter object", "gateway", gatewayKey.String(), "envoyfilter", existingEnvoyFilterUnstructured.Object) + // TODO: handle error + } + } + + state.Store(StateIstioAuthClustersModified, modifiedGateways) + + // cleanup istio clusters for gateways that are not in the effective policies + staleEnvoyFilters := topology.Objects().Items(func(o machinery.Object) bool { + _, desired := desiredEnvoyFilters[k8stypes.NamespacedName{Name: o.GetName(), Namespace: o.GetNamespace()}] + return o.GroupVersionKind().GroupKind() == kuadrantistio.EnvoyFilterGroupKind && labels.Set(o.(*controller.RuntimeObject).GetLabels()).AsSelector().Matches(AuthObjectLabels()) && !desired + }) + for _, envoyFilter := range staleEnvoyFilters { + if err := r.client.Resource(kuadrantistio.EnvoyFiltersResource).Namespace(envoyFilter.GetNamespace()).Delete(ctx, envoyFilter.GetName(), metav1.DeleteOptions{}); err != nil { + logger.Error(err, "failed to delete envoyfilter object", "envoyfilter", fmt.Sprintf("%s/%s", envoyFilter.GetNamespace(), envoyFilter.GetName())) + // TODO: handle error + } + } + + return nil +} + +func (r *IstioAuthClusterReconciler) buildDesiredEnvoyFilter(authorino *authorinooperatorv1beta1.Authorino, gateway *machinery.Gateway) (*istioclientgonetworkingv1alpha3.EnvoyFilter, error) { + envoyFilter := &istioclientgonetworkingv1alpha3.EnvoyFilter{ + TypeMeta: metav1.TypeMeta{ + Kind: kuadrantistio.EnvoyFilterGroupKind.Kind, + APIVersion: istioclientgonetworkingv1alpha3.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: AuthClusterName(gateway.GetName()), + Namespace: gateway.GetNamespace(), + Labels: AuthObjectLabels(), + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: gateway.GroupVersionKind().GroupVersion().String(), + Kind: gateway.GroupVersionKind().Kind, + Name: gateway.Name, + UID: gateway.UID, + BlockOwnerDeletion: ptr.To(true), + Controller: ptr.To(true), + }, + }, + }, + Spec: istioapinetworkingv1alpha3.EnvoyFilter{ + TargetRefs: []*istiov1beta1.PolicyTargetReference{ + { + Group: machinery.GatewayGroupKind.Group, + Kind: machinery.GatewayGroupKind.Kind, + Name: gateway.GetName(), + }, + }, + }, + } + + authorinoServiceInfo := authorinoServiceInfoFromAuthorino(authorino) + configPatches, err := kuadrantistio.BuildEnvoyFilterClusterPatch(authorinoServiceInfo.Host, int(authorinoServiceInfo.Port), authClusterPatch) + if err != nil { + return nil, err + } + envoyFilter.Spec.ConfigPatches = configPatches + + return envoyFilter, nil +} diff --git a/controllers/istio_extension_reconciler.go b/controllers/istio_extension_reconciler.go index 5ff22ac85..3e28e36b2 100644 --- a/controllers/istio_extension_reconciler.go +++ b/controllers/istio_extension_reconciler.go @@ -27,27 +27,29 @@ import ( "github.com/kuadrant/kuadrant-operator/pkg/wasm" ) -// istioExtensionReconciler reconciles Istio WasmPlugin custom resources -type istioExtensionReconciler struct { +// IstioExtensionReconciler reconciles Istio WasmPlugin custom resources +type IstioExtensionReconciler struct { client *dynamic.DynamicClient } -func (r *istioExtensionReconciler) Subscription() controller.Subscription { +// IstioExtensionReconciler subscribes to events with potential impact on the Istio WasmPlugin custom resources +func (r *IstioExtensionReconciler) Subscription() controller.Subscription { return controller.Subscription{ ReconcileFunc: r.Reconcile, - Events: []controller.ResourceEventMatcher{ // matches reconciliation events that change the rate limit definitions or status of rate limit policies + Events: []controller.ResourceEventMatcher{ {Kind: &kuadrantv1beta1.KuadrantGroupKind}, {Kind: &machinery.GatewayClassGroupKind}, {Kind: &machinery.GatewayGroupKind}, {Kind: &machinery.HTTPRouteGroupKind}, {Kind: &kuadrantv1beta3.RateLimitPolicyGroupKind}, + {Kind: &kuadrantv1beta3.AuthPolicyGroupKind}, {Kind: &kuadrantistio.WasmPluginGroupKind}, }, } } -func (r *istioExtensionReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { - logger := controller.LoggerFromContext(ctx).WithName("istioExtensionReconciler") +func (r *IstioExtensionReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("IstioExtensionReconciler") logger.V(1).Info("building istio extension") defer logger.V(1).Info("finished building istio extension") @@ -55,7 +57,7 @@ func (r *istioExtensionReconciler) Reconcile(ctx context.Context, _ []controller // build wasm plugin configs for each gateway wasmConfigs, err := r.buildWasmConfigs(ctx, state) if err != nil { - if errors.Is(err, ErrMissingStateEffectiveRateLimitPolicies) { + if errors.Is(err, ErrMissingStateEffectiveAuthPolicies) || errors.Is(err, ErrMissingStateEffectiveRateLimitPolicies) { logger.V(1).Info(err.Error()) } else { return err @@ -139,28 +141,59 @@ func (r *istioExtensionReconciler) Reconcile(ctx context.Context, _ []controller } // buildWasmConfigs returns a map of istio gateway locators to an ordered list of corresponding wasm policies -func (r *istioExtensionReconciler) buildWasmConfigs(ctx context.Context, state *sync.Map) (map[string]wasm.Config, error) { - logger := controller.LoggerFromContext(ctx).WithName("istioExtensionReconciler").WithName("buildWasmConfigs") +func (r *IstioExtensionReconciler) buildWasmConfigs(ctx context.Context, state *sync.Map) (map[string]wasm.Config, error) { + logger := controller.LoggerFromContext(ctx).WithName("IstioExtensionReconciler").WithName("buildWasmConfigs") - effectivePolicies, ok := state.Load(StateEffectiveRateLimitPolicies) + effectiveAuthPolicies, ok := state.Load(StateEffectiveAuthPolicies) + if !ok { + return nil, ErrMissingStateEffectiveAuthPolicies + } + effectiveAuthPoliciesMap := effectiveAuthPolicies.(EffectiveAuthPolicies) + + effectiveRateLimitPolicies, ok := state.Load(StateEffectiveRateLimitPolicies) if !ok { return nil, ErrMissingStateEffectiveRateLimitPolicies } + effectiveRateLimitPoliciesMap := effectiveRateLimitPolicies.(EffectiveRateLimitPolicies) + + logger.V(1).Info("building wasm configs for istio extension", "effectiveRateLimitPolicies", len(effectiveRateLimitPoliciesMap)) - logger.V(1).Info("building wasm configs for istio extension", "effectivePolicies", len(effectivePolicies.(EffectiveRateLimitPolicies))) + paths := lo.UniqBy(append( + lo.Entries(lo.MapValues(effectiveAuthPoliciesMap, func(p EffectiveAuthPolicy, _ string) []machinery.Targetable { return p.Path })), + lo.Entries(lo.MapValues(effectiveRateLimitPoliciesMap, func(p EffectiveRateLimitPolicy, _ string) []machinery.Targetable { return p.Path }))..., + ), func(e lo.Entry[string, []machinery.Targetable]) string { return e.Key }) wasmActionSets := kuadrantgatewayapi.GrouppedHTTPRouteMatchConfigs{} // build the wasm policies for each topological path that contains an effective rate limit policy affecting an istio gateway - for pathID, effectivePolicy := range effectivePolicies.(EffectiveRateLimitPolicies) { - gatewayClass, gateway, _, _, _, _ := common.ObjectsInRequestPath(effectivePolicy.Path) + for i := range paths { + pathID := paths[i].Key + path := paths[i].Value + + gatewayClass, gateway, _, _, _, _ := common.ObjectsInRequestPath(path) // ignore if not an istio gateway if gatewayClass.Spec.ControllerName != istioGatewayControllerName { continue } - wasmActionSetsForPath, err := wasm.BuildActionSetsForPath(pathID, effectivePolicy.Path, effectivePolicy.Spec.Rules(), rateLimitWasmActionBuilder(pathID, effectivePolicy, state)) + var actions []wasm.Action + + // auth + if effectivePolicy, ok := effectiveAuthPoliciesMap[pathID]; ok { + actions = append(actions, buildWasmActionsForAuth(pathID, effectivePolicy)...) + } + + // rate limit + if effectivePolicy, ok := effectiveRateLimitPoliciesMap[pathID]; ok { + actions = append(actions, buildWasmActionsForRateLimit(effectivePolicy, state)...) + } + + if len(actions) == 0 { + continue + } + + wasmActionSetsForPath, err := wasm.BuildActionSetsForPath(pathID, path, actions) if err != nil { logger.Error(err, "failed to build wasm policies for path", "pathID", pathID) continue diff --git a/controllers/istio_rate_limit_cluster_reconciler.go b/controllers/istio_ratelimit_cluster_reconciler.go similarity index 70% rename from controllers/istio_rate_limit_cluster_reconciler.go rename to controllers/istio_ratelimit_cluster_reconciler.go index 10c20b792..37462748f 100644 --- a/controllers/istio_rate_limit_cluster_reconciler.go +++ b/controllers/istio_ratelimit_cluster_reconciler.go @@ -2,7 +2,6 @@ package controllers import ( "context" - "encoding/json" "errors" "fmt" "sync" @@ -26,15 +25,16 @@ import ( kuadrantistio "github.com/kuadrant/kuadrant-operator/pkg/istio" ) -// istioRateLimitClusterReconciler reconciles Istio EnvoyFilter custom resources -type istioRateLimitClusterReconciler struct { +// IstioRateLimitClusterReconciler reconciles Istio EnvoyFilter custom resources for rate limiting +type IstioRateLimitClusterReconciler struct { client *dynamic.DynamicClient } -func (r *istioRateLimitClusterReconciler) Subscription() controller.Subscription { +// IstioRateLimitClusterReconciler subscribes to events with potential impact on the Istio EnvoyFilter custom resources for rate limiting +func (r *IstioRateLimitClusterReconciler) Subscription() controller.Subscription { return controller.Subscription{ ReconcileFunc: r.Reconcile, - Events: []controller.ResourceEventMatcher{ // matches reconciliation events that change the rate limit definitions or status of rate limit policies + Events: []controller.ResourceEventMatcher{ {Kind: &kuadrantv1beta1.KuadrantGroupKind}, {Kind: &machinery.GatewayClassGroupKind}, {Kind: &machinery.GatewayGroupKind}, @@ -45,8 +45,8 @@ func (r *istioRateLimitClusterReconciler) Subscription() controller.Subscription } } -func (r *istioRateLimitClusterReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { - logger := controller.LoggerFromContext(ctx).WithName("istioRateLimitClusterReconciler") +func (r *IstioRateLimitClusterReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("IstioRateLimitClusterReconciler") logger.V(1).Info("building istio rate limit clusters") defer logger.V(1).Info("finished building istio rate limit clusters") @@ -119,7 +119,7 @@ func (r *istioRateLimitClusterReconciler) Reconcile(ctx context.Context, _ []con existingEnvoyFilter := existingEnvoyFilterObj.(*controller.RuntimeObject).Object.(*istioclientgonetworkingv1alpha3.EnvoyFilter) - if equalEnvoyFilters(existingEnvoyFilter, desiredEnvoyFilter) { + if kuadrantistio.EqualEnvoyFilters(existingEnvoyFilter, desiredEnvoyFilter) { logger.V(1).Info("envoyfilter object is up to date, nothing to do") continue } @@ -149,7 +149,6 @@ func (r *istioRateLimitClusterReconciler) Reconcile(ctx context.Context, _ []con _, desired := desiredEnvoyFilters[k8stypes.NamespacedName{Name: o.GetName(), Namespace: o.GetNamespace()}] return o.GroupVersionKind().GroupKind() == kuadrantistio.EnvoyFilterGroupKind && labels.Set(o.(*controller.RuntimeObject).GetLabels()).AsSelector().Matches(RateLimitObjectLabels()) && !desired }) - for _, envoyFilter := range staleEnvoyFilters { if err := r.client.Resource(kuadrantistio.EnvoyFiltersResource).Namespace(envoyFilter.GetNamespace()).Delete(ctx, envoyFilter.GetName(), metav1.DeleteOptions{}); err != nil { logger.Error(err, "failed to delete envoyfilter object", "envoyfilter", fmt.Sprintf("%s/%s", envoyFilter.GetNamespace(), envoyFilter.GetName())) @@ -160,7 +159,7 @@ func (r *istioRateLimitClusterReconciler) Reconcile(ctx context.Context, _ []con return nil } -func (r *istioRateLimitClusterReconciler) buildDesiredEnvoyFilter(limitador *limitadorv1alpha1.Limitador, gateway *machinery.Gateway) (*istioclientgonetworkingv1alpha3.EnvoyFilter, error) { +func (r *IstioRateLimitClusterReconciler) buildDesiredEnvoyFilter(limitador *limitadorv1alpha1.Limitador, gateway *machinery.Gateway) (*istioclientgonetworkingv1alpha3.EnvoyFilter, error) { envoyFilter := &istioclientgonetworkingv1alpha3.EnvoyFilter{ TypeMeta: metav1.TypeMeta{ Kind: kuadrantistio.EnvoyFilterGroupKind.Kind, @@ -192,7 +191,11 @@ func (r *istioRateLimitClusterReconciler) buildDesiredEnvoyFilter(limitador *lim }, } - configPatches, err := istioEnvoyFilterClusterPatch(limitador.Status.Service.Host, int(limitador.Status.Service.Ports.GRPC)) + limitadorService := limitador.Status.Service + if limitadorService == nil { + return nil, ErrMissingLimitadorServiceInfo + } + configPatches, err := kuadrantistio.BuildEnvoyFilterClusterPatch(limitador.Status.Service.Host, int(limitador.Status.Service.Ports.GRPC), rateLimitClusterPatch) if err != nil { return nil, err } @@ -200,76 +203,3 @@ func (r *istioRateLimitClusterReconciler) buildDesiredEnvoyFilter(limitador *lim return envoyFilter, nil } - -// istioEnvoyFilterClusterPatch returns an envoy config patch that defines the rate limit cluster for the gateway. -// The rate limit cluster configures the endpoint of the external rate limit service. -func istioEnvoyFilterClusterPatch(host string, port int) ([]*istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch, error) { - patchRaw, _ := json.Marshal(map[string]any{"operation": "ADD", "value": rateLimitClusterPatch(host, port)}) - patch := &istioapinetworkingv1alpha3.EnvoyFilter_Patch{} - if err := patch.UnmarshalJSON(patchRaw); err != nil { - return nil, err - } - - return []*istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch{ - { - ApplyTo: istioapinetworkingv1alpha3.EnvoyFilter_CLUSTER, - Match: &istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectMatch{ - ObjectTypes: &istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectMatch_Cluster{ - Cluster: &istioapinetworkingv1alpha3.EnvoyFilter_ClusterMatch{ - Service: host, - }, - }, - }, - Patch: patch, - }, - }, nil -} - -func equalEnvoyFilters(a, b *istioclientgonetworkingv1alpha3.EnvoyFilter) bool { - if a.Spec.Priority != b.Spec.Priority || !kuadrantistio.EqualTargetRefs(a.Spec.TargetRefs, b.Spec.TargetRefs) { - return false - } - - aConfigPatches := a.Spec.ConfigPatches - bConfigPatches := b.Spec.ConfigPatches - if len(aConfigPatches) != len(bConfigPatches) { - return false - } - return lo.EveryBy(aConfigPatches, func(aConfigPatch *istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch) bool { - return lo.SomeBy(bConfigPatches, func(bConfigPatch *istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch) bool { - if aConfigPatch == nil && bConfigPatch == nil { - return true - } - if (aConfigPatch == nil && bConfigPatch != nil) || (aConfigPatch != nil && bConfigPatch == nil) { - return false - } - - // apply_to - if aConfigPatch.ApplyTo != bConfigPatch.ApplyTo { - return false - } - - // cluster match - aCluster := aConfigPatch.Match.GetCluster() - bCluster := bConfigPatch.Match.GetCluster() - if aCluster == nil || bCluster == nil { - return false - } - if aCluster.Service != bCluster.Service || aCluster.PortNumber != bCluster.PortNumber || aCluster.Subset != bCluster.Subset { - return false - } - - // patch - aPatch := aConfigPatch.Patch - bPatch := bConfigPatch.Patch - - if aPatch.Operation != bPatch.Operation || aPatch.FilterClass != bPatch.FilterClass { - return false - } - - aPatchJSON, _ := aPatch.Value.MarshalJSON() - bPatchJSON, _ := aPatch.Value.MarshalJSON() - return string(aPatchJSON) == string(bPatchJSON) - }) - }) -} diff --git a/controllers/kuadrant_controller.go b/controllers/kuadrant_controller.go index a3ce7f462..d77ed1a17 100644 --- a/controllers/kuadrant_controller.go +++ b/controllers/kuadrant_controller.go @@ -23,21 +23,14 @@ import ( "github.com/go-logr/logr" authorinov1beta1 "github.com/kuadrant/authorino-operator/api/v1beta1" limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" - iopv1alpha1 "istio.io/istio/operator/pkg/apis/istio/v1alpha1" - corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "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" - 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/istio" kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" "github.com/kuadrant/kuadrant-operator/pkg/log" @@ -124,10 +117,6 @@ func (r *KuadrantReconciler) Reconcile(eventCtx context.Context, req ctrl.Reques if kObj.GetDeletionTimestamp() != nil && controllerutil.ContainsFinalizer(kObj, kuadrantFinalizer) { logger.V(1).Info("Handling removal of kuadrant object") - if err := r.unregisterExternalAuthorizer(ctx, kObj); err != nil { - return ctrl.Result{}, err - } - logger.Info("removing finalizer") controllerutil.RemoveFinalizer(kObj, kuadrantFinalizer) if err := r.Client().Update(ctx, kObj); client.IgnoreNotFound(err) != nil { @@ -149,16 +138,9 @@ func (r *KuadrantReconciler) Reconcile(eventCtx context.Context, req ctrl.Reques } } - specErr := r.reconcileSpec(ctx, kObj) - - statusResult, statusErr := r.reconcileStatus(ctx, kObj, specErr) - - if specErr != nil { - return ctrl.Result{}, specErr - } - - if statusErr != nil { - return ctrl.Result{}, statusErr + statusResult, err := r.reconcileStatus(ctx, kObj, nil) + if err != nil { + return ctrl.Result{}, err } if statusResult.Requeue { @@ -170,233 +152,6 @@ func (r *KuadrantReconciler) Reconcile(eventCtx context.Context, req ctrl.Reques return ctrl.Result{}, nil } -func (r *KuadrantReconciler) unregisterExternalAuthorizer(ctx context.Context, kObj *kuadrantv1beta1.Kuadrant) error { - isIstioInstalled, err := r.unregisterExternalAuthorizerIstio(ctx, kObj) - - if err == nil && !isIstioInstalled { - err = r.unregisterExternalAuthorizerOSSM(ctx, kObj) - } - - return err -} - -func (r *KuadrantReconciler) unregisterExternalAuthorizerIstio(ctx context.Context, kObj *kuadrantv1beta1.Kuadrant) (bool, error) { - logger, _ := logr.FromContext(ctx) - configsToUpdate, err := r.getIstioConfigObjects(ctx) - isIstioInstalled := configsToUpdate != nil - - if !isIstioInstalled || err != nil { - return isIstioInstalled, err - } - - kuadrantAuthorizer := istio.NewKuadrantAuthorizer(kObj.Namespace) - - for _, config := range configsToUpdate { - hasAuthorizer, err := istio.HasKuadrantAuthorizer(config, *kuadrantAuthorizer) - if err != nil { - return true, err - } - if hasAuthorizer { - if err = istio.UnregisterKuadrantAuthorizer(config, kuadrantAuthorizer); err != nil { - return true, err - } - - logger.Info("remove external authorizer from istio meshconfig") - if err = r.UpdateResource(ctx, config.GetConfigObject()); err != nil { - return true, err - } - } - } - return true, nil -} - -func (r *KuadrantReconciler) unregisterExternalAuthorizerOSSM(ctx context.Context, kObj *kuadrantv1beta1.Kuadrant) error { - logger, _ := logr.FromContext(ctx) - - smcp := &maistrav2.ServiceMeshControlPlane{} - - smcpKey := client.ObjectKey{Name: controlPlaneProviderName(), Namespace: controlPlaneProviderNamespace()} - if err := r.Client().Get(ctx, smcpKey, smcp); err != nil { - logger.V(1).Info("failed to get servicemeshcontrolplane object", "key", smcp, "err", err) - if apierrors.IsNotFound(err) || meta.IsNoMatchError(err) { - logger.Info("OSSM installation as GatewayAPI provider not found") - return nil - } - return err - } - - smcpWrapper := istio.NewOSSMControlPlaneWrapper(smcp) - kuadrantAuthorizer := istio.NewKuadrantAuthorizer(kObj.Namespace) - - hasAuthorizer, err := istio.HasKuadrantAuthorizer(smcpWrapper, *kuadrantAuthorizer) - if err != nil { - return err - } - if hasAuthorizer { - err = istio.UnregisterKuadrantAuthorizer(smcpWrapper, kuadrantAuthorizer) - if err != nil { - return err - } - logger.Info("removing external authorizer from OSSM meshconfig") - if err := r.UpdateResource(ctx, smcpWrapper.GetConfigObject()); err != nil { - return err - } - } - - return nil -} - -func (r *KuadrantReconciler) registerExternalAuthorizer(ctx context.Context, kObj *kuadrantv1beta1.Kuadrant) error { - isIstioInstalled, err := r.registerExternalAuthorizerIstio(ctx, kObj) - - if err == nil && !isIstioInstalled { - err = r.registerExternalAuthorizerOSSM(ctx, kObj) - } - - return err -} - -func (r *KuadrantReconciler) registerExternalAuthorizerIstio(ctx context.Context, kObj *kuadrantv1beta1.Kuadrant) (bool, error) { - logger, _ := logr.FromContext(ctx) - configsToUpdate, err := r.getIstioConfigObjects(ctx) - isIstioInstalled := configsToUpdate != nil - - if !isIstioInstalled || err != nil { - return isIstioInstalled, err - } - - kuadrantAuthorizer := istio.NewKuadrantAuthorizer(kObj.Namespace) - for _, config := range configsToUpdate { - hasKuadrantAuthorizer, err := istio.HasKuadrantAuthorizer(config, *kuadrantAuthorizer) - if err != nil { - return true, err - } - if !hasKuadrantAuthorizer { - err = istio.RegisterKuadrantAuthorizer(config, kuadrantAuthorizer) - if err != nil { - return true, err - } - logger.Info("adding external authorizer to istio meshconfig") - if err = r.UpdateResource(ctx, config.GetConfigObject()); err != nil { - return true, err - } - } - } - - return true, nil -} - -func (r *KuadrantReconciler) registerExternalAuthorizerOSSM(ctx context.Context, kObj *kuadrantv1beta1.Kuadrant) error { - logger, _ := logr.FromContext(ctx) - - smcp := &maistrav2.ServiceMeshControlPlane{} - - smcpKey := client.ObjectKey{Name: controlPlaneProviderName(), Namespace: controlPlaneProviderNamespace()} - if err := r.GetResource(ctx, smcpKey, smcp); err != nil { - logger.V(1).Info("failed to get servicemeshcontrolplane object", "key", smcp, "err", err) - if apierrors.IsNotFound(err) || meta.IsNoMatchError(err) { - logger.Info("OSSM installation as GatewayAPI provider not found") - return nil - } - } - - if err := r.registerServiceMeshMember(ctx, kObj); err != nil { - return err - } - - smcpWrapper := istio.NewOSSMControlPlaneWrapper(smcp) - kuadrantAuthorizer := istio.NewKuadrantAuthorizer(kObj.Namespace) - - hasAuthorizer, err := istio.HasKuadrantAuthorizer(smcpWrapper, *kuadrantAuthorizer) - if err != nil { - return err - } - if !hasAuthorizer { - err = istio.RegisterKuadrantAuthorizer(smcpWrapper, kuadrantAuthorizer) - if err != nil { - return err - } - logger.Info("adding external authorizer to OSSM meshconfig") - if err := r.UpdateResource(ctx, smcpWrapper.GetConfigObject()); err != nil { - return err - } - } - - return nil -} - -func (r *KuadrantReconciler) getIstioConfigObjects(ctx context.Context) ([]istio.ConfigWrapper, error) { - logger, _ := logr.FromContext(ctx) - var configsToUpdate []istio.ConfigWrapper - - iop := &iopv1alpha1.IstioOperator{} - istKey := client.ObjectKey{Name: controlPlaneProviderName(), Namespace: controlPlaneProviderNamespace()} - err := r.GetResource(ctx, istKey, iop) - // TODO(eguzki): 🔥 this spaghetti code 🔥 - if err == nil { - configsToUpdate = append(configsToUpdate, istio.NewOperatorWrapper(iop)) - } else if meta.IsNoMatchError(err) || apierrors.IsNotFound(err) { - // IstioOperator not existing or not CRD not found, so check for Istio CR instead - ist := &istiov1alpha1.Istio{} - istKey := client.ObjectKey{Name: istioCRName} - if err := r.GetResource(ctx, istKey, ist); err != nil { - logger.V(1).Info("failed to get istio object", "key", istKey, "err", err) - if meta.IsNoMatchError(err) || apierrors.IsNotFound(err) { - // return nil and nil if there's no istiooperator or istio CR - logger.Info("Istio installation as GatewayAPI provider not found") - return nil, nil - } - // return nil and err if there's an error other than not found (no istio CR) - return nil, err - } - configsToUpdate = append(configsToUpdate, istio.NewSailWrapper(ist)) - } else { - logger.V(1).Info("failed to get istiooperator object", "key", istKey, "err", err) - return nil, err - } - - istioConfigMap := &corev1.ConfigMap{} - if err := r.GetResource(ctx, client.ObjectKey{Name: controlPlaneConfigMapName(), Namespace: controlPlaneProviderNamespace()}, istioConfigMap); err != nil { - if !apierrors.IsNotFound(err) { - logger.V(1).Info("failed to get istio configMap", "key", istKey, "err", err) - return configsToUpdate, err - } - } else { - configsToUpdate = append(configsToUpdate, istio.NewConfigMapWrapper(istioConfigMap)) - } - return configsToUpdate, nil -} - -func (r *KuadrantReconciler) registerServiceMeshMember(ctx context.Context, kObj *kuadrantv1beta1.Kuadrant) error { - member := &maistrav1.ServiceMeshMember{ - TypeMeta: metav1.TypeMeta{ - Kind: "ServiceMeshMember", - APIVersion: maistrav1.SchemeGroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "default", - Namespace: kObj.Namespace, - }, - Spec: maistrav1.ServiceMeshMemberSpec{ - ControlPlaneRef: maistrav1.ServiceMeshControlPlaneRef{ - Name: controlPlaneProviderName(), - Namespace: controlPlaneProviderNamespace(), - }, - }, - } - - err := r.SetOwnerReference(kObj, member) - if err != nil { - return err - } - - return r.ReconcileResource(ctx, &maistrav1.ServiceMeshMember{}, member, reconcilers.CreateOnlyMutator) -} - -func (r *KuadrantReconciler) reconcileSpec(ctx context.Context, kObj *kuadrantv1beta1.Kuadrant) error { - return r.registerExternalAuthorizer(ctx, kObj) -} - // SetupWithManager sets up the controller with the Manager. func (r *KuadrantReconciler) SetupWithManager(mgr ctrl.Manager) error { ok, err := kuadrantgatewayapi.IsGatewayAPIInstalled(mgr.GetRESTMapper()) diff --git a/controllers/limitador_limits_reconciler.go b/controllers/limitador_limits_reconciler.go index 6fcde547c..dd4548654 100644 --- a/controllers/limitador_limits_reconciler.go +++ b/controllers/limitador_limits_reconciler.go @@ -22,19 +22,27 @@ import ( "github.com/kuadrant/kuadrant-operator/pkg/ratelimit" ) -type limitadorLimitsReconciler struct { +type LimitadorLimitsReconciler struct { client *dynamic.DynamicClient } -func (r *limitadorLimitsReconciler) Subscription() controller.Subscription { +// LimitadorLimitsReconciler reconciles to events with impact to change the state of the Limitador custom resources regarding the definitions for the effective rate limit policies +func (r *LimitadorLimitsReconciler) Subscription() controller.Subscription { return controller.Subscription{ ReconcileFunc: r.Reconcile, - Events: rateLimitEventMatchers, + Events: []controller.ResourceEventMatcher{ + {Kind: &kuadrantv1beta1.KuadrantGroupKind}, + {Kind: &machinery.GatewayClassGroupKind}, + {Kind: &machinery.GatewayGroupKind}, + {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.RateLimitPolicyGroupKind}, + {Kind: &kuadrantv1beta1.LimitadorGroupKind}, + }, } } -func (r *limitadorLimitsReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { - logger := controller.LoggerFromContext(ctx).WithName("limitadorLimitsReconciler") +func (r *LimitadorLimitsReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("LimitadorLimitsReconciler") limitador, err := GetLimitadorFromTopology(topology) if err != nil { @@ -77,19 +85,20 @@ func (r *limitadorLimitsReconciler) Reconcile(ctx context.Context, _ []controlle return nil } -func (r *limitadorLimitsReconciler) buildLimitadorLimits(ctx context.Context, state *sync.Map) ([]limitadorv1alpha1.RateLimit, error) { - logger := controller.LoggerFromContext(ctx).WithName("limitadorLimitsReconciler").WithName("buildLimitadorLimits") +func (r *LimitadorLimitsReconciler) buildLimitadorLimits(ctx context.Context, state *sync.Map) ([]limitadorv1alpha1.RateLimit, error) { + logger := controller.LoggerFromContext(ctx).WithName("LimitadorLimitsReconciler").WithName("buildLimitadorLimits") effectivePolicies, ok := state.Load(StateEffectiveRateLimitPolicies) if !ok { return nil, ErrMissingStateEffectiveRateLimitPolicies } + effectivePoliciesMap := effectivePolicies.(EffectiveRateLimitPolicies) - logger.V(1).Info("building limitador limits", "effectivePolicies", len(effectivePolicies.(EffectiveRateLimitPolicies))) + logger.V(1).Info("building limitador limits", "effectivePolicies", len(effectivePoliciesMap)) rateLimitIndex := ratelimit.NewIndex() - for pathID, effectivePolicy := range effectivePolicies.(EffectiveRateLimitPolicies) { + for pathID, effectivePolicy := range effectivePoliciesMap { _, _, _, httpRoute, _, _ := common.ObjectsInRequestPath(effectivePolicy.Path) limitsNamespace := LimitsNamespaceFromRoute(httpRoute.HTTPRoute) for limitKey, mergeableLimit := range effectivePolicy.Spec.Rules() { @@ -101,7 +110,7 @@ func (r *limitadorLimitsReconciler) buildLimitadorLimits(ctx context.Context, st continue } limitIdentifier := LimitNameToLimitadorIdentifier(k8stypes.NamespacedName{Name: policy.GetName(), Namespace: policy.GetNamespace()}, limitKey) - limit := mergeableLimit.GetSpec().(kuadrantv1beta3.Limit) + limit := mergeableLimit.GetSpec().(*kuadrantv1beta3.Limit) rateLimits := lo.Map(limit.Rates, func(rate kuadrantv1beta3.Rate, _ int) limitadorv1alpha1.RateLimit { maxValue, seconds := rate.ToSeconds() return limitadorv1alpha1.RateLimit{ diff --git a/controllers/ratelimitpolicies_validator.go b/controllers/ratelimit_policies_validator.go similarity index 85% rename from controllers/ratelimitpolicies_validator.go rename to controllers/ratelimit_policies_validator.go index 76a89d3da..ccd9fdb27 100644 --- a/controllers/ratelimitpolicies_validator.go +++ b/controllers/ratelimit_policies_validator.go @@ -15,9 +15,10 @@ import ( kuadrant "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" ) -type rateLimitPolicyValidator struct{} +type RateLimitPolicyValidator struct{} -func (r *rateLimitPolicyValidator) Subscription() controller.Subscription { +// RateLimitPolicyValidator subscribes to events with potential to flip the validity of rate limit policies +func (r *RateLimitPolicyValidator) Subscription() controller.Subscription { return controller.Subscription{ ReconcileFunc: r.Validate, Events: []controller.ResourceEventMatcher{ @@ -29,8 +30,8 @@ func (r *rateLimitPolicyValidator) Subscription() controller.Subscription { } } -func (r *rateLimitPolicyValidator) Validate(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { - logger := controller.LoggerFromContext(ctx).WithName("rateLimitPolicyValidator") +func (r *RateLimitPolicyValidator) Validate(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("RateLimitPolicyValidator") policies := topology.Policies().Items(func(o machinery.Object) bool { return o.GroupVersionKind().GroupKind() == kuadrantv1beta3.RateLimitPolicyGroupKind diff --git a/controllers/ratelimitpolicy_status_updater.go b/controllers/ratelimit_policy_status_updater.go similarity index 89% rename from controllers/ratelimitpolicy_status_updater.go rename to controllers/ratelimit_policy_status_updater.go index 59f5c26ce..872db2f6d 100644 --- a/controllers/ratelimitpolicy_status_updater.go +++ b/controllers/ratelimit_policy_status_updater.go @@ -14,7 +14,6 @@ import ( "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" k8stypes "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/dynamic" "k8s.io/utils/ptr" @@ -31,19 +30,31 @@ import ( "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" ) -type rateLimitPolicyStatusUpdater struct { +type RateLimitPolicyStatusUpdater struct { client *dynamic.DynamicClient } -func (r *rateLimitPolicyStatusUpdater) Subscription() controller.Subscription { +// RateLimitPolicyStatusUpdater subscribe to events with potential impact on the status of RateLimitPolicy resources +func (r *RateLimitPolicyStatusUpdater) Subscription() controller.Subscription { return controller.Subscription{ ReconcileFunc: r.UpdateStatus, - Events: rateLimitEventMatchers, + Events: []controller.ResourceEventMatcher{ + {Kind: &kuadrantv1beta1.KuadrantGroupKind}, + {Kind: &machinery.GatewayClassGroupKind}, + {Kind: &machinery.GatewayGroupKind}, + {Kind: &machinery.HTTPRouteGroupKind}, + {Kind: &kuadrantv1beta3.RateLimitPolicyGroupKind}, + {Kind: &kuadrantv1beta1.LimitadorGroupKind}, + {Kind: &kuadrantistio.EnvoyFilterGroupKind}, + {Kind: &kuadrantistio.WasmPluginGroupKind}, + {Kind: &kuadrantenvoygateway.EnvoyPatchPolicyGroupKind}, + {Kind: &kuadrantenvoygateway.EnvoyExtensionPolicyGroupKind}, + }, } } -func (r *rateLimitPolicyStatusUpdater) UpdateStatus(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { - logger := controller.LoggerFromContext(ctx).WithName("rateLimitPolicyStatusUpdater") +func (r *RateLimitPolicyStatusUpdater) UpdateStatus(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, state *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("RateLimitPolicyStatusUpdater") policies := lo.FilterMap(topology.Policies().Items(), func(item machinery.Policy, index int) (*kuadrantv1beta3.RateLimitPolicy, bool) { p, ok := item.(*kuadrantv1beta3.RateLimitPolicy) @@ -52,8 +63,8 @@ func (r *rateLimitPolicyStatusUpdater) UpdateStatus(ctx context.Context, _ []con policyAcceptedFunc := rateLimitPolicyAcceptedStatusFunc(state) - logger.V(1).Info("updating rate limit policy statuses", "policies", len(policies)) - defer logger.V(1).Info("finished updating rate limit policy statuses") + logger.V(1).Info("updating ratelimitpolicy statuses", "policies", len(policies)) + defer logger.V(1).Info("finished updating ratelimitpolicy statuses") for _, policy := range policies { if policy.GetDeletionTimestamp() != nil { @@ -102,7 +113,7 @@ func (r *rateLimitPolicyStatusUpdater) UpdateStatus(ctx context.Context, _ []con return nil } -func (r *rateLimitPolicyStatusUpdater) enforcedCondition(policy *kuadrantv1beta3.RateLimitPolicy, topology *machinery.Topology, state *sync.Map) *metav1.Condition { +func (r *RateLimitPolicyStatusUpdater) enforcedCondition(policy *kuadrantv1beta3.RateLimitPolicy, topology *machinery.Topology, state *sync.Map) *metav1.Condition { policyKind := kuadrantv1beta3.RateLimitPolicyGroupKind.Kind effectivePolicies, ok := state.Load(StateEffectiveRateLimitPolicies) @@ -221,16 +232,3 @@ func (r *rateLimitPolicyStatusUpdater) enforcedCondition(policy *kuadrantv1beta3 return kuadrant.EnforcedCondition(policy, nil, len(overridingPolicies) == 0) } - -func gatewayComponentsToSync(gateway *machinery.Gateway, componentGroupKind schema.GroupKind, modifiedGatewayLocators any, topology *machinery.Topology, requiredCondition func(machinery.Object) bool) []string { - missingConditionInTopologyFunc := func() bool { - obj, found := lo.Find(topology.Objects().Children(gateway), func(child machinery.Object) bool { - return child.GroupVersionKind().GroupKind() == componentGroupKind - }) - return !found || !requiredCondition(obj) - } - if (modifiedGatewayLocators != nil && lo.Contains(modifiedGatewayLocators.([]string), gateway.GetLocator())) || missingConditionInTopologyFunc() { - return []string{fmt.Sprintf("%s (%s/%s)", componentGroupKind.Kind, gateway.GetNamespace(), gateway.GetName())} - } - return nil -} diff --git a/controllers/ratelimit_workflow.go b/controllers/ratelimit_workflow_helpers.go similarity index 68% rename from controllers/ratelimit_workflow.go rename to controllers/ratelimit_workflow_helpers.go index 91b1760d5..1eeb9a166 100644 --- a/controllers/ratelimit_workflow.go +++ b/controllers/ratelimit_workflow_helpers.go @@ -15,8 +15,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" k8stypes "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/dynamic" - "k8s.io/utils/env" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" @@ -24,45 +22,21 @@ import ( kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1" kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" "github.com/kuadrant/kuadrant-operator/pkg/common" - kuadrantenvoygateway "github.com/kuadrant/kuadrant-operator/pkg/envoygateway" - kuadrantistio "github.com/kuadrant/kuadrant-operator/pkg/istio" "github.com/kuadrant/kuadrant-operator/pkg/wasm" ) -const ( - rateLimitClusterLabelKey = "kuadrant.io/rate-limit-cluster" - - // make these configurable? - istioGatewayControllerName = "istio.io/gateway-controller" - envoyGatewayGatewayControllerName = "gateway.envoyproxy.io/gatewayclass-controller" -) +const rateLimitObjectLabelKey = "kuadrant.io/ratelimit" var ( - WASMFilterImageURL = env.GetString("RELATED_IMAGE_WASMSHIM", "oci://quay.io/kuadrant/wasm-shim:latest") - StateRateLimitPolicyValid = "RateLimitPolicyValid" StateEffectiveRateLimitPolicies = "EffectiveRateLimitPolicies" StateLimitadorLimitsModified = "LimitadorLimitsModified" StateIstioRateLimitClustersModified = "IstioRateLimitClustersModified" - StateIstioExtensionsModified = "IstioExtensionsModified" StateEnvoyGatewayRateLimitClustersModified = "EnvoyGatewayRateLimitClustersModified" - StateEnvoyGatewayExtensionsModified = "EnvoyGatewayExtensionsModified" ErrMissingLimitador = fmt.Errorf("missing limitador object in the topology") + ErrMissingLimitadorServiceInfo = fmt.Errorf("missing limitador service info in the limitador object") ErrMissingStateEffectiveRateLimitPolicies = fmt.Errorf("missing rate limit effective policies stored in the reconciliation state") - - rateLimitEventMatchers = []controller.ResourceEventMatcher{ // matches reconciliation events that change the rate limit definitions or status of rate limit policies - {Kind: &kuadrantv1beta1.KuadrantGroupKind}, - {Kind: &machinery.GatewayClassGroupKind}, - {Kind: &machinery.GatewayGroupKind}, - {Kind: &machinery.HTTPRouteGroupKind}, - {Kind: &kuadrantv1beta3.RateLimitPolicyGroupKind}, - {Kind: &kuadrantv1beta1.LimitadorGroupKind}, - {Kind: &kuadrantistio.EnvoyFilterGroupKind}, - {Kind: &kuadrantistio.WasmPluginGroupKind}, - {Kind: &kuadrantenvoygateway.EnvoyPatchPolicyGroupKind}, - {Kind: &kuadrantenvoygateway.EnvoyExtensionPolicyGroupKind}, - } ) //+kubebuilder:rbac:groups=kuadrant.io,resources=ratelimitpolicies,verbs=get;list;watch;create;update;patch;delete @@ -70,31 +44,6 @@ var ( //+kubebuilder:rbac:groups=kuadrant.io,resources=ratelimitpolicies/finalizers,verbs=update //+kubebuilder:rbac:groups=limitador.kuadrant.io,resources=limitadors,verbs=get;list;watch;create;update;patch;delete -func NewRateLimitWorkflow(client *dynamic.DynamicClient, isIstioInstalled, isEnvoyGatewayInstalled bool) *controller.Workflow { - effectiveRateLimitPoliciesWorkflow := &controller.Workflow{ - Precondition: (&effectiveRateLimitPolicyReconciler{client: client}).Subscription().Reconcile, - Tasks: []controller.ReconcileFunc{ - (&limitadorLimitsReconciler{client: client}).Subscription().Reconcile, - }, - } - - if isIstioInstalled { - effectiveRateLimitPoliciesWorkflow.Tasks = append(effectiveRateLimitPoliciesWorkflow.Tasks, (&istioRateLimitClusterReconciler{client: client}).Subscription().Reconcile) - effectiveRateLimitPoliciesWorkflow.Tasks = append(effectiveRateLimitPoliciesWorkflow.Tasks, (&istioExtensionReconciler{client: client}).Subscription().Reconcile) - } - - if isEnvoyGatewayInstalled { - effectiveRateLimitPoliciesWorkflow.Tasks = append(effectiveRateLimitPoliciesWorkflow.Tasks, (&envoyGatewayRateLimitClusterReconciler{client: client}).Subscription().Reconcile) - effectiveRateLimitPoliciesWorkflow.Tasks = append(effectiveRateLimitPoliciesWorkflow.Tasks, (&envoyGatewayExtensionReconciler{client: client}).Subscription().Reconcile) - } - - return &controller.Workflow{ - Precondition: (&rateLimitPolicyValidator{}).Subscription().Reconcile, - Tasks: []controller.ReconcileFunc{effectiveRateLimitPoliciesWorkflow.Run}, - Postcondition: (&rateLimitPolicyStatusUpdater{client: client}).Subscription().Reconcile, - } -} - func GetLimitadorFromTopology(topology *machinery.Topology) (*limitadorv1alpha1.Limitador, error) { kuadrant, err := GetKuadrantFromTopology(topology) if err != nil { @@ -137,7 +86,7 @@ func LimitNameToLimitadorIdentifier(rlpKey k8stypes.NamespacedName, uniqueLimitN func RateLimitObjectLabels() labels.Set { m := KuadrantManagedObjectLabels() - m[rateLimitClusterLabelKey] = "true" + m[rateLimitObjectLabelKey] = "true" return m } @@ -174,21 +123,25 @@ func rateLimitClusterPatch(host string, port int) map[string]any { } } -func rateLimitWasmActionBuilder(pathID string, effectivePolicy EffectiveRateLimitPolicy, state *sync.Map) wasm.ActionBuilderFunc { +func buildWasmActionsForRateLimit(effectivePolicy EffectiveRateLimitPolicy, state *sync.Map) []wasm.Action { policiesInPath := kuadrantv1.PoliciesInPath(effectivePolicy.Path, isRateLimitPolicyAcceptedAndNotDeletedFunc(state)) + _, _, _, httpRoute, _, _ := common.ObjectsInRequestPath(effectivePolicy.Path) limitsNamespace := LimitsNamespaceFromRoute(httpRoute.HTTPRoute) - return func(uniquePolicyRuleKey string, policyRule kuadrantv1.MergeableRule) (wasm.Action, error) { + + return lo.FilterMap(lo.Entries(effectivePolicy.Spec.Rules()), func(r lo.Entry[string, kuadrantv1.MergeableRule], _ int) (wasm.Action, bool) { + uniquePolicyRuleKey := r.Key + policyRule := r.Value source, found := lo.Find(policiesInPath, func(p machinery.Policy) bool { return p.GetLocator() == policyRule.GetSource() }) if !found { // should never happen - return wasm.Action{}, fmt.Errorf("could not find source policy %s in path %s", policyRule.GetSource(), pathID) + return wasm.Action{}, false } limitIdentifier := LimitNameToLimitadorIdentifier(k8stypes.NamespacedName{Name: source.GetName(), Namespace: source.GetNamespace()}, uniquePolicyRuleKey) - limit := policyRule.GetSpec().(kuadrantv1beta3.Limit) - return wasmActionFromLimit(limit, limitIdentifier, limitsNamespace), nil - } + limit := policyRule.GetSpec().(*kuadrantv1beta3.Limit) + return wasmActionFromLimit(limit, limitIdentifier, limitsNamespace), true + }) } // wasmActionFromLimit builds a wasm rate-limit action for a given limit. @@ -196,7 +149,7 @@ func rateLimitWasmActionBuilder(pathID string, effectivePolicy EffectiveRateLimi // // The only action of the rule is the ratelimit service, whose data includes the activation of the limit // and any counter qualifier of the limit. -func wasmActionFromLimit(limit kuadrantv1beta3.Limit, limitIdentifier, scope string) wasm.Action { +func wasmActionFromLimit(limit *kuadrantv1beta3.Limit, limitIdentifier, scope string) wasm.Action { action := wasm.Action{ ServiceName: wasm.RateLimitServiceName, Scope: scope, @@ -210,7 +163,7 @@ func wasmActionFromLimit(limit kuadrantv1beta3.Limit, limitIdentifier, scope str return action } -func wasmDataFromLimit(limitIdentifier string, limit kuadrantv1beta3.Limit) (data []wasm.DataType) { +func wasmDataFromLimit(limitIdentifier string, limit *kuadrantv1beta3.Limit) (data []wasm.DataType) { // static key representing the limit data = append(data, wasm.DataType{ diff --git a/controllers/ratelimit_workflow_test.go b/controllers/ratelimit_workflow_test.go index 759babe5f..00f1dc02b 100644 --- a/controllers/ratelimit_workflow_test.go +++ b/controllers/ratelimit_workflow_test.go @@ -70,14 +70,14 @@ func TestLimitNameToLimitadorIdentifier(t *testing.T) { func TestWasmActionFromLimit(t *testing.T) { testCases := []struct { name string - limit kuadrantv1beta3.Limit + limit *kuadrantv1beta3.Limit limitIdentifier string scope string expectedAction wasm.Action }{ { name: "limit without conditions nor counters", - limit: kuadrantv1beta3.Limit{}, + limit: &kuadrantv1beta3.Limit{}, limitIdentifier: "limit.myLimit__d681f6c3", scope: "my-ns/my-route", expectedAction: wasm.Action{ @@ -97,7 +97,7 @@ func TestWasmActionFromLimit(t *testing.T) { }, { name: "limit with counter qualifiers", - limit: kuadrantv1beta3.Limit{ + limit: &kuadrantv1beta3.Limit{ Counters: []kuadrantv1beta3.ContextSelector{"auth.identity.username"}, }, limitIdentifier: "limit.myLimit__d681f6c3", @@ -126,7 +126,7 @@ func TestWasmActionFromLimit(t *testing.T) { }, { name: "limit with counter qualifiers and when conditions", - limit: kuadrantv1beta3.Limit{ + limit: &kuadrantv1beta3.Limit{ Counters: []kuadrantv1beta3.ContextSelector{"auth.identity.username"}, When: []kuadrantv1beta3.WhenCondition{ { diff --git a/controllers/state_of_the_world.go b/controllers/state_of_the_world.go index 0f3174ef1..838212127 100644 --- a/controllers/state_of_the_world.go +++ b/controllers/state_of_the_world.go @@ -8,7 +8,8 @@ import ( certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" egv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/go-logr/logr" - authorinov1beta1 "github.com/kuadrant/authorino-operator/api/v1beta1" + authorinooperatorv1beta1 "github.com/kuadrant/authorino-operator/api/v1beta1" + authorinov1beta2 "github.com/kuadrant/authorino/api/v1beta2" limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" "github.com/kuadrant/policy-machinery/controller" "github.com/kuadrant/policy-machinery/machinery" @@ -16,7 +17,6 @@ import ( "github.com/samber/lo" istioclientgoextensionv1alpha1 "istio.io/client-go/pkg/apis/extensions/v1alpha1" istioclientnetworkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" - istioclientgosecurityv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -108,10 +108,16 @@ func NewPolicyMachineryController(manager ctrlruntime.Manager, client *dynamic.D metav1.NamespaceAll, )), controller.WithRunnable("authorino watcher", controller.Watch( - &authorinov1beta1.Authorino{}, + &authorinooperatorv1beta1.Authorino{}, kuadrantv1beta1.AuthorinosResource, metav1.NamespaceAll, )), + controller.WithRunnable("authconfig watcher", controller.Watch( + &authorinov1beta2.AuthConfig{}, + kuadrantv1beta1.AuthConfigsResource, + metav1.NamespaceAll, + controller.FilterResourcesByLabel[*authorinov1beta2.AuthConfig](fmt.Sprintf("%s=true", kuadrantManagedLabelKey)), + )), controller.WithPolicyKinds( kuadrantv1alpha1.DNSPolicyGroupKind, kuadrantv1alpha1.TLSPolicyGroupKind, @@ -123,11 +129,13 @@ func NewPolicyMachineryController(manager ctrlruntime.Manager, client *dynamic.D ConfigMapGroupKind, kuadrantv1beta1.LimitadorGroupKind, kuadrantv1beta1.AuthorinoGroupKind, + kuadrantv1beta1.AuthConfigGroupKind, ), controller.WithObjectLinks( kuadrantv1beta1.LinkKuadrantToGatewayClasses, kuadrantv1beta1.LinkKuadrantToLimitador, kuadrantv1beta1.LinkKuadrantToAuthorino, + kuadrantv1beta1.LinkHTTPRouteRuleToAuthConfig, ), } @@ -222,15 +230,9 @@ func (b *BootOptionsBuilder) getEnvoyGatewayOptions() []controller.ControllerOpt metav1.NamespaceAll, controller.FilterResourcesByLabel[*egv1alpha1.EnvoyExtensionPolicy](fmt.Sprintf("%s=true", kuadrantManagedLabelKey)), )), - controller.WithRunnable("envoysecuritypolicy watcher", controller.Watch( - &egv1alpha1.SecurityPolicy{}, - envoygateway.SecurityPoliciesResource, - metav1.NamespaceAll, - )), controller.WithObjectKinds( envoygateway.EnvoyPatchPolicyGroupKind, envoygateway.EnvoyExtensionPolicyGroupKind, - envoygateway.SecurityPolicyGroupKind, ), controller.WithObjectLinks( envoygateway.LinkGatewayToEnvoyPatchPolicy, @@ -263,15 +265,9 @@ func (b *BootOptionsBuilder) getIstioOptions() []controller.ControllerOption { metav1.NamespaceAll, controller.FilterResourcesByLabel[*istioclientgoextensionv1alpha1.WasmPlugin](fmt.Sprintf("%s=true", kuadrantManagedLabelKey)), )), - controller.WithRunnable("authorizationpolicy watcher", controller.Watch( - &istioclientgosecurityv1beta1.AuthorizationPolicy{}, - istio.AuthorizationPoliciesResource, - metav1.NamespaceAll, - )), controller.WithObjectKinds( istio.EnvoyFilterGroupKind, istio.WasmPluginGroupKind, - istio.AuthorizationPolicyGroupKind, ), controller.WithObjectLinks( istio.LinkGatewayToEnvoyFilter, @@ -323,8 +319,7 @@ func (b *BootOptionsBuilder) Reconciler() controller.ReconcileFunc { NewLimitadorReconciler(b.client).Subscription().Reconcile, NewDNSWorkflow().Run, NewTLSWorkflow(b.client, b.manager.GetScheme(), b.isCertManagerInstalled).Run, - NewAuthWorkflow().Run, - NewRateLimitWorkflow(b.client, b.isIstioInstalled, b.isEnvoyGatewayInstalled).Run, + NewDataPlanePoliciesWorkflow(b.client, b.isIstioInstalled, b.isEnvoyGatewayInstalled).Run, }, Postcondition: finalStepsWorkflow(b.client, b.isIstioInstalled, b.isGatewayAPIInstalled).Run, } diff --git a/pkg/common/common.go b/pkg/common/common.go index 68312ce2c..cd01b0568 100644 --- a/pkg/common/common.go +++ b/pkg/common/common.go @@ -27,10 +27,11 @@ import ( // TODO: move the const to a proper place, or get it from config const ( - KuadrantRateLimitClusterName = "kuadrant-rate-limiting-service" - AuthPolicyBackRefAnnotation = "kuadrant.io/authpolicy" - NamespaceSeparator = '/' + KuadrantRateLimitClusterName = "kuadrant-ratelimit-service" + KuadrantAuthClusterName = "kuadrant-auth-service" LimitadorName = "limitador" + + NamespaceSeparator = '/' ) // MergeMapStringString Merge desired into existing. diff --git a/pkg/common/policy_machinery_helpers.go b/pkg/common/policy_machinery_helpers.go index ccf096e2f..c0d3d1a92 100644 --- a/pkg/common/policy_machinery_helpers.go +++ b/pkg/common/policy_machinery_helpers.go @@ -3,11 +3,13 @@ package common import ( + "encoding/json" "fmt" "strings" "github.com/kuadrant/policy-machinery/machinery" "github.com/samber/lo" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" k8stypes "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" @@ -109,3 +111,14 @@ func NamespacedNameFromLocator(locator string) (k8stypes.NamespacedName, error) } return k8stypes.NamespacedName{Namespace: namespacedName[0], Name: namespacedName[1]}, nil } + +// Destruct converts an object to unstructured type via json +// Use it alternatively to github.com/policy-machinery/controller.Destruct for complex objects with nested fields +func Destruct[T any](obj T) (*unstructured.Unstructured, error) { + j, _ := json.Marshal(obj) + var u map[string]interface{} + if err := json.Unmarshal(j, &u); err != nil { + return nil, err + } + return &unstructured.Unstructured{Object: u}, nil +} diff --git a/pkg/envoygateway/utils.go b/pkg/envoygateway/utils.go index 34037c9cf..310205cc5 100644 --- a/pkg/envoygateway/utils.go +++ b/pkg/envoygateway/utils.go @@ -1,50 +1,52 @@ package envoygateway import ( - egv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + "encoding/json" + + envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/kuadrant/policy-machinery/controller" "github.com/kuadrant/policy-machinery/machinery" "github.com/samber/lo" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime/schema" 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/utils" ) var ( - EnvoyPatchPoliciesResource = egv1alpha1.SchemeBuilder.GroupVersion.WithResource("envoypatchpolicies") - EnvoyExtensionPoliciesResource = egv1alpha1.SchemeBuilder.GroupVersion.WithResource("envoyextensionpolicies") - SecurityPoliciesResource = egv1alpha1.SchemeBuilder.GroupVersion.WithResource("securitypolicies") + EnvoyPatchPoliciesResource = envoygatewayv1alpha1.SchemeBuilder.GroupVersion.WithResource("envoypatchpolicies") + EnvoyExtensionPoliciesResource = envoygatewayv1alpha1.SchemeBuilder.GroupVersion.WithResource("envoyextensionpolicies") - EnvoyPatchPolicyGroupKind = schema.GroupKind{Group: egv1alpha1.GroupName, Kind: egv1alpha1.KindEnvoyPatchPolicy} - EnvoyExtensionPolicyGroupKind = schema.GroupKind{Group: egv1alpha1.GroupName, Kind: egv1alpha1.KindEnvoyExtensionPolicy} - SecurityPolicyGroupKind = schema.GroupKind{Group: egv1alpha1.GroupName, Kind: egv1alpha1.KindSecurityPolicy} + EnvoyPatchPolicyGroupKind = schema.GroupKind{Group: envoygatewayv1alpha1.GroupName, Kind: envoygatewayv1alpha1.KindEnvoyPatchPolicy} + EnvoyExtensionPolicyGroupKind = schema.GroupKind{Group: envoygatewayv1alpha1.GroupName, Kind: envoygatewayv1alpha1.KindEnvoyExtensionPolicy} ) func IsEnvoyPatchPolicyInstalled(restMapper meta.RESTMapper) (bool, error) { return utils.IsCRDInstalled( restMapper, - egv1alpha1.GroupName, - egv1alpha1.KindEnvoyPatchPolicy, - egv1alpha1.GroupVersion.Version) + envoygatewayv1alpha1.GroupName, + envoygatewayv1alpha1.KindEnvoyPatchPolicy, + envoygatewayv1alpha1.GroupVersion.Version) } func IsEnvoyExtensionPolicyInstalled(restMapper meta.RESTMapper) (bool, error) { return utils.IsCRDInstalled( restMapper, - egv1alpha1.GroupName, - egv1alpha1.KindEnvoyExtensionPolicy, - egv1alpha1.GroupVersion.Version) + envoygatewayv1alpha1.GroupName, + envoygatewayv1alpha1.KindEnvoyExtensionPolicy, + envoygatewayv1alpha1.GroupVersion.Version) } func IsEnvoyGatewaySecurityPolicyInstalled(restMapper meta.RESTMapper) (bool, error) { return utils.IsCRDInstalled( restMapper, - egv1alpha1.GroupName, - egv1alpha1.KindSecurityPolicy, - egv1alpha1.GroupVersion.Version) + envoygatewayv1alpha1.GroupName, + envoygatewayv1alpha1.KindSecurityPolicy, + envoygatewayv1alpha1.GroupVersion.Version) } func IsEnvoyGatewayInstalled(restMapper meta.RESTMapper) (bool, error) { @@ -85,7 +87,7 @@ func LinkGatewayToEnvoyPatchPolicy(objs controller.Store) machinery.LinkFunc { From: machinery.GatewayGroupKind, To: EnvoyPatchPolicyGroupKind, Func: func(child machinery.Object) []machinery.Object { - envoyPatchPolicy := child.(*controller.RuntimeObject).Object.(*egv1alpha1.EnvoyPatchPolicy) + envoyPatchPolicy := child.(*controller.RuntimeObject).Object.(*envoygatewayv1alpha1.EnvoyPatchPolicy) namespace := envoyPatchPolicy.GetNamespace() targetRef := envoyPatchPolicy.Spec.TargetRef group := string(targetRef.Group) @@ -116,7 +118,7 @@ func LinkGatewayToEnvoyExtensionPolicy(objs controller.Store) machinery.LinkFunc From: machinery.GatewayGroupKind, To: EnvoyExtensionPolicyGroupKind, Func: func(child machinery.Object) []machinery.Object { - envoyExtensionPolicy := child.(*controller.RuntimeObject).Object.(*egv1alpha1.EnvoyExtensionPolicy) + envoyExtensionPolicy := child.(*controller.RuntimeObject).Object.(*envoygatewayv1alpha1.EnvoyExtensionPolicy) return lo.Filter(gateways, func(gateway machinery.Object, _ int) bool { if gateway.GetNamespace() != envoyExtensionPolicy.GetNamespace() { return false @@ -142,3 +144,41 @@ func LinkGatewayToEnvoyExtensionPolicy(objs controller.Store) machinery.LinkFunc }, } } + +// BuildEnvoyPatchPolicyClusterPatch returns an envoy config patch that adds a cluster to the gateway. +func BuildEnvoyPatchPolicyClusterPatch(host string, port int, clusterPatchBuilder func(string, int) map[string]any) ([]envoygatewayv1alpha1.EnvoyJSONPatchConfig, error) { + patchRaw, _ := json.Marshal(clusterPatchBuilder(host, port)) + patch := &apiextensionsv1.JSON{} + if err := patch.UnmarshalJSON(patchRaw); err != nil { + return nil, err + } + + return []envoygatewayv1alpha1.EnvoyJSONPatchConfig{ + { + Type: envoygatewayv1alpha1.ClusterEnvoyResourceType, + Name: common.KuadrantRateLimitClusterName, + Operation: envoygatewayv1alpha1.JSONPatchOperation{ + Op: envoygatewayv1alpha1.JSONPatchOperationType("add"), + Path: "", + Value: patch, + }, + }, + }, nil +} + +func EqualEnvoyPatchPolicies(a, b *envoygatewayv1alpha1.EnvoyPatchPolicy) bool { + if a.Spec.Priority != b.Spec.Priority || a.Spec.TargetRef != b.Spec.TargetRef { + return false + } + + aJSONPatches := a.Spec.JSONPatches + bJSONPatches := b.Spec.JSONPatches + if len(aJSONPatches) != len(bJSONPatches) { + return false + } + return lo.EveryBy(aJSONPatches, func(aJSONPatch envoygatewayv1alpha1.EnvoyJSONPatchConfig) bool { + return lo.SomeBy(bJSONPatches, func(bJSONPatch envoygatewayv1alpha1.EnvoyJSONPatchConfig) bool { + return aJSONPatch.Type == bJSONPatch.Type && aJSONPatch.Name == bJSONPatch.Name && aJSONPatch.Operation == bJSONPatch.Operation + }) + }) +} diff --git a/pkg/istio/mesh_config.go b/pkg/istio/mesh_config.go deleted file mode 100644 index 1ac217dcc..000000000 --- a/pkg/istio/mesh_config.go +++ /dev/null @@ -1,382 +0,0 @@ -package istio - -import ( - "encoding/json" - "fmt" - - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/types/known/structpb" - istiomeshv1alpha1 "istio.io/api/mesh/v1alpha1" - istioapiv1alpha1 "istio.io/api/operator/v1alpha1" - iopv1alpha1 "istio.io/istio/operator/pkg/apis/istio/v1alpha1" - "istio.io/istio/pkg/util/protomarshal" - corev1 "k8s.io/api/core/v1" - istiov1alpha1 "maistra.io/istio-operator/api/v1alpha1" - "maistra.io/istio-operator/pkg/helm" - "sigs.k8s.io/controller-runtime/pkg/client" - - maistrav2 "github.com/kuadrant/kuadrant-operator/api/external/maistra/v2" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" -) - -const ( - ExtAuthorizerName = "kuadrant-authorization" -) - -type authorizer interface { - GetExtensionProvider() *istiomeshv1alpha1.MeshConfig_ExtensionProvider -} - -type ConfigWrapper interface { - GetConfigObject() client.Object - GetMeshConfig() (*istiomeshv1alpha1.MeshConfig, error) - SetMeshConfig(*istiomeshv1alpha1.MeshConfig) error -} - -type KuadrantAuthorizer struct { - extensionProvider *istiomeshv1alpha1.MeshConfig_ExtensionProvider -} - -// NewKuadrantAuthorizer Creates a new KuadrantAuthorizer -func NewKuadrantAuthorizer(namespace string) *KuadrantAuthorizer { - return &KuadrantAuthorizer{ - extensionProvider: createKuadrantAuthorizer(namespace), - } -} - -// GetExtensionProvider Returns the Istio MeshConfig ExtensionProvider for Kuadrant -func (k *KuadrantAuthorizer) GetExtensionProvider() *istiomeshv1alpha1.MeshConfig_ExtensionProvider { - return k.extensionProvider -} - -// createKuadrantAuthorizer Creates the Istio MeshConfig ExtensionProvider for Kuadrant -func createKuadrantAuthorizer(namespace string) *istiomeshv1alpha1.MeshConfig_ExtensionProvider { - envoyExtAuthGRPC := &istiomeshv1alpha1.MeshConfig_ExtensionProvider_EnvoyExtAuthzGrpc{ - EnvoyExtAuthzGrpc: &istiomeshv1alpha1.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationGrpcProvider{ - Port: 50051, - Service: fmt.Sprintf("%s.%s.svc.cluster.local", kuadrant.AuthorinoServiceName, namespace), - }, - } - return &istiomeshv1alpha1.MeshConfig_ExtensionProvider{ - Name: ExtAuthorizerName, - Provider: envoyExtAuthGRPC, - } -} - -// HasKuadrantAuthorizer returns true if the IstioOperator has the Kuadrant ExtensionProvider -func HasKuadrantAuthorizer(configWrapper ConfigWrapper, authorizer KuadrantAuthorizer) (bool, error) { - config, err := configWrapper.GetMeshConfig() - if err != nil { - return false, err - } - return hasExtensionProvider(authorizer.GetExtensionProvider(), extensionProvidersFromMeshConfig(config)), nil -} - -// RegisterKuadrantAuthorizer adds the Kuadrant ExtensionProvider to the IstioOperator -func RegisterKuadrantAuthorizer(configWrapper ConfigWrapper, authorizer authorizer) error { - config, err := configWrapper.GetMeshConfig() - if err != nil { - return err - } - if !hasExtensionProvider(authorizer.GetExtensionProvider(), extensionProvidersFromMeshConfig(config)) { - config.ExtensionProviders = append(config.ExtensionProviders, authorizer.GetExtensionProvider()) - if err = configWrapper.SetMeshConfig(config); err != nil { - return err - } - } - return nil -} - -// UnregisterKuadrantAuthorizer removes the Kuadrant ExtensionProvider from the IstioOperator -func UnregisterKuadrantAuthorizer(configWrapper ConfigWrapper, authorizer authorizer) error { - config, err := configWrapper.GetMeshConfig() - if err != nil { - return err - } - if hasExtensionProvider(authorizer.GetExtensionProvider(), extensionProvidersFromMeshConfig(config)) { - config.ExtensionProviders = removeExtensionProvider(authorizer.GetExtensionProvider(), extensionProvidersFromMeshConfig(config)) - if err = configWrapper.SetMeshConfig(config); err != nil { - return err - } - } - return nil -} - -func extensionProvidersFromMeshConfig(config *istiomeshv1alpha1.MeshConfig) (extensionProviders []*istiomeshv1alpha1.MeshConfig_ExtensionProvider) { - extensionProviders = config.ExtensionProviders - if len(extensionProviders) == 0 { - extensionProviders = make([]*istiomeshv1alpha1.MeshConfig_ExtensionProvider, 0) - } - return -} - -// hasExtensionProvider returns true if the MeshConfig has an ExtensionProvider with the given name -func hasExtensionProvider(provider *istiomeshv1alpha1.MeshConfig_ExtensionProvider, extensionProviders []*istiomeshv1alpha1.MeshConfig_ExtensionProvider) bool { - for _, extensionProvider := range extensionProviders { - if extensionProvider.Name == provider.Name { - return true - } - } - return false -} - -func removeExtensionProvider(provider *istiomeshv1alpha1.MeshConfig_ExtensionProvider, providers []*istiomeshv1alpha1.MeshConfig_ExtensionProvider) []*istiomeshv1alpha1.MeshConfig_ExtensionProvider { - for i, extensionProvider := range providers { - if extensionProvider.Name == provider.Name { - return append(providers[:i], providers[i+1:]...) - } - } - return providers -} - -// OperatorWrapper wraps the IstioOperator CRD -type OperatorWrapper struct { - config *iopv1alpha1.IstioOperator -} - -// NewOperatorWrapper creates a new IstioOperatorWrapper -func NewOperatorWrapper(config *iopv1alpha1.IstioOperator) *OperatorWrapper { - return &OperatorWrapper{config: config} -} - -// GetConfigObject returns the IstioOperator CRD -func (w *OperatorWrapper) GetConfigObject() client.Object { - return w.config -} - -// GetMeshConfig returns the IstioOperator MeshConfig -func (w *OperatorWrapper) GetMeshConfig() (*istiomeshv1alpha1.MeshConfig, error) { - if w.config.Spec == nil { - w.config.Spec = &istioapiv1alpha1.IstioOperatorSpec{} - } - return meshConfigFromStruct(w.config.Spec.MeshConfig) -} - -// SetMeshConfig sets the IstioOperator MeshConfig -func (w *OperatorWrapper) SetMeshConfig(config *istiomeshv1alpha1.MeshConfig) error { - meshConfigStruct, err := meshConfigToStruct(config) - if err != nil { - return err - } - w.config.Spec.MeshConfig = meshConfigStruct - return nil -} - -// ConfigMapWrapper wraps the ConfigMap holding the Istio MeshConfig -type ConfigMapWrapper struct { - config *corev1.ConfigMap -} - -// NewConfigMapWrapper creates a new ConfigMapWrapper -func NewConfigMapWrapper(config *corev1.ConfigMap) *ConfigMapWrapper { - return &ConfigMapWrapper{config: config} -} - -// GetConfigObject returns the ConfigMap -func (w *ConfigMapWrapper) GetConfigObject() client.Object { - return w.config -} - -// GetMeshConfig returns the MeshConfig from the ConfigMap -func (w *ConfigMapWrapper) GetMeshConfig() (*istiomeshv1alpha1.MeshConfig, error) { - meshConfigString, ok := w.config.Data["mesh"] - if !ok { - return nil, fmt.Errorf("mesh config not found in ConfigMap") - } - return meshConfigFromString(meshConfigString) -} - -// SetMeshConfig sets the MeshConfig in the ConfigMap -func (w *ConfigMapWrapper) SetMeshConfig(config *istiomeshv1alpha1.MeshConfig) error { - meshConfigString, err := meshConfigToString(config) - if err != nil { - return err - } - w.config.Data["mesh"] = meshConfigString - return nil -} - -// OSSMControlPlaneWrapper wraps the OSSM ServiceMeshControlPlane -type OSSMControlPlaneWrapper struct { - config *maistrav2.ServiceMeshControlPlane -} - -// NewOSSMControlPlaneWrapper creates a new OSSMControlPlaneWrapper -func NewOSSMControlPlaneWrapper(config *maistrav2.ServiceMeshControlPlane) *OSSMControlPlaneWrapper { - return &OSSMControlPlaneWrapper{config: config} -} - -// GetConfigObject returns the OSSM ServiceMeshControlPlane -func (w *OSSMControlPlaneWrapper) GetConfigObject() client.Object { - return w.config -} - -// SailWrapper wraps the IstioCR -type SailWrapper struct { - config *istiov1alpha1.Istio -} - -// NewSailWrapper creates a new SailWrapper -func NewSailWrapper(config *istiov1alpha1.Istio) *SailWrapper { - return &SailWrapper{config: config} -} - -// GetConfigObject returns the IstioCR -func (w *SailWrapper) GetConfigObject() client.Object { - return w.config -} - -// GetMeshConfig returns the Istio MeshConfig -func (w *SailWrapper) GetMeshConfig() (*istiomeshv1alpha1.MeshConfig, error) { - values := w.config.Spec.GetValues() - config, ok := values["meshConfig"].(map[string]any) - if !ok { - return &istiomeshv1alpha1.MeshConfig{}, nil - } - meshConfigStruct, err := structpb.NewStruct(config) - if err != nil { - return nil, err - } - meshConfig, err := meshConfigFromStruct(meshConfigStruct) - if err != nil { - return nil, err - } - return meshConfig, nil -} - -// SetMeshConfig sets the Istio MeshConfig -func (w *SailWrapper) SetMeshConfig(config *istiomeshv1alpha1.MeshConfig) error { - meshConfigStruct, err := meshConfigToStruct(config) - if err != nil { - return err - } - values := w.config.Spec.GetValues() - if values == nil { - values = helm.HelmValues{} - } - if err := values.Set("meshConfig", meshConfigStruct.AsMap()); err != nil { - return err - } - return w.config.Spec.SetValues(values) -} - -// GetMeshConfig returns the MeshConfig from the OSSM ServiceMeshControlPlane -func (w *OSSMControlPlaneWrapper) GetMeshConfig() (*istiomeshv1alpha1.MeshConfig, error) { - config := w.config.Spec.MeshConfig - if config == nil { - return &istiomeshv1alpha1.MeshConfig{}, nil - } - meshConfigStruct, err := ossmMeshConfigToStruct(config) - if err != nil { - return nil, err - } - meshConfig, err := meshConfigFromStruct(meshConfigStruct) - if err != nil { - return nil, err - } - return meshConfig, nil -} - -// SetMeshConfig sets the MeshConfig in the OSSM ServiceMeshControlPlane -func (w *OSSMControlPlaneWrapper) SetMeshConfig(config *istiomeshv1alpha1.MeshConfig) error { - meshConfigStruct, err := meshConfigToStruct(config) - if err != nil { - return err - } - w.config.Spec.MeshConfig, err = ossmMeshConfigFromStruct(meshConfigStruct) - return err -} - -// meshConfigFromStruct Builds the Istio/OSSM MeshConfig from a compatible structure: -// -// meshConfig: -// extensionProviders: -// - envoyExtAuthzGrpc: -// port: -// service: -// name: kuadrant-authorization -func meshConfigFromStruct(structure *structpb.Struct) (*istiomeshv1alpha1.MeshConfig, error) { - if structure == nil { - return &istiomeshv1alpha1.MeshConfig{}, nil - } - - meshConfigJSON, err := structure.MarshalJSON() - if err != nil { - return nil, err - } - meshConfig := &istiomeshv1alpha1.MeshConfig{} - // istiomeshv1alpha1.MeshConfig doesn't implement JSON/Yaml marshalling, only protobuf - if err = protojson.Unmarshal(meshConfigJSON, meshConfig); err != nil { - return nil, err - } - - return meshConfig, nil -} - -// meshConfigToStruct Marshals the Istio MeshConfig into a struct -func meshConfigToStruct(config *istiomeshv1alpha1.MeshConfig) (*structpb.Struct, error) { - configJSON, err := protojson.Marshal(config) - if err != nil { - return nil, err - } - configStruct := &structpb.Struct{} - - if err = configStruct.UnmarshalJSON(configJSON); err != nil { - return nil, err - } - return configStruct, nil -} - -// meshConfigFromString returns the Istio MeshConfig from a ConfigMap -func meshConfigFromString(config string) (*istiomeshv1alpha1.MeshConfig, error) { - meshConfig := &istiomeshv1alpha1.MeshConfig{} - err := protomarshal.ApplyYAML(config, meshConfig) - if err != nil { - return nil, err - } - return meshConfig, nil -} - -// meshConfigToString returns the Istio MeshConfig as a string -func meshConfigToString(config *istiomeshv1alpha1.MeshConfig) (string, error) { - configString, err := protomarshal.ToYAML(config) - if err != nil { - return "", err - } - return configString, nil -} - -// ossmMeshConfigFromStruct returns a maistrav2.MeshConfig from struct -func ossmMeshConfigFromStruct(structure *structpb.Struct) (*maistrav2.MeshConfig, error) { - if structure == nil { - return &maistrav2.MeshConfig{}, nil - } - - meshConfigJSON, err := structure.MarshalJSON() - if err != nil { - return nil, err - } - - meshConfig := &maistrav2.MeshConfig{} - if err = json.Unmarshal(meshConfigJSON, meshConfig); err != nil { - return nil, err - } - - return meshConfig, nil -} - -func ossmMeshConfigToStruct(config *maistrav2.MeshConfig) (*structpb.Struct, error) { - configJSON, err := json.Marshal(config) - if err != nil { - return nil, err - } - return jsonByteToStruct(configJSON) -} - -func jsonByteToStruct(configJSON []byte) (*structpb.Struct, error) { - configStruct := &structpb.Struct{} - if err := configStruct.UnmarshalJSON(configJSON); err != nil { - return nil, err - } - return configStruct, nil -} diff --git a/pkg/istio/mesh_config_test.go b/pkg/istio/mesh_config_test.go deleted file mode 100644 index ac7cee634..000000000 --- a/pkg/istio/mesh_config_test.go +++ /dev/null @@ -1,321 +0,0 @@ -//go:build unit - -package istio - -import ( - "fmt" - "testing" - - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" - "google.golang.org/protobuf/types/known/structpb" - "gotest.tools/assert" - istiomeshv1alpha1 "istio.io/api/mesh/v1alpha1" - istioapiv1alpha1 "istio.io/api/operator/v1alpha1" - iopv1alpha1 "istio.io/istio/operator/pkg/apis/istio/v1alpha1" - corev1 "k8s.io/api/core/v1" - istiov1alpha1 "maistra.io/istio-operator/api/v1alpha1" - "maistra.io/istio-operator/pkg/helm" - "sigs.k8s.io/controller-runtime/pkg/client" - - maistrav2 "github.com/kuadrant/kuadrant-operator/api/external/maistra/v2" -) - -type stubbedConfigWrapper struct { - istioMeshConfig *istiomeshv1alpha1.MeshConfig -} - -func (c *stubbedConfigWrapper) SetMeshConfig(config *istiomeshv1alpha1.MeshConfig) error { - c.istioMeshConfig = config - return nil -} - -func (c *stubbedConfigWrapper) GetMeshConfig() (*istiomeshv1alpha1.MeshConfig, error) { - return c.istioMeshConfig, nil -} - -func (c *stubbedConfigWrapper) GetConfigObject() client.Object { - return nil -} - -func TestKuadrantAuthorizer_GetExtensionProvider(t *testing.T) { - authorizer := NewKuadrantAuthorizer("default") - provider := authorizer.GetExtensionProvider() - - assert.Equal(t, provider.Name, ExtAuthorizerName) - assert.Equal(t, provider.GetEnvoyExtAuthzGrpc().Service, fmt.Sprintf("%s.default.svc.cluster.local", kuadrant.AuthorinoServiceName)) -} - -func TestHasKuadrantAuthorizer(t *testing.T) { - authorizer := NewKuadrantAuthorizer("default") - configWrapper := &stubbedConfigWrapper{getStubbedMeshConfig()} - - hasAuthorizer, err := HasKuadrantAuthorizer(configWrapper, *authorizer) - - assert.NilError(t, err) - assert.Equal(t, hasAuthorizer, false) - - configWrapper.istioMeshConfig.ExtensionProviders = append(configWrapper.istioMeshConfig.ExtensionProviders, authorizer.GetExtensionProvider()) - hasAuthorizer, err = HasKuadrantAuthorizer(configWrapper, *authorizer) - assert.NilError(t, err) - assert.Equal(t, hasAuthorizer, true) -} - -func TestRegisterKuadrantAuthorizer(t *testing.T) { - authorizer := NewKuadrantAuthorizer("default") - configWrapper := &stubbedConfigWrapper{getStubbedMeshConfig()} - - err := RegisterKuadrantAuthorizer(configWrapper, authorizer) - assert.NilError(t, err) - - meshConfig, _ := configWrapper.GetMeshConfig() - assert.Equal(t, meshConfig.ExtensionProviders[1].Name, "kuadrant-authorization") -} - -func TestUnregisterKuadrantAuthorizer(t *testing.T) { - authorizer := NewKuadrantAuthorizer("default") - configWrapper := &stubbedConfigWrapper{getStubbedMeshConfig()} - - err := RegisterKuadrantAuthorizer(configWrapper, authorizer) - assert.NilError(t, err) - assert.Equal(t, len(configWrapper.istioMeshConfig.ExtensionProviders), 2) - - err = UnregisterKuadrantAuthorizer(configWrapper, authorizer) - assert.NilError(t, err) - assert.Equal(t, len(configWrapper.istioMeshConfig.ExtensionProviders), 1) - - meshConfig, _ := configWrapper.GetMeshConfig() - assert.Equal(t, meshConfig.GetExtensionProviders()[0].Name, "custom-authorizer") -} - -func getStubbedMeshConfig() *istiomeshv1alpha1.MeshConfig { - providers := make([]*istiomeshv1alpha1.MeshConfig_ExtensionProvider, 0) - provider := &istiomeshv1alpha1.MeshConfig_ExtensionProvider{ - Name: "custom-authorizer", - Provider: &istiomeshv1alpha1.MeshConfig_ExtensionProvider_EnvoyExtAuthzGrpc{ - EnvoyExtAuthzGrpc: &istiomeshv1alpha1.MeshConfig_ExtensionProvider_EnvoyExternalAuthorizationGrpcProvider{ - Port: 50051, - Service: "custom-authorizer.default.svc.cluster.local", - }, - }, - } - providers = append(providers, provider) - return &istiomeshv1alpha1.MeshConfig{ - ExtensionProviders: providers, - } -} - -func getStubbedMeshConfigStruct() *structpb.Struct { - return &structpb.Struct{ - Fields: map[string]*structpb.Value{ - "extensionProviders": { - Kind: &structpb.Value_ListValue{ - ListValue: &structpb.ListValue{ - Values: []*structpb.Value{ - { - Kind: &structpb.Value_StructValue{ - StructValue: &structpb.Struct{ - Fields: map[string]*structpb.Value{ - "name": { - Kind: &structpb.Value_StringValue{ - StringValue: "custom-authorizer", - }, - }, - "envoyExtAuthzGrpc": { - Kind: &structpb.Value_StructValue{ - StructValue: &structpb.Struct{ - Fields: map[string]*structpb.Value{ - "port": { - Kind: &structpb.Value_NumberValue{ - NumberValue: 50051, - }, - }, - "service": { - Kind: &structpb.Value_StringValue{ - StringValue: "custom-authorizer.default.svc.cluster.local", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - } -} - -func TestOperatorWrapper_GetConfigObject(t *testing.T) { - config := &iopv1alpha1.IstioOperator{} - wrapper := NewOperatorWrapper(config) - - assert.Equal(t, wrapper.GetConfigObject(), config) -} - -func TestOperatorWrapper_GetMeshConfig(t *testing.T) { - structConfig := getStubbedMeshConfigStruct() - - config := &iopv1alpha1.IstioOperator{ - Spec: &istioapiv1alpha1.IstioOperatorSpec{ - MeshConfig: structConfig, - }, - } - wrapper := NewOperatorWrapper(config) - - meshConfig, err := wrapper.GetMeshConfig() - assert.NilError(t, err) - assert.Equal(t, meshConfig.ExtensionProviders[0].Name, "custom-authorizer") - assert.Equal(t, meshConfig.ExtensionProviders[0].GetEnvoyExtAuthzGrpc().GetPort(), uint32(50051)) -} - -func TestOperatorWrapper_SetMeshConfig(t *testing.T) { - config := &iopv1alpha1.IstioOperator{ - Spec: &istioapiv1alpha1.IstioOperatorSpec{}, - } - wrapper := NewOperatorWrapper(config) - - stubbedMeshConfig := getStubbedMeshConfig() - err := wrapper.SetMeshConfig(stubbedMeshConfig) - assert.NilError(t, err) - - meshConfig, _ := wrapper.GetMeshConfig() - - assert.Equal(t, meshConfig.ExtensionProviders[0].Name, stubbedMeshConfig.ExtensionProviders[0].Name) - assert.Equal(t, meshConfig.ExtensionProviders[0].GetEnvoyExtAuthzGrpc().GetPort(), uint32(50051)) -} - -func TestConfigMapWrapper_GetConfigObject(t *testing.T) { - configMap := &corev1.ConfigMap{} - wrapper := NewConfigMapWrapper(configMap) - - assert.Equal(t, wrapper.GetConfigObject(), configMap) -} - -func TestConfigMapWrapper_GetMeshConfig(t *testing.T) { - configMap := &corev1.ConfigMap{ - Data: map[string]string{ - "mesh": ` -extensionProviders: -- name: "custom-authorizer" - envoyExtAuthzGrpc: - service: "custom-authorizer.default.svc.cluster.local" - port: "50051" -`, - }, - } - wrapper := NewConfigMapWrapper(configMap) - - meshConfig, _ := wrapper.GetMeshConfig() - assert.Equal(t, meshConfig.ExtensionProviders[0].Name, "custom-authorizer") - assert.Equal(t, meshConfig.ExtensionProviders[0].GetEnvoyExtAuthzGrpc().GetPort(), uint32(50051)) -} - -func TestConfigMapWrapper_SetMeshConfig(t *testing.T) { - configMap := &corev1.ConfigMap{ - Data: map[string]string{ - "mesh": "", - }, - } - wrapper := NewConfigMapWrapper(configMap) - - stubbedMeshConfig := getStubbedMeshConfig() - err := wrapper.SetMeshConfig(stubbedMeshConfig) - assert.NilError(t, err) - - meshConfig, _ := wrapper.GetMeshConfig() - - assert.Equal(t, meshConfig.ExtensionProviders[0].Name, "custom-authorizer") - assert.Equal(t, meshConfig.ExtensionProviders[0].GetEnvoyExtAuthzGrpc().GetPort(), uint32(50051)) -} - -func TestOSSMControlPlaneWrapper_GetConfigObject(t *testing.T) { - ossmControlPlane := &maistrav2.ServiceMeshControlPlane{} - wrapper := NewOSSMControlPlaneWrapper(ossmControlPlane) - assert.Equal(t, wrapper.GetConfigObject(), ossmControlPlane) -} - -func TestOSSMControlPlaneWrapper_GetMeshConfig(t *testing.T) { - ossmControlPlane := &maistrav2.ServiceMeshControlPlane{} - ossmMeshConfig, err := ossmMeshConfigFromStruct(getStubbedMeshConfigStruct()) - ossmControlPlane.Spec.MeshConfig = ossmMeshConfig - assert.NilError(t, err) - - wrapper := NewOSSMControlPlaneWrapper(ossmControlPlane) - meshConfig, _ := wrapper.GetMeshConfig() - - assert.Equal(t, meshConfig.ExtensionProviders[0].Name, "custom-authorizer") - assert.Equal(t, meshConfig.ExtensionProviders[0].GetEnvoyExtAuthzGrpc().GetPort(), uint32(50051)) - - // additional test branches for ossmMeshConfigFromStruct - ossmMeshConfig, err = ossmMeshConfigFromStruct(nil) - assert.NilError(t, err) - assert.DeepEqual(t, ossmMeshConfig, &maistrav2.MeshConfig{}) - - invalidStruct := &structpb.Struct{ - Fields: map[string]*structpb.Value{ - "invalid": {}, - }, - } - - ossmMeshConfig, err = ossmMeshConfigFromStruct(invalidStruct) - assert.Check(t, err != nil) - assert.Check(t, ossmMeshConfig == nil) -} - -func TestOSSMControlPlaneWrapper_SetMeshConfig(t *testing.T) { - ossmControlPlane := &maistrav2.ServiceMeshControlPlane{} - wrapper := NewOSSMControlPlaneWrapper(ossmControlPlane) - - stubbedMeshConfig := getStubbedMeshConfig() - err := wrapper.SetMeshConfig(stubbedMeshConfig) - assert.NilError(t, err) - - meshConfig, _ := wrapper.GetMeshConfig() - - assert.Equal(t, meshConfig.ExtensionProviders[0].Name, "custom-authorizer") - assert.Equal(t, meshConfig.ExtensionProviders[0].GetEnvoyExtAuthzGrpc().GetPort(), uint32(50051)) -} - -func TestSailWrapper_GetConfigObject(t *testing.T) { - ist := &istiov1alpha1.Istio{} - wrapper := NewSailWrapper(ist) - - assert.Equal(t, wrapper.GetConfigObject(), ist) -} - -func TestSailWrapper_GetMeshConfig(t *testing.T) { - structConfig := getStubbedMeshConfigStruct() - values := helm.HelmValues{} - if err := values.Set("meshConfig", structConfig.AsMap()); err != nil { - assert.NilError(t, err) - } - config := &istiov1alpha1.Istio{} - if err := config.Spec.SetValues(values); err != nil { - assert.NilError(t, err) - } - wrapper := NewSailWrapper(config) - - meshConfig, err := wrapper.GetMeshConfig() - assert.NilError(t, err) - assert.Equal(t, meshConfig.ExtensionProviders[0].Name, "custom-authorizer") - assert.Equal(t, meshConfig.ExtensionProviders[0].GetEnvoyExtAuthzGrpc().GetPort(), uint32(50051)) -} - -func TestSailWrapper_SetMeshConfig(t *testing.T) { - config := &istiov1alpha1.Istio{} - wrapper := NewSailWrapper(config) - - stubbedMeshConfig := getStubbedMeshConfig() - err := wrapper.SetMeshConfig(stubbedMeshConfig) - assert.NilError(t, err) - - meshConfig, _ := wrapper.GetMeshConfig() - - assert.Equal(t, meshConfig.ExtensionProviders[0].Name, stubbedMeshConfig.ExtensionProviders[0].Name) - assert.Equal(t, meshConfig.ExtensionProviders[0].GetEnvoyExtAuthzGrpc().GetPort(), uint32(50051)) -} diff --git a/pkg/istio/utils.go b/pkg/istio/utils.go index 6a3a01762..c31ee30ed 100644 --- a/pkg/istio/utils.go +++ b/pkg/istio/utils.go @@ -1,10 +1,13 @@ package istio import ( + "encoding/json" + "github.com/kuadrant/policy-machinery/controller" "github.com/kuadrant/policy-machinery/machinery" "github.com/samber/lo" istioapimetav1alpha1 "istio.io/api/meta/v1alpha1" + istioapinetworkingv1alpha3 "istio.io/api/networking/v1alpha3" istioapiv1beta1 "istio.io/api/type/v1beta1" istioclientgoextensionv1alpha1 "istio.io/client-go/pkg/apis/extensions/v1alpha1" istioclientgonetworkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" @@ -18,13 +21,11 @@ import ( ) var ( - EnvoyFiltersResource = istioclientgonetworkingv1alpha3.SchemeGroupVersion.WithResource("envoyfilters") - WasmPluginsResource = istioclientgoextensionv1alpha1.SchemeGroupVersion.WithResource("wasmplugins") - AuthorizationPoliciesResource = istioclientgosecurityv1beta1.SchemeGroupVersion.WithResource("authorizationpolicies") + EnvoyFiltersResource = istioclientgonetworkingv1alpha3.SchemeGroupVersion.WithResource("envoyfilters") + WasmPluginsResource = istioclientgoextensionv1alpha1.SchemeGroupVersion.WithResource("wasmplugins") - EnvoyFilterGroupKind = schema.GroupKind{Group: istioclientgonetworkingv1alpha3.GroupName, Kind: "EnvoyFilter"} - WasmPluginGroupKind = schema.GroupKind{Group: istioclientgoextensionv1alpha1.GroupName, Kind: "WasmPlugin"} - AuthorizationPolicyGroupKind = schema.GroupKind{Group: istioclientgosecurityv1beta1.GroupName, Kind: "AuthorizationPolicy"} + EnvoyFilterGroupKind = schema.GroupKind{Group: istioclientgonetworkingv1alpha3.GroupName, Kind: "EnvoyFilter"} + WasmPluginGroupKind = schema.GroupKind{Group: istioclientgoextensionv1alpha1.GroupName, Kind: "WasmPlugin"} ) func PolicyTargetRefFromGateway(gateway *gatewayapiv1.Gateway) *istioapiv1beta1.PolicyTargetReference { @@ -43,6 +44,78 @@ func EqualTargetRefs(a, b []*istioapiv1beta1.PolicyTargetReference) bool { }) } +// BuildEnvoyFilterClusterPatch returns an envoy config patch that adds a cluster to the gateway. +func BuildEnvoyFilterClusterPatch(host string, port int, clusterPatchBuilder func(string, int) map[string]any) ([]*istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch, error) { + patchRaw, _ := json.Marshal(map[string]any{"operation": "ADD", "value": clusterPatchBuilder(host, port)}) + patch := &istioapinetworkingv1alpha3.EnvoyFilter_Patch{} + if err := patch.UnmarshalJSON(patchRaw); err != nil { + return nil, err + } + + return []*istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch{ + { + ApplyTo: istioapinetworkingv1alpha3.EnvoyFilter_CLUSTER, + Match: &istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectMatch{ + ObjectTypes: &istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectMatch_Cluster{ + Cluster: &istioapinetworkingv1alpha3.EnvoyFilter_ClusterMatch{ + Service: host, + }, + }, + }, + Patch: patch, + }, + }, nil +} + +func EqualEnvoyFilters(a, b *istioclientgonetworkingv1alpha3.EnvoyFilter) bool { + if a.Spec.Priority != b.Spec.Priority || !EqualTargetRefs(a.Spec.TargetRefs, b.Spec.TargetRefs) { + return false + } + + aConfigPatches := a.Spec.ConfigPatches + bConfigPatches := b.Spec.ConfigPatches + if len(aConfigPatches) != len(bConfigPatches) { + return false + } + return lo.EveryBy(aConfigPatches, func(aConfigPatch *istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch) bool { + return lo.SomeBy(bConfigPatches, func(bConfigPatch *istioapinetworkingv1alpha3.EnvoyFilter_EnvoyConfigObjectPatch) bool { + if aConfigPatch == nil && bConfigPatch == nil { + return true + } + if (aConfigPatch == nil && bConfigPatch != nil) || (aConfigPatch != nil && bConfigPatch == nil) { + return false + } + + // apply_to + if aConfigPatch.ApplyTo != bConfigPatch.ApplyTo { + return false + } + + // cluster match + aCluster := aConfigPatch.Match.GetCluster() + bCluster := bConfigPatch.Match.GetCluster() + if aCluster == nil || bCluster == nil { + return false + } + if aCluster.Service != bCluster.Service || aCluster.PortNumber != bCluster.PortNumber || aCluster.Subset != bCluster.Subset { + return false + } + + // patch + aPatch := aConfigPatch.Patch + bPatch := bConfigPatch.Patch + + if aPatch.Operation != bPatch.Operation || aPatch.FilterClass != bPatch.FilterClass { + return false + } + + aPatchJSON, _ := aPatch.Value.MarshalJSON() + bPatchJSON, _ := aPatch.Value.MarshalJSON() + return string(aPatchJSON) == string(bPatchJSON) + }) + }) +} + func ConditionToProperConditionFunc(istioCondition *istioapimetav1alpha1.IstioCondition, _ int) metav1.Condition { return metav1.Condition{ Type: istioCondition.GetType(), diff --git a/pkg/library/kuadrant/apimachinery_status_conditions.go b/pkg/library/kuadrant/apimachinery_status_conditions.go index 6c98f4bf8..6a9256fa0 100644 --- a/pkg/library/kuadrant/apimachinery_status_conditions.go +++ b/pkg/library/kuadrant/apimachinery_status_conditions.go @@ -6,11 +6,8 @@ import ( "fmt" "slices" "sort" - "sync" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" ) @@ -23,52 +20,6 @@ const ( PolicyReasonMissingDependency gatewayapiv1alpha2.PolicyConditionReason = "MissingDependency" ) -func NewAffectedPolicyMap() *AffectedPolicyMap { - return &AffectedPolicyMap{ - policies: make(map[types.UID][]client.ObjectKey), - } -} - -type AffectedPolicyMap struct { - policies map[types.UID][]client.ObjectKey - mu sync.RWMutex -} - -// SetAffectedPolicy sets the provided Policy as Affected in the tracking map. -func (o *AffectedPolicyMap) SetAffectedPolicy(p Policy, affectedBy []client.ObjectKey) { - o.mu.Lock() - defer o.mu.Unlock() - - if o.policies == nil { - o.policies = make(map[types.UID][]client.ObjectKey) - } - o.policies[p.GetUID()] = affectedBy -} - -// RemoveAffectedPolicy removes the provided Policy from the tracking map of Affected policies. -func (o *AffectedPolicyMap) RemoveAffectedPolicy(p Policy) { - o.mu.Lock() - defer o.mu.Unlock() - - delete(o.policies, p.GetUID()) -} - -// IsPolicyAffected checks if the provided Policy is affected based on the tracking map maintained. -func (o *AffectedPolicyMap) IsPolicyAffected(p Policy) bool { - return o.policies[p.GetUID()] != nil -} - -// IsPolicyOverridden checks if the provided Policy is affected based on the tracking map maintained. -// It is overridden if there is policies affecting it -func (o *AffectedPolicyMap) IsPolicyOverridden(p Policy) bool { - return o.IsPolicyAffected(p) && len(o.policies[p.GetUID()]) > 0 -} - -// PolicyAffectedBy returns the clients keys that a policy is Affected by -func (o *AffectedPolicyMap) PolicyAffectedBy(p Policy) []client.ObjectKey { - return o.policies[p.GetUID()] -} - // ConditionMarshal marshals the set of conditions as a JSON array, sorted by condition type. func ConditionMarshal(conditions []metav1.Condition) ([]byte, error) { condCopy := slices.Clone(conditions) diff --git a/pkg/library/kuadrant/kuadrant.go b/pkg/library/kuadrant/kuadrant.go index e50a6d033..a607f5009 100644 --- a/pkg/library/kuadrant/kuadrant.go +++ b/pkg/library/kuadrant/kuadrant.go @@ -20,7 +20,6 @@ const ( KuadrantNamespaceAnnotation = "kuadrant.io/namespace" TopologyLabel = "kuadrant.io/topology" ControllerName = "kuadrant.io/policy-controller" - AuthorinoServiceName = "authorino-authorino-authorization" ) type Policy interface { diff --git a/pkg/wasm/utils.go b/pkg/wasm/utils.go index 462f7bdd5..969fe4861 100644 --- a/pkg/wasm/utils.go +++ b/pkg/wasm/utils.go @@ -13,7 +13,6 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" - kuadrantv1 "github.com/kuadrant/kuadrant-operator/api/v1" kuadrantv1beta3 "github.com/kuadrant/kuadrant-operator/api/v1beta3" "github.com/kuadrant/kuadrant-operator/pkg/common" kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" @@ -42,23 +41,12 @@ func BuildConfigForActionSet(actionSets []ActionSet) Config { } } -type ActionBuilderFunc func(uniquePolicyRuleKey string, policyRule kuadrantv1.MergeableRule) (Action, error) - -func BuildActionSetsForPath(pathID string, path []machinery.Targetable, policyRules map[string]kuadrantv1.MergeableRule, actionBuilder ActionBuilderFunc) ([]kuadrantgatewayapi.HTTPRouteMatchConfig, error) { +func BuildActionSetsForPath(pathID string, path []machinery.Targetable, actions []Action) ([]kuadrantgatewayapi.HTTPRouteMatchConfig, error) { _, _, listener, httpRoute, httpRouteRule, err := common.ObjectsInRequestPath(path) if err != nil { return nil, err } - actions := lo.FilterMap(lo.Entries(policyRules), func(r lo.Entry[string, kuadrantv1.MergeableRule], _ int) (Action, bool) { - action, err := actionBuilder(r.Key, r.Value) - if err != nil { - errors.Join(err) - return Action{}, false - } - return action, true - }) - return lo.FlatMap(kuadrantgatewayapi.HostnamesFromListenerAndHTTPRoute(listener.Listener, httpRoute.HTTPRoute), func(hostname gatewayapiv1.Hostname, _ int) []kuadrantgatewayapi.HTTPRouteMatchConfig { return lo.Map(httpRouteRule.Matches, func(httpRouteMatch gatewayapiv1.HTTPRouteMatch, j int) kuadrantgatewayapi.HTTPRouteMatchConfig { actionSet := ActionSet{