diff --git a/controller/controller.go b/controller/controller.go index 72275c3..ec3198d 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -1,6 +1,7 @@ package controller import ( + "context" "log" "sync" "time" @@ -12,6 +13,13 @@ import ( "k8s.io/client-go/tools/cache" ) +type ResourceEvent struct { + Resource schema.GroupVersionResource + EventType EventType + OldObject RuntimeObject + NewObject RuntimeObject +} + type RuntimeLinkFunc func(objs Store) machinery.LinkFunc type ControllerOptions struct { @@ -24,7 +32,7 @@ type ControllerOptions struct { } type ControllerOptionFunc func(*ControllerOptions) -type CallbackFunc func(EventType, RuntimeObject, RuntimeObject, *machinery.Topology) +type CallbackFunc func(context.Context, ResourceEvent, *machinery.Topology) func WithClient(client *dynamic.DynamicClient) ControllerOptionFunc { return func(o *ControllerOptions) { @@ -65,7 +73,8 @@ func WithObjectLinks(objectLinks ...RuntimeLinkFunc) ControllerOptionFunc { func NewController(f ...ControllerOptionFunc) *Controller { opts := &ControllerOptions{ informers: map[string]InformerBuilder{}, - callback: func(EventType, RuntimeObject, RuntimeObject, *machinery.Topology) {}, + callback: func(context.Context, ResourceEvent, *machinery.Topology) { + }, } for _, fn := range f { @@ -75,7 +84,7 @@ func NewController(f ...ControllerOptionFunc) *Controller { controller := &Controller{ client: opts.client, cache: newCacheStore(), - topology: NewGatewayAPITopology(opts.policyKinds, opts.objectKinds, opts.objectLinks), + topology: newGatewayAPITopologyBuilder(opts.policyKinds, opts.objectKinds, opts.objectLinks), informers: map[string]cache.SharedInformer{}, callback: opts.callback, } @@ -88,10 +97,10 @@ func NewController(f ...ControllerOptionFunc) *Controller { } type Controller struct { - mu sync.Mutex + mu sync.RWMutex client *dynamic.DynamicClient cache *cacheStore - topology *GatewayAPITopology + topology *gatewayAPITopologyBuilder informers map[string]cache.SharedInformer callback CallbackFunc } @@ -117,16 +126,15 @@ func (c *Controller) Start() { wait.Until(func() {}, time.Second, stopCh) } -func (c *Controller) add(obj RuntimeObject) { +func (c *Controller) add(resource schema.GroupVersionResource, obj RuntimeObject) { c.mu.Lock() defer c.mu.Unlock() c.cache.Add(obj) - c.topology.Refresh(c.cache.List()) - c.propagate(CreateEvent, nil, obj) + c.propagate(ResourceEvent{resource, CreateEvent, nil, obj}) } -func (c *Controller) update(oldObj, newObj RuntimeObject) { +func (c *Controller) update(resource schema.GroupVersionResource, oldObj, newObj RuntimeObject) { c.mu.Lock() defer c.mu.Unlock() @@ -135,19 +143,18 @@ func (c *Controller) update(oldObj, newObj RuntimeObject) { } c.cache.Add(newObj) - c.topology.Refresh(c.cache.List()) - c.propagate(UpdateEvent, oldObj, newObj) + c.propagate(ResourceEvent{resource, UpdateEvent, oldObj, newObj}) } -func (c *Controller) delete(obj RuntimeObject) { +func (c *Controller) delete(resource schema.GroupVersionResource, obj RuntimeObject) { c.mu.Lock() defer c.mu.Unlock() c.cache.Delete(obj) - c.topology.Refresh(c.cache.List()) - c.propagate(DeleteEvent, obj, nil) + c.propagate(ResourceEvent{resource, DeleteEvent, obj, nil}) } -func (c *Controller) propagate(eventType EventType, oldObj, newObj RuntimeObject) { - c.callback(eventType, oldObj, newObj, c.topology.Get()) +func (c *Controller) propagate(resourceEvent ResourceEvent) { + topology := c.topology.Build(c.cache.List()) + c.callback(context.TODO(), resourceEvent, topology) } diff --git a/controller/informer.go b/controller/informer.go index e47c010..d702cc4 100644 --- a/controller/informer.go +++ b/controller/informer.go @@ -79,16 +79,16 @@ func For[T RuntimeObject](resource schema.GroupVersionResource, namespace string informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(o any) { obj := o.(T) - controller.add(obj) + controller.add(resource, obj) }, UpdateFunc: func(o, newO any) { oldObj := o.(T) newObj := newO.(T) - controller.update(oldObj, newObj) + controller.update(resource, oldObj, newObj) }, DeleteFunc: func(o any) { obj := o.(T) - controller.delete(obj) + controller.delete(resource, obj) }, }) informer.SetTransform(Restructure[T]) diff --git a/controller/resources.go b/controller/resources.go new file mode 100644 index 0000000..a5b42ef --- /dev/null +++ b/controller/resources.go @@ -0,0 +1,9 @@ +package controller + +import gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + +var ( + GatewayClassesResource = gwapiv1.SchemeGroupVersion.WithResource("gatewayclasses") + GatewaysResource = gwapiv1.SchemeGroupVersion.WithResource("gateways") + HTTPRoutesResource = gwapiv1.SchemeGroupVersion.WithResource("httproutes") +) diff --git a/controller/subscriber.go b/controller/subscriber.go new file mode 100644 index 0000000..75318fc --- /dev/null +++ b/controller/subscriber.go @@ -0,0 +1,44 @@ +package controller + +import ( + "context" + + "github.com/samber/lo" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/kuadrant/policy-machinery/machinery" +) + +type ResourceEventMatcher struct { + Resource *schema.GroupVersionResource + EventType *EventType + ObjectNamespace string + ObjectName string +} + +type Subscription struct { + ReconcileFunc CallbackFunc + Events []ResourceEventMatcher +} + +// Subscriber calls the reconciler function of the first subscription that matches the event +type Subscriber []Subscription + +func (s Subscriber) Reconcile(ctx context.Context, resourceEvent ResourceEvent, topology *machinery.Topology) { + subscription, found := lo.Find(s, func(subscription Subscription) bool { + _, found := lo.Find(subscription.Events, func(m ResourceEventMatcher) bool { + obj := resourceEvent.OldObject + if obj == nil { + obj = resourceEvent.NewObject + } + return (m.Resource == nil || *m.Resource == resourceEvent.Resource) && + (m.EventType == nil || *m.EventType == resourceEvent.EventType) && + (m.ObjectNamespace == "" || m.ObjectNamespace == obj.GetNamespace()) && + (m.ObjectName == "" || m.ObjectName == obj.GetName()) + }) + return found + }) + if found && subscription.ReconcileFunc != nil { + subscription.ReconcileFunc(ctx, resourceEvent, topology) + } +} diff --git a/controller/topology.go b/controller/topology_builder.go similarity index 83% rename from controller/topology.go rename to controller/topology_builder.go index e388bcd..ac23dae 100644 --- a/controller/topology.go +++ b/controller/topology_builder.go @@ -1,8 +1,6 @@ package controller import ( - "sync" - "github.com/samber/lo" core "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -11,27 +9,21 @@ import ( "github.com/kuadrant/policy-machinery/machinery" ) -func NewGatewayAPITopology(policyKinds, objectKinds []schema.GroupKind, objectLinks []RuntimeLinkFunc) *GatewayAPITopology { - return &GatewayAPITopology{ - topology: machinery.NewTopology(), +func newGatewayAPITopologyBuilder(policyKinds, objectKinds []schema.GroupKind, objectLinks []RuntimeLinkFunc) *gatewayAPITopologyBuilder { + return &gatewayAPITopologyBuilder{ policyKinds: policyKinds, objectKinds: objectKinds, objectLinks: objectLinks, } } -type GatewayAPITopology struct { - mu sync.RWMutex - topology *machinery.Topology +type gatewayAPITopologyBuilder struct { policyKinds []schema.GroupKind objectKinds []schema.GroupKind objectLinks []RuntimeLinkFunc } -func (t *GatewayAPITopology) Refresh(objs Store) { - t.mu.Lock() - defer t.mu.Unlock() - +func (t *gatewayAPITopologyBuilder) Build(objs Store) *machinery.Topology { gatewayClasses := lo.FilterMap(lo.Values(objs[schema.GroupKind{Group: gwapiv1.GroupVersion.Group, Kind: "GatewayClass"}]), func(obj RuntimeObject, _ int) (*gwapiv1.GatewayClass, bool) { gc, ok := obj.(*gwapiv1.GatewayClass) if !ok { @@ -102,17 +94,7 @@ func (t *GatewayAPITopology) Refresh(objs Store) { opts = append(opts, machinery.WithGatewayAPITopologyObjects(objects...)) } - t.topology = machinery.NewGatewayAPITopology(opts...) -} - -func (t *GatewayAPITopology) Get() *machinery.Topology { - t.mu.RLock() - defer t.mu.RUnlock() - if t.topology == nil { - return nil - } - topology := *t.topology - return &topology + return machinery.NewGatewayAPITopology(opts...) } type Object struct { diff --git a/controller/workflow.go b/controller/workflow.go new file mode 100644 index 0000000..8d06b26 --- /dev/null +++ b/controller/workflow.go @@ -0,0 +1,40 @@ +package controller + +import ( + "context" + "sync" + + "github.com/kuadrant/policy-machinery/machinery" +) + +// Workflow runs an optional precondition reconciliation function, then dispatches the reconciliation event to +// a list of concurrent reconciliation tasks, and runs an optional postcondition reconciliation function. +type Workflow struct { + Precondition CallbackFunc + Tasks []CallbackFunc + Postcondition CallbackFunc +} + +func (d *Workflow) Run(ctx context.Context, resourceEvent ResourceEvent, topology *machinery.Topology) { + // run precondition reconcile function + if d.Precondition != nil { + d.Precondition(ctx, resourceEvent, topology) + } + + // dispatch the event to concurrent tasks + funcs := d.Tasks + waitGroup := &sync.WaitGroup{} + waitGroup.Add(len(funcs)) + for _, f := range funcs { + go func() { + defer waitGroup.Done() + f(ctx, resourceEvent, topology) + }() + } + waitGroup.Wait() + + // run precondition reconcile function + if d.Postcondition != nil { + d.Postcondition(ctx, resourceEvent, topology) + } +} diff --git a/examples/kuadrant/apis/v1alpha2/dnspolicy_types.go b/examples/kuadrant/apis/v1alpha2/dnspolicy_types.go index 0bbd577..6c25d6d 100644 --- a/examples/kuadrant/apis/v1alpha2/dnspolicy_types.go +++ b/examples/kuadrant/apis/v1alpha2/dnspolicy_types.go @@ -2,6 +2,7 @@ package v1alpha2 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" gwapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "github.com/kuadrant/policy-machinery/machinery" @@ -9,6 +10,11 @@ import ( kuadrantapis "github.com/kuadrant/policy-machinery/examples/kuadrant/apis" ) +var ( + DNSPolicyKind = schema.GroupKind{Group: SchemeGroupVersion.Group, Kind: "DNSPolicy"} + DNSPoliciesResource = SchemeGroupVersion.WithResource("dnspolicies") +) + // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:metadata:labels="gateway.networking.k8s.io/policy=inherited" diff --git a/examples/kuadrant/apis/v1alpha2/tlspolicy_types.go b/examples/kuadrant/apis/v1alpha2/tlspolicy_types.go index 9ee88dc..760989f 100644 --- a/examples/kuadrant/apis/v1alpha2/tlspolicy_types.go +++ b/examples/kuadrant/apis/v1alpha2/tlspolicy_types.go @@ -4,6 +4,7 @@ import ( certmanv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" certmanmetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" gwapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "github.com/kuadrant/policy-machinery/machinery" @@ -11,6 +12,11 @@ import ( kuadrantapis "github.com/kuadrant/policy-machinery/examples/kuadrant/apis" ) +var ( + TLSPolicyKind = schema.GroupKind{Group: SchemeGroupVersion.Group, Kind: "TLSPolicy"} + TLSPoliciesResource = SchemeGroupVersion.WithResource("tlspolicies") +) + // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:metadata:labels="gateway.networking.k8s.io/policy=inherited" diff --git a/examples/kuadrant/apis/v1beta3/authpolicy_types.go b/examples/kuadrant/apis/v1beta3/authpolicy_types.go index aaa1193..192bbf0 100644 --- a/examples/kuadrant/apis/v1beta3/authpolicy_types.go +++ b/examples/kuadrant/apis/v1beta3/authpolicy_types.go @@ -7,6 +7,7 @@ import ( authorinov1beta2 "github.com/kuadrant/authorino/api/v1beta2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" gwapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "github.com/kuadrant/policy-machinery/machinery" @@ -14,6 +15,11 @@ import ( kuadrantapis "github.com/kuadrant/policy-machinery/examples/kuadrant/apis" ) +var ( + AuthPolicyKind = schema.GroupKind{Group: SchemeGroupVersion.Group, Kind: "AuthPolicy"} + AuthPoliciesResource = SchemeGroupVersion.WithResource("authpolicies") +) + // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:metadata:labels="gateway.networking.k8s.io/policy=inherited" diff --git a/examples/kuadrant/apis/v1beta3/ratelimitpolicy_types.go b/examples/kuadrant/apis/v1beta3/ratelimitpolicy_types.go index a35ce8f..37f6928 100644 --- a/examples/kuadrant/apis/v1beta3/ratelimitpolicy_types.go +++ b/examples/kuadrant/apis/v1beta3/ratelimitpolicy_types.go @@ -4,6 +4,7 @@ import ( "encoding/json" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" gwapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "github.com/kuadrant/policy-machinery/machinery" @@ -11,6 +12,11 @@ import ( kuadrantapis "github.com/kuadrant/policy-machinery/examples/kuadrant/apis" ) +var ( + RateLimitPolicyKind = schema.GroupKind{Group: SchemeGroupVersion.Group, Kind: "RateLimitPolicy"} + RateLimitPoliciesResource = SchemeGroupVersion.WithResource("ratelimitpolicies") +) + const ( EqualOperator WhenConditionOperator = "eq" NotEqualOperator WhenConditionOperator = "neq" diff --git a/examples/kuadrant/envoy-gateway.md b/examples/kuadrant/docs/envoy-gateway.md similarity index 100% rename from examples/kuadrant/envoy-gateway.md rename to examples/kuadrant/docs/envoy-gateway.md diff --git a/examples/kuadrant/multiple-gateway-providers.md b/examples/kuadrant/docs/multiple-gateway-providers.md similarity index 100% rename from examples/kuadrant/multiple-gateway-providers.md rename to examples/kuadrant/docs/multiple-gateway-providers.md diff --git a/examples/kuadrant/main.go b/examples/kuadrant/main.go index 9172365..4e82c9d 100644 --- a/examples/kuadrant/main.go +++ b/examples/kuadrant/main.go @@ -1,26 +1,30 @@ package main import ( + "context" "log" "os" "strings" egv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/google/go-cmp/cmp" "github.com/samber/lo" istiov1 "istio.io/client-go/pkg/apis/security/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/tools/clientcmd" + "k8s.io/utils/ptr" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" kuadrantv1alpha2 "github.com/kuadrant/policy-machinery/examples/kuadrant/apis/v1alpha2" kuadrantv1beta3 "github.com/kuadrant/policy-machinery/examples/kuadrant/apis/v1beta3" + "github.com/kuadrant/policy-machinery/examples/kuadrant/reconcilers" ) -var supportedGatewayProviders = []string{envoyGatewayProvider, istioGatewayProvider} +var supportedGatewayProviders = []string{reconcilers.EnvoyGatewayProviderName, reconcilers.IstioGatewayProviderName} func main() { var gatewayProviders []string @@ -58,44 +62,94 @@ func main() { controllerOpts := []controller.ControllerOptionFunc{ controller.WithClient(client), - controller.WithInformer("gateway", controller.For[*gwapiv1.Gateway](gwapiv1.SchemeGroupVersion.WithResource("gateways"), metav1.NamespaceAll)), - controller.WithInformer("httproute", controller.For[*gwapiv1.HTTPRoute](gwapiv1.SchemeGroupVersion.WithResource("httproutes"), metav1.NamespaceAll)), - controller.WithInformer("dnspolicy", controller.For[*kuadrantv1alpha2.DNSPolicy](kuadrantv1alpha2.SchemeGroupVersion.WithResource("dnspolicies"), metav1.NamespaceAll)), - controller.WithInformer("tlspolicy", controller.For[*kuadrantv1alpha2.TLSPolicy](kuadrantv1alpha2.SchemeGroupVersion.WithResource("tlspolicies"), metav1.NamespaceAll)), - controller.WithInformer("authpolicy", controller.For[*kuadrantv1beta3.AuthPolicy](kuadrantv1beta3.SchemeGroupVersion.WithResource("authpolicies"), metav1.NamespaceAll)), - controller.WithInformer("ratelimitpolicy", controller.For[*kuadrantv1beta3.RateLimitPolicy](kuadrantv1beta3.SchemeGroupVersion.WithResource("ratelimitpolicies"), metav1.NamespaceAll)), + controller.WithInformer("gateway", controller.For[*gwapiv1.Gateway](controller.GatewaysResource, metav1.NamespaceAll)), + controller.WithInformer("httproute", controller.For[*gwapiv1.HTTPRoute](controller.HTTPRoutesResource, metav1.NamespaceAll)), + controller.WithInformer("dnspolicy", controller.For[*kuadrantv1alpha2.DNSPolicy](kuadrantv1alpha2.DNSPoliciesResource, metav1.NamespaceAll)), + controller.WithInformer("tlspolicy", controller.For[*kuadrantv1alpha2.TLSPolicy](kuadrantv1alpha2.TLSPoliciesResource, metav1.NamespaceAll)), + controller.WithInformer("authpolicy", controller.For[*kuadrantv1beta3.AuthPolicy](kuadrantv1beta3.AuthPoliciesResource, metav1.NamespaceAll)), + controller.WithInformer("ratelimitpolicy", controller.For[*kuadrantv1beta3.RateLimitPolicy](kuadrantv1beta3.RateLimitPoliciesResource, metav1.NamespaceAll)), controller.WithPolicyKinds( - schema.GroupKind{Group: kuadrantv1alpha2.SchemeGroupVersion.Group, Kind: "DNSPolicy"}, - schema.GroupKind{Group: kuadrantv1alpha2.SchemeGroupVersion.Group, Kind: "TLSPolicy"}, - schema.GroupKind{Group: kuadrantv1beta3.SchemeGroupVersion.Group, Kind: "AuthPolicy"}, - schema.GroupKind{Group: kuadrantv1beta3.SchemeGroupVersion.Group, Kind: "RateLimitPolicy"}, + kuadrantv1alpha2.DNSPolicyKind, + kuadrantv1alpha2.TLSPolicyKind, + kuadrantv1beta3.AuthPolicyKind, + kuadrantv1beta3.RateLimitPolicyKind, ), - controller.WithCallback(buildReconciler(gatewayProviders, client).Reconcile), + controller.WithCallback(buildReconciler(gatewayProviders, client)), } controllerOpts = append(controllerOpts, controllerOptionsFor(gatewayProviders)...) controller.NewController(controllerOpts...).Start() } -func buildReconciler(gatewayProviders []string, client *dynamic.DynamicClient) *Reconciler { - var providers []GatewayProvider +// buildReconciler builds a reconciler that executes the following workflow: +// 1. log event +// 2. save topology to file +// 3. effective policies +// 4. (gateway deleted) delete SecurityPolicy / (other events) reconcile SecurityPolicies +// 4. (gateway deleted) delete AuthorizationPolicy / (other events) reconcile AuthorizationPolicies +func buildReconciler(gatewayProviders []string, client *dynamic.DynamicClient) controller.CallbackFunc { + effectivePolicyReconciler := &reconcilers.EffectivePoliciesReconciler{Client: client} + + commonAuthPolicyResourceEventMatchers := []controller.ResourceEventMatcher{ + {Resource: ptr.To(controller.GatewayClassesResource)}, + {Resource: ptr.To(controller.GatewaysResource), EventType: ptr.To(controller.CreateEvent)}, + {Resource: ptr.To(controller.GatewaysResource), EventType: ptr.To(controller.UpdateEvent)}, + {Resource: ptr.To(controller.HTTPRoutesResource)}, + {Resource: ptr.To(kuadrantv1beta3.AuthPoliciesResource)}, + } for _, gatewayProvider := range gatewayProviders { switch gatewayProvider { - case envoyGatewayProvider: - providers = append(providers, &EnvoyGatewayProvider{client}) - case istioGatewayProvider: - providers = append(providers, &IstioGatewayProvider{client}) + case reconcilers.EnvoyGatewayProviderName: + envoyGatewayProvider := &reconcilers.EnvoyGatewayProvider{Client: client} + effectivePolicyReconciler.ReconcileFuncs = append(effectivePolicyReconciler.ReconcileFuncs, (&controller.Subscriber{ + { + ReconcileFunc: envoyGatewayProvider.ReconcileSecurityPolicies, + Events: append(commonAuthPolicyResourceEventMatchers, controller.ResourceEventMatcher{Resource: ptr.To(reconcilers.EnvoyGatewaySecurityPoliciesResource)}), + }, + { + ReconcileFunc: envoyGatewayProvider.DeleteSecurityPolicy, + Events: []controller.ResourceEventMatcher{ + {Resource: ptr.To(controller.GatewaysResource), EventType: ptr.To(controller.DeleteEvent)}, + }, + }, + }).Reconcile) + case reconcilers.IstioGatewayProviderName: + istioGatewayProvider := &reconcilers.IstioGatewayProvider{Client: client} + effectivePolicyReconciler.ReconcileFuncs = append(effectivePolicyReconciler.ReconcileFuncs, (&controller.Subscriber{ + { + ReconcileFunc: istioGatewayProvider.ReconcileAuthorizationPolicies, + Events: append(commonAuthPolicyResourceEventMatchers, controller.ResourceEventMatcher{Resource: ptr.To(reconcilers.IstioAuthorizationPoliciesResource)}), + }, + { + ReconcileFunc: istioGatewayProvider.DeleteAuthorizationPolicy, + Events: []controller.ResourceEventMatcher{ + {Resource: ptr.To(controller.GatewaysResource), EventType: ptr.To(controller.DeleteEvent)}, + }, + }, + }).Reconcile) } } - if len(providers) == 0 { - providers = append(providers, &DefaultGatewayProvider{}) + reconciler := &controller.Workflow{ + Precondition: func(_ context.Context, resourceEvent controller.ResourceEvent, topology *machinery.Topology) { + // log the event + obj := resourceEvent.OldObject + if obj == nil { + obj = resourceEvent.NewObject + } + log.Printf("%s %sd: %s/%s\n", obj.GetObjectKind().GroupVersionKind().Kind, resourceEvent.EventType.String(), obj.GetNamespace(), obj.GetName()) + if resourceEvent.EventType == controller.UpdateEvent { + log.Println(cmp.Diff(resourceEvent.OldObject, resourceEvent.NewObject)) + } + }, + Tasks: []controller.CallbackFunc{ + effectivePolicyReconciler.Reconcile, + }, + Postcondition: (&reconcilers.TopologyFileReconciler{}).Reconcile, // Graphiz frees the memory that might be simutanously used by the reconcilers, so this needs to run in a precondition } - return &Reconciler{ - GatewayProviders: providers, - } + return reconciler.Run } func controllerOptionsFor(gatewayProviders []string) []controller.ControllerOptionFunc { @@ -103,19 +157,19 @@ func controllerOptionsFor(gatewayProviders []string) []controller.ControllerOpti // if we care about specificities of gateway controllers, then let's add gateway classes to the topology too if len(gatewayProviders) > 0 { - opts = append(opts, controller.WithInformer("gatewayclass", controller.For[*gwapiv1.GatewayClass](gwapiv1.SchemeGroupVersion.WithResource("gatewayclasses"), metav1.NamespaceNone))) + opts = append(opts, controller.WithInformer("gatewayclass", controller.For[*gwapiv1.GatewayClass](controller.GatewayClassesResource, metav1.NamespaceNone))) } for _, gatewayProvider := range gatewayProviders { switch gatewayProvider { - case envoyGatewayProvider: - opts = append(opts, controller.WithInformer("envoygateway/securitypolicy", controller.For[*egv1alpha1.SecurityPolicy](envoyGatewaySecurityPoliciesResource, metav1.NamespaceAll))) - opts = append(opts, controller.WithObjectKinds(envoyGatewaySecurityPolicyKind)) - opts = append(opts, controller.WithObjectLinks(linkGatewayToEnvoyGatewaySecurityPolicyFunc)) - case istioGatewayProvider: - opts = append(opts, controller.WithInformer("istio/authorizationpolicy", controller.For[*istiov1.AuthorizationPolicy](istioAuthorizationPoliciesResource, metav1.NamespaceAll))) - opts = append(opts, controller.WithObjectKinds(istioAuthorizationPolicyKind)) - opts = append(opts, controller.WithObjectLinks(linkGatewayToIstioAuthorizationPolicyFunc)) + case reconcilers.EnvoyGatewayProviderName: + opts = append(opts, controller.WithInformer("envoygateway/securitypolicy", controller.For[*egv1alpha1.SecurityPolicy](reconcilers.EnvoyGatewaySecurityPoliciesResource, metav1.NamespaceAll))) + opts = append(opts, controller.WithObjectKinds(reconcilers.EnvoyGatewaySecurityPolicyKind)) + opts = append(opts, controller.WithObjectLinks(reconcilers.LinkGatewayToEnvoyGatewaySecurityPolicyFunc)) + case reconcilers.IstioGatewayProviderName: + opts = append(opts, controller.WithInformer("istio/authorizationpolicy", controller.For[*istiov1.AuthorizationPolicy](reconcilers.IstioAuthorizationPoliciesResource, metav1.NamespaceAll))) + opts = append(opts, controller.WithObjectKinds(reconcilers.IstioAuthorizationPolicyKind)) + opts = append(opts, controller.WithObjectLinks(reconcilers.LinkGatewayToIstioAuthorizationPolicyFunc)) } } diff --git a/examples/kuadrant/reconciler.go b/examples/kuadrant/reconcilers/effective_policies_reconciler.go similarity index 68% rename from examples/kuadrant/reconciler.go rename to examples/kuadrant/reconcilers/effective_policies_reconciler.go index 3172f7b..02e502c 100644 --- a/examples/kuadrant/reconciler.go +++ b/examples/kuadrant/reconcilers/effective_policies_reconciler.go @@ -1,15 +1,16 @@ -package main +package reconcilers import ( + "context" "encoding/json" "fmt" "log" - "os" "sort" "strings" + "sync" - "github.com/google/go-cmp/cmp" "github.com/samber/lo" + "k8s.io/client-go/dynamic" "github.com/kuadrant/policy-machinery/controller" "github.com/kuadrant/policy-machinery/machinery" @@ -19,30 +20,17 @@ import ( kuadrantv1beta3 "github.com/kuadrant/policy-machinery/examples/kuadrant/apis/v1beta3" ) -const topologyFile = "topology.dot" +const authPathsKey = "authPaths" -type GatewayProvider interface { - ReconcileGateway(topology *machinery.Topology, gateway machinery.Targetable, capabilities map[string][][]machinery.Targetable) +// EffectivePoliciesReconciler works exactly like a controller.Workflow where the precondition reconcile function +// reconciles the effective policies for the given topology paths, occasionally modifying the context that is passed +// as argument to the subsequent concurrent reconcilers. +type EffectivePoliciesReconciler struct { + Client *dynamic.DynamicClient + ReconcileFuncs []controller.CallbackFunc } -type Reconciler struct { - GatewayProviders []GatewayProvider -} - -func (r *Reconciler) Reconcile(eventType controller.EventType, oldObj, newObj controller.RuntimeObject, topology *machinery.Topology) { - // print the event - obj := oldObj - if obj == nil { - obj = newObj - } - log.Printf("%s %sd: %s/%s\n", obj.GetObjectKind().GroupVersionKind().Kind, eventType.String(), obj.GetNamespace(), obj.GetName()) - if eventType == controller.UpdateEvent { - log.Println(cmp.Diff(oldObj, newObj)) - } - - // update the topology file - saveTopologyToFile(topology) - +func (r *EffectivePoliciesReconciler) Reconcile(ctx context.Context, resourceEvent controller.ResourceEvent, topology *machinery.Topology) { targetables := topology.Targetables() // reconcile policies @@ -61,11 +49,6 @@ func (r *Reconciler) Reconcile(eventType controller.EventType, oldObj, newObj co return ok }) - capabilities := map[string][][]machinery.Targetable{ - "auth": {}, - "ratelimit": {}, - } - for _, gateway := range gateways { // reconcile Gateway -> Listener policies for _, listener := range listeners { @@ -85,31 +68,26 @@ func (r *Reconciler) Reconcile(eventType controller.EventType, oldObj, newObj co paths := targetables.Paths(gateway, httpRouteRule) for i := range paths { if p := effectivePolicyForPath[*kuadrantv1beta3.AuthPolicy](paths[i]); p != nil { - capabilities["auth"] = append(capabilities["auth"], paths[i]) + ctx = pathIntoContext(ctx, authPathsKey, paths[i]) // TODO: reconcile auth effective policy (i.e. create the Authorino AuthConfig) } if p := effectivePolicyForPath[*kuadrantv1beta3.RateLimitPolicy](paths[i]); p != nil { - capabilities["ratelimit"] = append(capabilities["ratelimit"], paths[i]) // TODO: reconcile rate-limit effective policy (i.e. create the Limitador limits config) } } } - - for _, gatewayProvider := range r.GatewayProviders { - gatewayProvider.ReconcileGateway(topology, gateway, capabilities) - } } -} -func saveTopologyToFile(topology *machinery.Topology) { - file, err := os.Create(topologyFile) - if err != nil { - log.Fatal(err) - } - defer file.Close() - _, err = file.Write(topology.ToDot().Bytes()) - if err != nil { - log.Fatal(err) + // dispatch the event to subsequent reconcilers + funcs := r.ReconcileFuncs + waitGroup := &sync.WaitGroup{} + defer waitGroup.Wait() + waitGroup.Add(len(funcs)) + for _, f := range funcs { + go func() { + defer waitGroup.Done() + f(ctx, resourceEvent, topology) + }() } } @@ -146,9 +124,17 @@ func effectivePolicyForPath[T machinery.Policy](path []machinery.Targetable) *T return &concreteEffectivePolicy } -var _ GatewayProvider = &DefaultGatewayProvider{} - -type DefaultGatewayProvider struct{} +func pathIntoContext(ctx context.Context, key string, path []machinery.Targetable) context.Context { + if p := ctx.Value(key); p != nil { + return context.WithValue(ctx, key, append(p.([][]machinery.Targetable), path)) + } + return context.WithValue(ctx, key, [][]machinery.Targetable{path}) +} -func (p *DefaultGatewayProvider) ReconcileGateway(_ *machinery.Topology, _ machinery.Targetable, _ map[string][][]machinery.Targetable) { +func pathsFromContext(ctx context.Context, key string) [][]machinery.Targetable { + var paths [][]machinery.Targetable + if p := ctx.Value(key); p != nil { + paths = p.([][]machinery.Targetable) + } + return paths } diff --git a/examples/kuadrant/envoy_gateway.go b/examples/kuadrant/reconcilers/envoy_gateway.go similarity index 59% rename from examples/kuadrant/envoy_gateway.go rename to examples/kuadrant/reconcilers/envoy_gateway.go index d63b988..d80c16f 100644 --- a/examples/kuadrant/envoy_gateway.go +++ b/examples/kuadrant/reconcilers/envoy_gateway.go @@ -1,8 +1,9 @@ -package main +package reconcilers import ( "context" "log" + "strings" egv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" "github.com/samber/lo" @@ -17,43 +18,52 @@ import ( "github.com/kuadrant/policy-machinery/machinery" ) -const envoyGatewayProvider = "envoygateway" +const EnvoyGatewayProviderName = "envoygateway" var ( - _ GatewayProvider = &EnvoyGatewayProvider{} - - envoyGatewaySecurityPolicyKind = schema.GroupKind{Group: egv1alpha1.GroupName, Kind: "SecurityPolicy"} - envoyGatewaySecurityPoliciesResource = egv1alpha1.SchemeBuilder.GroupVersion.WithResource("securitypolicies") + EnvoyGatewaySecurityPolicyKind = schema.GroupKind{Group: egv1alpha1.GroupName, Kind: "SecurityPolicy"} + EnvoyGatewaySecurityPoliciesResource = egv1alpha1.SchemeBuilder.GroupVersion.WithResource("securitypolicies") ) type EnvoyGatewayProvider struct { - *dynamic.DynamicClient + Client *dynamic.DynamicClient } -func (p *EnvoyGatewayProvider) ReconcileGateway(topology *machinery.Topology, gateway machinery.Targetable, capabilities map[string][][]machinery.Targetable) { - // check if the gateway is managed by the envoy gateway controller - if !lo.ContainsBy(topology.Targetables().Parents(gateway), func(p machinery.Targetable) bool { - gc, ok := p.(*machinery.GatewayClass) - return ok && gc.Spec.ControllerName == "gateway.envoyproxy.io/gatewayclass-controller" - }) { - return +func (p *EnvoyGatewayProvider) ReconcileSecurityPolicies(ctx context.Context, resourceEvent controller.ResourceEvent, topology *machinery.Topology) { + authPaths := pathsFromContext(ctx, authPathsKey) + targetables := topology.Targetables() + gateways := targetables.Items(func(o machinery.Object) bool { + _, ok := o.(*machinery.Gateway) + return ok + }) + for _, gateway := range gateways { + paths := lo.Filter(authPaths, func(path []machinery.Targetable, _ int) bool { + if len(path) < 4 { // should never happen + log.Fatalf("Unexpected topology path length to build Envoy SecurityPolicy: %s\n", strings.Join(lo.Map(path, machinery.MapTargetableToURLFunc), " → ")) + } + return path[0].GetURL() == gateway.GetURL() && lo.ContainsBy(targetables.Parents(path[0]), func(parent machinery.Targetable) bool { + gc, ok := parent.(*machinery.GatewayClass) + return ok && gc.Spec.ControllerName == "gateway.envoyproxy.io/gatewayclass-controller" + }) + }) + if len(paths) > 0 { + p.createSecurityPolicy(ctx, topology, gateway) + continue + } + p.deleteSecurityPolicy(ctx, topology, gateway.GetNamespace(), gateway.GetName(), gateway) } +} - // reconcile envoy gateway securitypolicy resources - if lo.ContainsBy(capabilities["auth"], func(path []machinery.Targetable) bool { - return lo.Contains(lo.Map(path, machinery.MapTargetableToURLFunc), gateway.GetURL()) - }) { - p.createSecurityPolicy(topology, gateway) - return - } - p.deleteSecurityPolicy(topology, gateway) +func (p *EnvoyGatewayProvider) DeleteSecurityPolicy(ctx context.Context, resourceEvent controller.ResourceEvent, topology *machinery.Topology) { + gateway := resourceEvent.OldObject + p.deleteSecurityPolicy(ctx, topology, gateway.GetNamespace(), gateway.GetName(), nil) } -func (p *EnvoyGatewayProvider) createSecurityPolicy(topology *machinery.Topology, gateway machinery.Targetable) { +func (p *EnvoyGatewayProvider) createSecurityPolicy(ctx context.Context, topology *machinery.Topology, gateway machinery.Targetable) { desiredSecurityPolicy := &egv1alpha1.SecurityPolicy{ TypeMeta: metav1.TypeMeta{ APIVersion: egv1alpha1.GroupVersion.String(), - Kind: envoyGatewaySecurityPolicyKind.Kind, + Kind: EnvoyGatewaySecurityPolicyKind.Kind, }, ObjectMeta: metav1.ObjectMeta{ Name: gateway.GetName(), @@ -81,15 +91,15 @@ func (p *EnvoyGatewayProvider) createSecurityPolicy(topology *machinery.Topology }, } - resource := p.Resource(envoyGatewaySecurityPoliciesResource).Namespace(gateway.GetNamespace()) + resource := p.Client.Resource(EnvoyGatewaySecurityPoliciesResource).Namespace(gateway.GetNamespace()) obj, found := lo.Find(topology.Objects().Children(gateway), func(o machinery.Object) bool { - return o.GroupVersionKind().GroupKind() == envoyGatewaySecurityPolicyKind && o.GetNamespace() == gateway.GetNamespace() && o.GetName() == gateway.GetName() + return o.GroupVersionKind().GroupKind() == EnvoyGatewaySecurityPolicyKind && o.GetNamespace() == gateway.GetNamespace() && o.GetName() == gateway.GetName() }) if !found { o, _ := controller.Destruct(desiredSecurityPolicy) - _, err := resource.Create(context.TODO(), o, metav1.CreateOptions{}) + _, err := resource.Create(ctx, o, metav1.CreateOptions{}) if err != nil { log.Println("failed to create SecurityPolicy", err) } @@ -111,29 +121,33 @@ func (p *EnvoyGatewayProvider) createSecurityPolicy(topology *machinery.Topology securityPolicy.Spec = desiredSecurityPolicy.Spec o, _ := controller.Destruct(securityPolicy) - _, err := resource.Update(context.TODO(), o, metav1.UpdateOptions{}) + _, err := resource.Update(ctx, o, metav1.UpdateOptions{}) if err != nil { log.Println("failed to update SecurityPolicy", err) } } -func (p *EnvoyGatewayProvider) deleteSecurityPolicy(topology *machinery.Topology, gateway machinery.Targetable) { - _, found := lo.Find(topology.Objects().Children(gateway), func(o machinery.Object) bool { - return o.GroupVersionKind().GroupKind() == envoyGatewaySecurityPolicyKind && o.GetNamespace() == gateway.GetNamespace() && o.GetName() == gateway.GetName() +func (p *EnvoyGatewayProvider) deleteSecurityPolicy(ctx context.Context, topology *machinery.Topology, namespace, name string, parent machinery.Targetable) { + var objs []machinery.Object + if parent != nil { + objs = topology.Objects().Children(parent) + } else { + objs = topology.Objects().Items() + } + _, found := lo.Find(objs, func(o machinery.Object) bool { + return o.GroupVersionKind().GroupKind() == EnvoyGatewaySecurityPolicyKind && o.GetNamespace() == namespace && o.GetName() == name }) - if !found { return } - - resource := p.Resource(envoyGatewaySecurityPoliciesResource).Namespace(gateway.GetNamespace()) - err := resource.Delete(context.TODO(), gateway.GetName(), metav1.DeleteOptions{}) + resource := p.Client.Resource(EnvoyGatewaySecurityPoliciesResource).Namespace(namespace) + err := resource.Delete(ctx, name, metav1.DeleteOptions{}) if err != nil { log.Println("failed to delete SecurityPolicy", err) } } -func linkGatewayToEnvoyGatewaySecurityPolicyFunc(objs controller.Store) machinery.LinkFunc { +func LinkGatewayToEnvoyGatewaySecurityPolicyFunc(objs controller.Store) machinery.LinkFunc { gatewayKind := schema.GroupKind{Group: gwapiv1.GroupName, Kind: "Gateway"} gateways := lo.FilterMap(lo.Values(objs[gatewayKind]), func(obj controller.RuntimeObject, _ int) (*gwapiv1.Gateway, bool) { g, ok := obj.(*gwapiv1.Gateway) @@ -145,7 +159,7 @@ func linkGatewayToEnvoyGatewaySecurityPolicyFunc(objs controller.Store) machiner return machinery.LinkFunc{ From: gatewayKind, - To: envoyGatewaySecurityPolicyKind, + To: EnvoyGatewaySecurityPolicyKind, Func: func(child machinery.Object) []machinery.Object { o := child.(*controller.Object) sp := o.RuntimeObject.(*egv1alpha1.SecurityPolicy) diff --git a/examples/kuadrant/istio.go b/examples/kuadrant/reconcilers/istio.go similarity index 63% rename from examples/kuadrant/istio.go rename to examples/kuadrant/reconcilers/istio.go index 4b8d4a0..7896af6 100644 --- a/examples/kuadrant/istio.go +++ b/examples/kuadrant/reconcilers/istio.go @@ -1,4 +1,4 @@ -package main +package reconcilers import ( "context" @@ -21,44 +21,52 @@ import ( "github.com/kuadrant/policy-machinery/machinery" ) -const istioGatewayProvider = "istio" +const IstioGatewayProviderName = "istio" var ( - _ GatewayProvider = &IstioGatewayProvider{} - - istioAuthorizationPolicyKind = schema.GroupKind{Group: istiov1.GroupName, Kind: "AuthorizationPolicy"} - istioAuthorizationPoliciesResource = istiov1.SchemeGroupVersion.WithResource("authorizationpolicies") + IstioAuthorizationPolicyKind = schema.GroupKind{Group: istiov1.GroupName, Kind: "AuthorizationPolicy"} + IstioAuthorizationPoliciesResource = istiov1.SchemeGroupVersion.WithResource("authorizationpolicies") ) type IstioGatewayProvider struct { - *dynamic.DynamicClient + Client *dynamic.DynamicClient } -func (p *IstioGatewayProvider) ReconcileGateway(topology *machinery.Topology, gateway machinery.Targetable, capabilities map[string][][]machinery.Targetable) { - // check if the gateway is managed by the istio gateway controller - if !lo.ContainsBy(topology.Targetables().Parents(gateway), func(p machinery.Targetable) bool { - gc, ok := p.(*machinery.GatewayClass) - return ok && gc.Spec.ControllerName == "istio.io/gateway-controller" - }) { - return - } - - // reconcile istio authorizationpolicy resources - paths := lo.Filter(capabilities["auth"], func(path []machinery.Targetable, _ int) bool { - return lo.Contains(lo.Map(path, machinery.MapTargetableToURLFunc), gateway.GetURL()) +func (p *IstioGatewayProvider) ReconcileAuthorizationPolicies(ctx context.Context, resourceEvent controller.ResourceEvent, topology *machinery.Topology) { + authPaths := pathsFromContext(ctx, authPathsKey) + targetables := topology.Targetables() + gateways := targetables.Items(func(o machinery.Object) bool { + _, ok := o.(*machinery.Gateway) + return ok }) - if len(paths) > 0 { - p.createAuthorizationPolicy(topology, gateway, paths) - return + for _, gateway := range gateways { + paths := lo.Filter(authPaths, func(path []machinery.Targetable, _ int) bool { + if len(path) < 4 { // should never happen + log.Fatalf("Unexpected topology path length to build Istio AuthorizationPolicy: %s\n", strings.Join(lo.Map(path, machinery.MapTargetableToURLFunc), " → ")) + } + return path[0].GetURL() == gateway.GetURL() && lo.ContainsBy(targetables.Parents(path[0]), func(parent machinery.Targetable) bool { + gc, ok := parent.(*machinery.GatewayClass) + return ok && gc.Spec.ControllerName == "istio.io/gateway-controller" + }) + }) + if len(paths) > 0 { + p.createAuthorizationPolicy(ctx, topology, gateway, paths) + continue + } + p.deleteAuthorizationPolicy(ctx, topology, gateway.GetNamespace(), gateway.GetName(), gateway) } - p.deleteAuthorizationPolicy(topology, gateway) } -func (p *IstioGatewayProvider) createAuthorizationPolicy(topology *machinery.Topology, gateway machinery.Targetable, paths [][]machinery.Targetable) { +func (p *IstioGatewayProvider) DeleteAuthorizationPolicy(ctx context.Context, resourceEvent controller.ResourceEvent, topology *machinery.Topology) { + gateway := resourceEvent.OldObject + p.deleteAuthorizationPolicy(ctx, topology, gateway.GetNamespace(), gateway.GetName(), nil) +} + +func (p *IstioGatewayProvider) createAuthorizationPolicy(ctx context.Context, topology *machinery.Topology, gateway machinery.Targetable, paths [][]machinery.Targetable) { desiredAuthorizationPolicy := &istiov1.AuthorizationPolicy{ TypeMeta: metav1.TypeMeta{ APIVersion: istiov1.SchemeGroupVersion.String(), - Kind: istioAuthorizationPolicyKind.Kind, + Kind: IstioAuthorizationPolicyKind.Kind, }, ObjectMeta: metav1.ObjectMeta{ Name: gateway.GetName(), @@ -80,26 +88,27 @@ func (p *IstioGatewayProvider) createAuthorizationPolicy(topology *machinery.Top } for _, path := range paths { - if len(path) < 4 { - log.Printf("Unexpected topology path length to build Istio AuthorizationPolicy: %s\n", strings.Join(lo.Map(path, machinery.MapTargetableToURLFunc), " → ")) - continue - } listener := path[1].(*machinery.Listener) + httpRoute := path[2].(*machinery.HTTPRoute) routeRule := path[3].(*machinery.HTTPRouteRule) hostname := ptr.Deref(listener.Hostname, gwapiv1.Hostname("*")) - rules := istioAuthorizationPolicyRulesFromHTTPRouteRule(routeRule.HTTPRouteRule, []gwapiv1.Hostname{hostname}) + hostnames := []gwapiv1.Hostname{hostname} + if len(httpRoute.Spec.Hostnames) > 0 { + hostnames = lo.Filter(httpRoute.Spec.Hostnames, hostSubsetOf(hostname)) + } + rules := istioAuthorizationPolicyRulesFromHTTPRouteRule(routeRule.HTTPRouteRule, hostnames) desiredAuthorizationPolicy.Spec.Rules = append(desiredAuthorizationPolicy.Spec.Rules, rules...) } - resource := p.Resource(istioAuthorizationPoliciesResource).Namespace(gateway.GetNamespace()) + resource := p.Client.Resource(IstioAuthorizationPoliciesResource).Namespace(gateway.GetNamespace()) obj, found := lo.Find(topology.Objects().Children(gateway), func(o machinery.Object) bool { - return o.GroupVersionKind().GroupKind() == istioAuthorizationPolicyKind && o.GetNamespace() == gateway.GetNamespace() && o.GetName() == gateway.GetName() + return o.GroupVersionKind().GroupKind() == IstioAuthorizationPolicyKind && o.GetNamespace() == gateway.GetNamespace() && o.GetName() == gateway.GetName() }) if !found { o, _ := controller.Destruct(desiredAuthorizationPolicy) - _, err := resource.Create(context.TODO(), o, metav1.CreateOptions{}) + _, err := resource.Create(ctx, o, metav1.CreateOptions{}) if err != nil { log.Println("failed to create AuthorizationPolicy", err) } @@ -120,23 +129,27 @@ func (p *IstioGatewayProvider) createAuthorizationPolicy(topology *machinery.Top authorizationPolicy.Spec.ActionDetail = desiredAuthorizationPolicy.Spec.ActionDetail authorizationPolicy.Spec.Rules = desiredAuthorizationPolicy.Spec.Rules o, _ := controller.Destruct(authorizationPolicy) - _, err := resource.Update(context.TODO(), o, metav1.UpdateOptions{}) + _, err := resource.Update(ctx, o, metav1.UpdateOptions{}) if err != nil { log.Println("failed to update AuthorizationPolicy", err) } } -func (p *IstioGatewayProvider) deleteAuthorizationPolicy(topology *machinery.Topology, gateway machinery.Targetable) { - _, found := lo.Find(topology.Objects().Children(gateway), func(o machinery.Object) bool { - return o.GroupVersionKind().GroupKind() == istioAuthorizationPolicyKind && o.GetNamespace() == gateway.GetNamespace() && o.GetName() == gateway.GetName() +func (p *IstioGatewayProvider) deleteAuthorizationPolicy(ctx context.Context, topology *machinery.Topology, namespace, name string, parent machinery.Targetable) { + var objs []machinery.Object + if parent != nil { + objs = topology.Objects().Children(parent) + } else { + objs = topology.Objects().Items() + } + _, found := lo.Find(objs, func(o machinery.Object) bool { + return o.GroupVersionKind().GroupKind() == IstioAuthorizationPolicyKind && o.GetNamespace() == namespace && o.GetName() == name }) - if !found { return } - - resource := p.Resource(istioAuthorizationPoliciesResource).Namespace(gateway.GetNamespace()) - err := resource.Delete(context.TODO(), gateway.GetName(), metav1.DeleteOptions{}) + resource := p.Client.Resource(IstioAuthorizationPoliciesResource).Namespace(namespace) + err := resource.Delete(ctx, name, metav1.DeleteOptions{}) if err != nil { log.Println("failed to delete AuthorizationPolicy", err) } @@ -244,7 +257,36 @@ func istioAuthorizationPolicyRulesFromHTTPRouteRule(rule *gwapiv1.HTTPRouteRule, return } -func linkGatewayToIstioAuthorizationPolicyFunc(objs controller.Store) machinery.LinkFunc { +// TODO: move this to a shared package +func hostSubsetOf(superset gwapiv1.Hostname) func(gwapiv1.Hostname, int) bool { + wildcarded := func(hostname gwapiv1.Hostname) bool { + return len(hostname) > 0 && hostname[0] == '*' + } + + return func(hostname gwapiv1.Hostname, _ int) bool { + if wildcarded(hostname) { + if wildcarded(superset) { + // both hostname and superset contain wildcard + if len(hostname) < len(superset) { + return false + } + return strings.HasSuffix(string(hostname[1:]), string(superset[1:])) + } + // only hostname contains wildcard + return false + } + + if wildcarded(superset) { + // only superset contains wildcard + return strings.HasSuffix(string(hostname), string(superset[1:])) + } + + // neither contains wildcard, so do normal string comparison + return hostname == superset + } +} + +func LinkGatewayToIstioAuthorizationPolicyFunc(objs controller.Store) machinery.LinkFunc { gatewayKind := schema.GroupKind{Group: gwapiv1.GroupName, Kind: "Gateway"} gateways := lo.FilterMap(lo.Values(objs[gatewayKind]), func(obj controller.RuntimeObject, _ int) (*gwapiv1.Gateway, bool) { g, ok := obj.(*gwapiv1.Gateway) @@ -256,7 +298,7 @@ func linkGatewayToIstioAuthorizationPolicyFunc(objs controller.Store) machinery. return machinery.LinkFunc{ From: gatewayKind, - To: istioAuthorizationPolicyKind, + To: IstioAuthorizationPolicyKind, Func: func(child machinery.Object) []machinery.Object { o := child.(*controller.Object) ap := o.RuntimeObject.(*istiov1.AuthorizationPolicy) diff --git a/examples/kuadrant/reconcilers/topology_file_reconciler.go b/examples/kuadrant/reconcilers/topology_file_reconciler.go new file mode 100644 index 0000000..269ae6f --- /dev/null +++ b/examples/kuadrant/reconcilers/topology_file_reconciler.go @@ -0,0 +1,26 @@ +package reconcilers + +import ( + "context" + "log" + "os" + + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" +) + +const topologyFile = "topology.dot" + +type TopologyFileReconciler struct{} + +func (r *TopologyFileReconciler) Reconcile(_ context.Context, _ controller.ResourceEvent, topology *machinery.Topology) { + file, err := os.Create(topologyFile) + if err != nil { + log.Fatal(err) + } + defer file.Close() + _, err = file.Write(topology.ToDot().Bytes()) + if err != nil { + log.Fatal(err) + } +}