diff --git a/controller/cache.go b/controller/cache.go index a4b7761..9a9715c 100644 --- a/controller/cache.go +++ b/controller/cache.go @@ -13,24 +13,24 @@ type RuntimeObject interface { metav1.Object } -type cacheMap map[schema.GroupKind]map[string]RuntimeObject +type Store map[schema.GroupKind]map[string]RuntimeObject type cacheStore struct { mu sync.RWMutex - store cacheMap + store Store } func newCacheStore() *cacheStore { return &cacheStore{ - store: make(cacheMap), + store: make(Store), } } -func (c *cacheStore) List() cacheMap { +func (c *cacheStore) List() Store { c.mu.RLock() defer c.mu.RUnlock() - cm := make(cacheMap, len(c.store)) + cm := make(Store, len(c.store)) for gk, objs := range c.store { if _, ok := cm[gk]; !ok { cm[gk] = map[string]RuntimeObject{} diff --git a/controller/controller.go b/controller/controller.go index 090c244..72275c3 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -12,11 +12,15 @@ import ( "k8s.io/client-go/tools/cache" ) +type RuntimeLinkFunc func(objs Store) machinery.LinkFunc + type ControllerOptions struct { client *dynamic.DynamicClient informers map[string]InformerBuilder callback CallbackFunc policyKinds []schema.GroupKind + objectKinds []schema.GroupKind + objectLinks []RuntimeLinkFunc } type ControllerOptionFunc func(*ControllerOptions) @@ -42,7 +46,19 @@ func WithCallback(callback CallbackFunc) ControllerOptionFunc { func WithPolicyKinds(policyKinds ...schema.GroupKind) ControllerOptionFunc { return func(o *ControllerOptions) { - o.policyKinds = policyKinds + o.policyKinds = append(o.policyKinds, policyKinds...) + } +} + +func WithObjectKinds(objectKinds ...schema.GroupKind) ControllerOptionFunc { + return func(o *ControllerOptions) { + o.objectKinds = append(o.objectKinds, objectKinds...) + } +} + +func WithObjectLinks(objectLinks ...RuntimeLinkFunc) ControllerOptionFunc { + return func(o *ControllerOptions) { + o.objectLinks = append(o.objectLinks, objectLinks...) } } @@ -59,7 +75,7 @@ func NewController(f ...ControllerOptionFunc) *Controller { controller := &Controller{ client: opts.client, cache: newCacheStore(), - topology: NewGatewayAPITopology(opts.policyKinds...), + topology: NewGatewayAPITopology(opts.policyKinds, opts.objectKinds, opts.objectLinks), informers: map[string]cache.SharedInformer{}, callback: opts.callback, } diff --git a/controller/informer.go b/controller/informer.go index 67f92c9..e47c010 100644 --- a/controller/informer.go +++ b/controller/informer.go @@ -25,16 +25,51 @@ func (t *EventType) String() string { return [...]string{"create", "update", "delete"}[*t] } +type InformerBuilderOptions struct { + LabelSelector string + FieldSelector string +} + +type InformerBuilderOptionsFunc func(*InformerBuilderOptions) + +func FilterResourcesByLabel(selector string) InformerBuilderOptionsFunc { + return func(o *InformerBuilderOptions) { + o.LabelSelector = selector + } +} + +func FilterResourcesByField(selector string) InformerBuilderOptionsFunc { + return func(o *InformerBuilderOptions) { + o.FieldSelector = selector + } +} + type InformerBuilder func(controller *Controller) cache.SharedInformer -func For[T RuntimeObject](resource schema.GroupVersionResource, namespace string) InformerBuilder { +func For[T RuntimeObject](resource schema.GroupVersionResource, namespace string, options ...InformerBuilderOptionsFunc) InformerBuilder { + o := &InformerBuilderOptions{} + for _, f := range options { + f(o) + } return func(controller *Controller) cache.SharedInformer { informer := cache.NewSharedInformer( &cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { + if o.LabelSelector != "" { + options.LabelSelector = o.LabelSelector + } + if o.FieldSelector != "" { + options.FieldSelector = o.FieldSelector + } return controller.client.Resource(resource).Namespace(namespace).List(context.Background(), options) }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { + if o.LabelSelector != "" { + options.LabelSelector = o.LabelSelector + } + if o.FieldSelector != "" { + options.FieldSelector = o.FieldSelector + } return controller.client.Resource(resource).Namespace(namespace).Watch(context.Background(), options) }, }, @@ -56,12 +91,12 @@ func For[T RuntimeObject](resource schema.GroupVersionResource, namespace string controller.delete(obj) }, }) - informer.SetTransform(restructure[T]) + informer.SetTransform(Restructure[T]) return informer } } -func restructure[T any](obj any) (any, error) { +func Restructure[T any](obj any) (any, error) { unstructuredObj, ok := obj.(*unstructured.Unstructured) if !ok { return nil, fmt.Errorf("unexpected object type: %T", obj) @@ -72,3 +107,11 @@ func restructure[T any](obj any) (any, error) { } return o, nil } + +func Destruct[T any](obj T) (*unstructured.Unstructured, error) { + u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&obj) + if err != nil { + return nil, err + } + return &unstructured.Unstructured{Object: u}, nil +} diff --git a/controller/topology.go b/controller/topology.go index 39b476e..e388bcd 100644 --- a/controller/topology.go +++ b/controller/topology.go @@ -11,10 +11,12 @@ import ( "github.com/kuadrant/policy-machinery/machinery" ) -func NewGatewayAPITopology(policyKinds ...schema.GroupKind) *GatewayAPITopology { +func NewGatewayAPITopology(policyKinds, objectKinds []schema.GroupKind, objectLinks []RuntimeLinkFunc) *GatewayAPITopology { return &GatewayAPITopology{ topology: machinery.NewTopology(), policyKinds: policyKinds, + objectKinds: objectKinds, + objectLinks: objectLinks, } } @@ -22,9 +24,11 @@ type GatewayAPITopology struct { mu sync.RWMutex topology *machinery.Topology policyKinds []schema.GroupKind + objectKinds []schema.GroupKind + objectLinks []RuntimeLinkFunc } -func (t *GatewayAPITopology) Refresh(objs cacheMap) { +func (t *GatewayAPITopology) Refresh(objs Store) { t.mu.Lock() defer t.mu.Unlock() @@ -60,6 +64,10 @@ func (t *GatewayAPITopology) Refresh(objs cacheMap) { return service, true }) + linkFuncs := lo.Map(t.objectLinks, func(linkFunc RuntimeLinkFunc, _ int) machinery.LinkFunc { + return linkFunc(objs) + }) + opts := []machinery.GatewayAPITopologyOptionsFunc{ machinery.WithGatewayClasses(gatewayClasses...), machinery.WithGateways(gateways...), @@ -68,6 +76,7 @@ func (t *GatewayAPITopology) Refresh(objs cacheMap) { machinery.ExpandGatewayListeners(), machinery.ExpandHTTPRouteRules(), machinery.ExpandServicePorts(), + machinery.WithGatewayAPITopologyLinks(linkFuncs...), } for i := range t.policyKinds { @@ -80,6 +89,19 @@ func (t *GatewayAPITopology) Refresh(objs cacheMap) { opts = append(opts, machinery.WithGatewayAPITopologyPolicies(policies...)) } + for i := range t.objectKinds { + objectKind := t.objectKinds[i] + objects := lo.FilterMap(lo.Values(objs[objectKind]), func(obj RuntimeObject, _ int) (machinery.Object, bool) { + object, ok := obj.(machinery.Object) + if ok { + return object, ok + } + return &Object{obj}, true + }) + + opts = append(opts, machinery.WithGatewayAPITopologyObjects(objects...)) + } + t.topology = machinery.NewGatewayAPITopology(opts...) } @@ -92,3 +114,25 @@ func (t *GatewayAPITopology) Get() *machinery.Topology { topology := *t.topology return &topology } + +type Object struct { + RuntimeObject RuntimeObject +} + +func (g *Object) GroupVersionKind() schema.GroupVersionKind { + return g.RuntimeObject.GetObjectKind().GroupVersionKind() +} + +func (g *Object) SetGroupVersionKind(schema.GroupVersionKind) {} + +func (g *Object) GetNamespace() string { + return g.RuntimeObject.GetNamespace() +} + +func (g *Object) GetName() string { + return g.RuntimeObject.GetName() +} + +func (g *Object) GetURL() string { + return machinery.UrlFromObject(g) +} diff --git a/examples/color_policy/integration_test.go b/examples/color_policy/integration_test.go index 8d87b77..3a3b75c 100644 --- a/examples/color_policy/integration_test.go +++ b/examples/color_policy/integration_test.go @@ -151,11 +151,13 @@ func TestKuadrantMergeBasedOnTopology(t *testing.T) { machinery.SaveToOutputDir(t, topology.ToDot(), "../../tests/out", ".dot") - gateways := topology.Targetables(func(o machinery.Object) bool { + targetables := topology.Targetables() + + gateways := targetables.Items(func(o machinery.Object) bool { _, ok := o.(*machinery.Gateway) return ok }) - httpRouteRules := topology.Targetables(func(o machinery.Object) bool { + httpRouteRules := targetables.Items(func(o machinery.Object) bool { _, ok := o.(*machinery.HTTPRouteRule) return ok }) @@ -163,7 +165,7 @@ func TestKuadrantMergeBasedOnTopology(t *testing.T) { effectivePoliciesByPath := make(map[string]ColorPolicy) for _, httpRouteRule := range httpRouteRules { - for _, path := range topology.Paths(gateways[0], httpRouteRule) { + for _, path := range targetables.Paths(gateways[0], httpRouteRule) { // Gather all policies in the path sorted from the least specific (gateway) to the most specific (httprouterule) // Since in this example there are no targetables with more than one policy attached to it, we can safely just // flat the slices of policies; otherwise we would need to ensure that the policies at the same level are sorted diff --git a/examples/json_patch/integration_test.go b/examples/json_patch/integration_test.go index 88796d3..e60471e 100644 --- a/examples/json_patch/integration_test.go +++ b/examples/json_patch/integration_test.go @@ -121,11 +121,13 @@ func TestJSONPatchMergeBasedOnTopology(t *testing.T) { machinery.SaveToOutputDir(t, topology.ToDot(), "../../tests/out", ".dot") - gateways := topology.Targetables(func(o machinery.Object) bool { + targetables := topology.Targetables() + + gateways := targetables.Items(func(o machinery.Object) bool { _, ok := o.(*machinery.Gateway) return ok }) - httpRouteRules := topology.Targetables(func(o machinery.Object) bool { + httpRouteRules := targetables.Items(func(o machinery.Object) bool { _, ok := o.(*machinery.HTTPRouteRule) return ok }) @@ -133,7 +135,7 @@ func TestJSONPatchMergeBasedOnTopology(t *testing.T) { effectivePoliciesByPath := make(map[string]ColorPolicy) for _, httpRouteRule := range httpRouteRules { - for _, path := range topology.Paths(gateways[0], httpRouteRule) { + for _, path := range targetables.Paths(gateways[0], httpRouteRule) { // Gather all policies in the path sorted from the least specific (gateway) to the most specific (httprouterule) // Since in this example there are no targetables with more than one policy attached to it, we can safely just // flat the slices of policies; otherwise we would need to ensure that the policies at the same level are sorted diff --git a/examples/kuadrant/Makefile b/examples/kuadrant/Makefile index 5a6d4b1..185d937 100644 --- a/examples/kuadrant/Makefile +++ b/examples/kuadrant/Makefile @@ -49,6 +49,23 @@ $(CONTROLLER_GEN): .PHONY: controller-gen controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. +HELM_VERSION = v3.15.0 +HELM = $(PROJECT_PATH)/bin/helm +$(HELM): + @{ \ + set -e ;\ + mkdir -p $(dir $(HELM)) ;\ + OS=$(shell go env GOOS) && ARCH=$(shell go env GOARCH) && \ + curl -sL -o helm.tar.gz https://get.helm.sh/helm-$(HELM_VERSION)-$${OS}-$${ARCH}.tar.gz ;\ + tar -zxvf helm.tar.gz ;\ + mv $${OS}-$${ARCH}/helm $(HELM) ;\ + chmod +x $(HELM) ;\ + rm -rf $${OS}-$${ARCH} helm.tar.gz ;\ + } + +.PHONY: helm +helm: $(HELM) ## Download helm locally if necessary. + ##@ Development .PHONY: generate @@ -66,4 +83,27 @@ install: manifests ## Install CRDs into a cluster. .PHONY: run run: generate ## Run the controller. +ifneq ($(PROVIDERS),) + go run *.go --gateway-providers $(PROVIDERS) +else go run *.go +endif + +##@ Testing + +.PHONY: install-envoy-gateway +install-envoy-gateway: helm ## Install Envoy Gateway. + $(HELM) install eg oci://docker.io/envoyproxy/gateway-helm --version v1.0.2 -n envoy-gateway-system --create-namespace + kubectl wait --timeout=5m -n envoy-gateway-system deployment/envoy-gateway --for=condition=Available + +.PHONY: install-istio +install-istio: helm ## Install Istio. + $(HELM) repo add istio https://istio-release.storage.googleapis.com/charts + $(HELM) repo update + kubectl create namespace istio-system + $(HELM) install istio-base istio/base -n istio-system --set defaultRevision=default + $(HELM) install istiod istio/istiod -n istio-system --wait + +.PHONY: install-kuadrant +install-kuadrant: ## Install Kuadrant CRDs. + kubectl apply -f config/crds diff --git a/examples/kuadrant/README.md b/examples/kuadrant/README.md index cd53e72..eb22c78 100644 --- a/examples/kuadrant/README.md +++ b/examples/kuadrant/README.md @@ -1,10 +1,10 @@ # Kuadrant Controller -Practical example of using the [Policy Machinery](https://github.com/kuadrant/policy-machinery) to implment a custom controller. +Practical example of using the [Policy Machinery](https://github.com/kuadrant/policy-machinery) to implement a custom controller.
-The examples defines 4 kinds of policies: +The example defines 4 kinds of policies: - **DNSPolicy:** can target Gateways and Listeners - **TLSPolicy:** can target Gateways and Listeners - **AuthPolicy:** can target Gateways, Listeners, HTTPRoutes, and HTTPRouteRules; support for Defaults & Overrides and 2 merge strategies (`atomic` or `merge`) @@ -35,7 +35,7 @@ Install the CRDs: make install ``` -Run the operator (holds the shell): +Run the controller (holds the shell): ```sh make run @@ -43,7 +43,7 @@ make run ### Create the resources -> **Note:** After each step below, check out the state of the topology (`topology.dot`) and the operator logs for the new effective policies in place. +> **Note:** After each step below, check out the state of the topology (`topology.dot`) and the controller logs for the new effective policies in place. 1. Create a Gateway: diff --git a/examples/kuadrant/envoy-gateway.md b/examples/kuadrant/envoy-gateway.md new file mode 100644 index 0000000..be0571e --- /dev/null +++ b/examples/kuadrant/envoy-gateway.md @@ -0,0 +1,270 @@ +# Kuadrant Controller with Envoy Gateway + +The example [custom controller](./README.md) working alongside with [Envoy Gateway](https://gateway.envoyproxy.io/). + +This example demonstrates how a controller can use the topology for reconciling other generic objects as well, along with targetables and policies. + +
+ +The controller watches for events related to: +- the 4 kinds of custom policies: DNSPolicy, TLSPolicy, AuthPolicy, and RateLimitPolicy; +- Gateway API resources: GatewayClass, Gateway, and HTTPRoute; +- Envoy Gateway resources: SecurityPolicy. + +Apart from computing effective policies, the callback reconcile function also manages Envoy Gateway SecurityPolicy custom resources (create/update/delete) (used internally to implement the AuthPolicies.) + +## Demo + +### Requirements + +- [kubectl](https://kubernetes.io/docs/reference/kubectl/introduction/) +- [Kind](https://kind.sigs.k8s.io/) + +### Setup + +Create the cluster: + +```sh +kind create cluster +``` + +Install Envoy Gateway: + +```sh +make install-envoy-gateway +``` + +Install the CRDs: + +```sh +make install-kuadrant +``` + +Run the controller (holds the shell): + +```sh +make run PROVIDERS=envoygateway +``` + +### Create the resources + +> **Note:** After each step below, check out the state of the topology (`topology.dot`). + +1. Create a Gateway managed by the Envoy Gateway gateway controller: + +```sh +kubectl apply -f -<= 8; h <= 17 } + strategy: merge +EOF +``` + +5. Try to delete the Envoy Gateway SecurityPolicy: + +```sh +kubectl delete securitypolicy/prod-web +``` + +6. Create a HTTPRoute-wide AuthPolicy to enforce API key authentication and affiliation to the 'admin' group: + +```sh +kubectl apply -f - < 0 { + p.createAuthorizationPolicy(topology, gateway, paths) + return + } + p.deleteAuthorizationPolicy(topology, gateway) +} + +func (p *IstioGatewayProvider) createAuthorizationPolicy(topology *machinery.Topology, gateway machinery.Targetable, paths [][]machinery.Targetable) { + desiredAuthorizationPolicy := &istiov1.AuthorizationPolicy{ + TypeMeta: metav1.TypeMeta{ + APIVersion: istiov1.SchemeGroupVersion.String(), + Kind: istioAuthorizationPolicyKind.Kind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: gateway.GetName(), + Namespace: gateway.GetNamespace(), + }, + Spec: istioapiv1.AuthorizationPolicy{ + TargetRef: &istiov1beta1.PolicyTargetReference{ + Group: gwapiv1alpha2.GroupName, + Kind: "Gateway", + Name: gateway.GetName(), + }, + Action: istioapiv1.AuthorizationPolicy_CUSTOM, + ActionDetail: &istioapiv1.AuthorizationPolicy_Provider{ + Provider: &istioapiv1.AuthorizationPolicy_ExtensionProvider{ + Name: "kuadrant-external-authorization", + }, + }, + }, + } + + 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) + routeRule := path[3].(*machinery.HTTPRouteRule) + hostname := ptr.Deref(listener.Hostname, gwapiv1.Hostname("*")) + rules := istioAuthorizationPolicyRulesFromHTTPRouteRule(routeRule.HTTPRouteRule, []gwapiv1.Hostname{hostname}) + desiredAuthorizationPolicy.Spec.Rules = append(desiredAuthorizationPolicy.Spec.Rules, rules...) + } + + resource := p.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() + }) + + if !found { + o, _ := controller.Destruct(desiredAuthorizationPolicy) + _, err := resource.Create(context.TODO(), o, metav1.CreateOptions{}) + if err != nil { + log.Println("failed to create AuthorizationPolicy", err) + } + return + } + + authorizationPolicy := obj.(*controller.Object).RuntimeObject.(*istiov1.AuthorizationPolicy) + + if authorizationPolicy.Spec.Action == desiredAuthorizationPolicy.Spec.Action && + authorizationPolicy.Spec.GetProvider() != nil && + authorizationPolicy.Spec.GetProvider().Name == desiredAuthorizationPolicy.Spec.GetProvider().Name && + len(authorizationPolicy.Spec.Rules) == len(desiredAuthorizationPolicy.Spec.Rules) && + lo.Every(authorizationPolicy.Spec.Rules, desiredAuthorizationPolicy.Spec.Rules) { + return + } + + authorizationPolicy.Spec.Action = desiredAuthorizationPolicy.Spec.Action + authorizationPolicy.Spec.ActionDetail = desiredAuthorizationPolicy.Spec.ActionDetail + authorizationPolicy.Spec.Rules = desiredAuthorizationPolicy.Spec.Rules + o, _ := controller.Destruct(authorizationPolicy) + _, err := resource.Update(context.TODO(), 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() + }) + + if !found { + return + } + + resource := p.Resource(istioAuthorizationPoliciesResource).Namespace(gateway.GetNamespace()) + err := resource.Delete(context.TODO(), gateway.GetName(), metav1.DeleteOptions{}) + if err != nil { + log.Println("failed to delete AuthorizationPolicy", err) + } +} + +func istioAuthorizationPolicyRulesFromHTTPRouteRule(rule *gwapiv1.HTTPRouteRule, hostnames []gwapiv1.Hostname) (istioRules []*istioapiv1.Rule) { + hosts := []string{} + for _, hostname := range hostnames { + if hostname == "*" { + continue + } + hosts = append(hosts, string(hostname)) + } + + // no http route matches → we only need one simple istio rule or even no rule at all + if len(rule.Matches) == 0 { + if len(hosts) == 0 { + return + } + istioRule := &istioapiv1.Rule{ + To: []*istioapiv1.Rule_To{ + { + Operation: &istioapiv1.Operation{ + Hosts: hosts, + }, + }, + }, + } + istioRules = append(istioRules, istioRule) + return + } + + // http route matches and possibly hostnames → we need one istio rule per http route match + for _, match := range rule.Matches { + istioRule := &istioapiv1.Rule{} + + var operation *istioapiv1.Operation + method := match.Method + path := match.Path + + if len(hosts) > 0 || method != nil || path != nil { + operation = &istioapiv1.Operation{} + } + + // hosts + if len(hosts) > 0 { + operation.Hosts = hosts + } + + // method + if method != nil { + operation.Methods = []string{string(*method)} + } + + // path + if path != nil { + operator := "*" // gateway api defaults to PathMatchPathPrefix + skip := false + if path.Type != nil { + switch *path.Type { + case gwapiv1.PathMatchExact: + operator = "" + case gwapiv1.PathMatchRegularExpression: + // ignore this rule as it is not supported by Istio - Authorino will check it anyway + skip = true + } + } + if !skip { + value := "/" + if path.Value != nil { + value = *path.Value + } + operation.Paths = []string{fmt.Sprintf("%s%s", value, operator)} + } + } + + if operation != nil { + istioRule.To = []*istioapiv1.Rule_To{ + {Operation: operation}, + } + } + + // headers + if len(match.Headers) > 0 { + istioRule.When = []*istioapiv1.Condition{} + + for idx := range match.Headers { + header := match.Headers[idx] + if header.Type != nil && *header.Type == gwapiv1.HeaderMatchRegularExpression { + // skip this rule as it is not supported by Istio - Authorino will check it anyway + continue + } + headerCondition := &istioapiv1.Condition{ + Key: fmt.Sprintf("request.headers[%s]", header.Name), + Values: []string{header.Value}, + } + istioRule.When = append(istioRule.When, headerCondition) + } + } + + // query params: istio does not support query params in authorization policies, so we build them in the authconfig instead + + istioRules = append(istioRules, istioRule) + } + return +} + +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) + if !ok { + return nil, false + } + return g, true + }) + + return machinery.LinkFunc{ + From: gatewayKind, + To: istioAuthorizationPolicyKind, + Func: func(child machinery.Object) []machinery.Object { + o := child.(*controller.Object) + ap := o.RuntimeObject.(*istiov1.AuthorizationPolicy) + refs := ap.Spec.TargetRefs + if ref := ap.Spec.TargetRef; ref != nil { + refs = append(refs, ref) + } + refs = lo.Filter(refs, func(ref *istiov1beta1.PolicyTargetReference, _ int) bool { + return ref.Group == gwapiv1.GroupName && ref.Kind == gatewayKind.Kind + }) + if len(refs) == 0 { + return nil + } + gateway, ok := lo.Find(gateways, func(g *gwapiv1.Gateway) bool { + if g.GetNamespace() != ap.GetNamespace() { + return false + } + return lo.ContainsBy(refs, func(ref *istiov1beta1.PolicyTargetReference) bool { + return ref.Name == g.GetName() + }) + }) + if ok { + return []machinery.Object{&machinery.Gateway{Gateway: gateway}} + } + return nil + }, + } +} diff --git a/examples/kuadrant/main.go b/examples/kuadrant/main.go index 82c5f1a..9172365 100644 --- a/examples/kuadrant/main.go +++ b/examples/kuadrant/main.go @@ -1,15 +1,13 @@ package main import ( - "encoding/json" - "fmt" "log" "os" - "sort" "strings" - "github.com/google/go-cmp/cmp" + egv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" "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" @@ -17,22 +15,34 @@ import ( gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" "github.com/kuadrant/policy-machinery/controller" - "github.com/kuadrant/policy-machinery/machinery" - kuadrantapis "github.com/kuadrant/policy-machinery/examples/kuadrant/apis" kuadrantv1alpha2 "github.com/kuadrant/policy-machinery/examples/kuadrant/apis/v1alpha2" kuadrantv1beta3 "github.com/kuadrant/policy-machinery/examples/kuadrant/apis/v1beta3" ) -const topologyFile = "topology.dot" - -var _ controller.RuntimeObject = &gwapiv1.Gateway{} -var _ controller.RuntimeObject = &gwapiv1.HTTPRoute{} -var _ controller.RuntimeObject = &kuadrantv1alpha2.DNSPolicy{} -var _ controller.RuntimeObject = &kuadrantv1beta3.AuthPolicy{} -var _ controller.RuntimeObject = &kuadrantv1beta3.RateLimitPolicy{} +var supportedGatewayProviders = []string{envoyGatewayProvider, istioGatewayProvider} func main() { + var gatewayProviders []string + for i := range os.Args { + switch os.Args[i] { + case "--gateway-providers": + { + defer func() { + if recover() != nil { + log.Fatalf("Invalid gateway provider. Supported: %s\n", strings.Join(supportedGatewayProviders, ",")) + } + }() + gatewayProviders = lo.Map(strings.Split(os.Args[i+1], ","), func(gp string, _ int) string { + return strings.TrimSpace(gp) + }) + if !lo.Every(supportedGatewayProviders, gatewayProviders) { + panic("") + } + } + } + } + // load kubeconfig kubeconfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(clientcmd.NewDefaultClientConfigLoadingRules(), &clientcmd.ConfigOverrides{}) config, err := kubeconfig.ClientConfig() @@ -46,7 +56,7 @@ func main() { log.Fatalf("Error creating client: %v", err) } - controller := controller.NewController( + 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)), @@ -60,104 +70,54 @@ func main() { schema.GroupKind{Group: kuadrantv1beta3.SchemeGroupVersion.Group, Kind: "AuthPolicy"}, schema.GroupKind{Group: kuadrantv1beta3.SchemeGroupVersion.Group, Kind: "RateLimitPolicy"}, ), - controller.WithCallback(reconcile), - ) + controller.WithCallback(buildReconciler(gatewayProviders, client).Reconcile), + } + controllerOpts = append(controllerOpts, controllerOptionsFor(gatewayProviders)...) - controller.Start() + controller.NewController(controllerOpts...).Start() } -func 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) - - // reconcile policies - gateways := topology.Targetables(func(o machinery.Object) bool { - _, ok := o.(*machinery.Gateway) - return ok - }) - - listeners := topology.Targetables(func(o machinery.Object) bool { - _, ok := o.(*machinery.Listener) - return ok - }) - - httpRouteRules := topology.Targetables(func(o machinery.Object) bool { - _, ok := o.(*machinery.HTTPRouteRule) - return ok - }) - - for _, gateway := range gateways { - // reconcile Gateway -> Listener policies - for _, listener := range listeners { - paths := topology.Paths(gateway, listener) - for i := range paths { - effectivePolicyForPath[*kuadrantv1alpha2.DNSPolicy](paths[i]) - effectivePolicyForPath[*kuadrantv1alpha2.TLSPolicy](paths[i]) - } - } +func buildReconciler(gatewayProviders []string, client *dynamic.DynamicClient) *Reconciler { + var providers []GatewayProvider - // reconcile Gateway -> HTTPRouteRule policies - for _, httpRouteRule := range httpRouteRules { - paths := topology.Paths(gateway, httpRouteRule) - for i := range paths { - effectivePolicyForPath[*kuadrantv1beta3.AuthPolicy](paths[i]) - effectivePolicyForPath[*kuadrantv1beta3.RateLimitPolicy](paths[i]) - } + for _, gatewayProvider := range gatewayProviders { + switch gatewayProvider { + case envoyGatewayProvider: + providers = append(providers, &EnvoyGatewayProvider{client}) + case istioGatewayProvider: + providers = append(providers, &IstioGatewayProvider{client}) } } -} -func saveTopologyToFile(topology *machinery.Topology) { - file, err := os.Create(topologyFile) - if err != nil { - log.Fatal(err) + if len(providers) == 0 { + providers = append(providers, &DefaultGatewayProvider{}) } - defer file.Close() - _, err = file.Write(topology.ToDot().Bytes()) - if err != nil { - log.Fatal(err) + + return &Reconciler{ + GatewayProviders: providers, } } -func effectivePolicyForPath[T machinery.Policy](path []machinery.Targetable) *T { - // gather all policies in the path sorted from the least specific to the most specific - policies := lo.FlatMap(path, func(targetable machinery.Targetable, _ int) []machinery.Policy { - policies := lo.FilterMap(targetable.Policies(), func(p machinery.Policy, _ int) (kuadrantapis.MergeablePolicy, bool) { - _, ok := p.(T) - mergeablePolicy, mergeable := p.(kuadrantapis.MergeablePolicy) - return mergeablePolicy, mergeable && ok - }) - sort.Sort(kuadrantapis.PolicyByCreationTimestamp(policies)) - return lo.Map(policies, func(p kuadrantapis.MergeablePolicy, _ int) machinery.Policy { return p }) - }) - - pathStr := strings.Join(lo.Map(path, func(t machinery.Targetable, _ int) string { - return fmt.Sprintf("%s::%s/%s", t.GroupVersionKind().Kind, t.GetNamespace(), t.GetName()) - }), " → ") - - if len(policies) == 0 { - log.Printf("No %T for path %s\n", new(T), pathStr) - return nil - } +func controllerOptionsFor(gatewayProviders []string) []controller.ControllerOptionFunc { + var opts []controller.ControllerOptionFunc - // map reduces the policies from most specific to least specific, merging them into one effective policy - effectivePolicy := lo.ReduceRight(policies, func(effectivePolicy machinery.Policy, policy machinery.Policy, _ int) machinery.Policy { - return effectivePolicy.Merge(policy) - }, policies[len(policies)-1]) + // 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))) + } - jsonEffectivePolicy, _ := json.MarshalIndent(effectivePolicy, "", " ") - log.Printf("Effective %T for path %s:\n%s\n", new(T), pathStr, jsonEffectivePolicy) + 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)) + } + } - concreteEffectivePolicy, _ := effectivePolicy.(T) - return &concreteEffectivePolicy + return opts } diff --git a/examples/kuadrant/multiple-gateway-providers.md b/examples/kuadrant/multiple-gateway-providers.md new file mode 100644 index 0000000..e312500 --- /dev/null +++ b/examples/kuadrant/multiple-gateway-providers.md @@ -0,0 +1,266 @@ +# Kuadrant Controller with multiple gateway providers + +The example [custom controller](./README.md) working alongside with [Envoy Gateway](https://gateway.envoyproxy.io/) and [Istio](https://istio.io) gateway controllers. + +This example demonstrates how a controller can use the topology for reconciling other generic objects as well, along with targetables and policies. + +
+ +The controller watches for events related to: +- the 4 kinds of custom policies: DNSPolicy, TLSPolicy, AuthPolicy, and RateLimitPolicy; +- Gateway API resources: GatewayClass, Gateway, and HTTPRoute; +- Envoy Gateway resources: SecurityPolicy. +- Istio resources: AuthorizationPolicy. + +Apart from computing effective policies, the callback reconcile function also manages Envoy Gateway SecurityPolicy and Istio AuthorizationPolicy custom resources (create/update/delete) (used internally to implement the AuthPolicies.) + +## Demo + +### Requirements + +- [kubectl](https://kubernetes.io/docs/reference/kubectl/introduction/) +- [Kind](https://kind.sigs.k8s.io/) + +### Setup + +Create the cluster: + +```sh +kind create cluster +``` + +Install Envoy Gateway (installs Gateway API CRDs as well): + +```sh +make install-envoy-gateway +``` + +Install Istio: + +```sh +make install-istio +``` + +Install the CRDs: + +```sh +make install-kuadrant +``` + +Run the controller (holds the shell): + +```sh +make run PROVIDERS=envoygateway,istio +``` + +### Create the resources + +> **Note:** After each step below, check out the state of the topology (`topology.dot`). + +1. Create a Gateway managed by the Envoy Gateway gateway controller: + +```sh +kubectl apply -f -<= 8; h <= 17 } + strategy: merge +EOF +``` + +5. Try to delete the Envoy Gateway SecurityPolicy: + +```sh +kubectl delete securitypolicy/eg-gateway +``` + +6. Create a HTTPRoute-wide AuthPolicy to enforce API key authentication and affiliation to the 'admin' group: + +```sh +kubectl apply -f - < Listener policies + for _, listener := range listeners { + paths := targetables.Paths(gateway, listener) + for i := range paths { + if p := effectivePolicyForPath[*kuadrantv1alpha2.DNSPolicy](paths[i]); p != nil { + // TODO: reconcile dns effective policy (i.e. create the DNSRecords for it) + } + if p := effectivePolicyForPath[*kuadrantv1alpha2.TLSPolicy](paths[i]); p != nil { + // TODO: reconcile tls effective policy (i.e. create the certificate request for it) + } + } + } + + // reconcile Gateway -> HTTPRouteRule policies + for _, httpRouteRule := range httpRouteRules { + 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]) + // 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) + } +} + +func effectivePolicyForPath[T machinery.Policy](path []machinery.Targetable) *T { + // gather all policies in the path sorted from the least specific to the most specific + policies := lo.FlatMap(path, func(targetable machinery.Targetable, _ int) []machinery.Policy { + policies := lo.FilterMap(targetable.Policies(), func(p machinery.Policy, _ int) (kuadrantapis.MergeablePolicy, bool) { + _, ok := p.(T) + mergeablePolicy, mergeable := p.(kuadrantapis.MergeablePolicy) + return mergeablePolicy, mergeable && ok + }) + sort.Sort(kuadrantapis.PolicyByCreationTimestamp(policies)) + return lo.Map(policies, func(p kuadrantapis.MergeablePolicy, _ int) machinery.Policy { return p }) + }) + + pathStr := strings.Join(lo.Map(path, func(t machinery.Targetable, _ int) string { + return fmt.Sprintf("%s::%s/%s", t.GroupVersionKind().Kind, t.GetNamespace(), t.GetName()) + }), " → ") + + if len(policies) == 0 { + log.Printf("No %T for path %s\n", new(T), pathStr) + return nil + } + + // map reduces the policies from most specific to least specific, merging them into one effective policy + effectivePolicy := lo.ReduceRight(policies, func(effectivePolicy machinery.Policy, policy machinery.Policy, _ int) machinery.Policy { + return effectivePolicy.Merge(policy) + }, policies[len(policies)-1]) + + jsonEffectivePolicy, _ := json.MarshalIndent(effectivePolicy, "", " ") + log.Printf("Effective %T for path %s:\n%s\n", new(T), pathStr, jsonEffectivePolicy) + + concreteEffectivePolicy, _ := effectivePolicy.(T) + return &concreteEffectivePolicy +} + +var _ GatewayProvider = &DefaultGatewayProvider{} + +type DefaultGatewayProvider struct{} + +func (p *DefaultGatewayProvider) ReconcileGateway(_ *machinery.Topology, _ machinery.Targetable, _ map[string][][]machinery.Targetable) { +} diff --git a/go.mod b/go.mod index c0e86ad..9e9bd9a 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,17 @@ module github.com/kuadrant/policy-machinery -go 1.22.2 +go 1.22.5 require ( github.com/cert-manager/cert-manager v1.15.1 + github.com/envoyproxy/gateway v0.5.0-rc.1.0.20240712105350-9e155c2f4ee2 github.com/evanphx/json-patch v5.9.0+incompatible github.com/goccy/go-graphviz v0.1.3 github.com/google/go-cmp v0.6.0 github.com/kuadrant/authorino v0.17.2 github.com/samber/lo v1.39.0 + istio.io/api v1.22.3 + istio.io/client-go v1.22.3 k8s.io/api v0.30.2 k8s.io/apimachinery v0.30.2 k8s.io/client-go v0.30.2 @@ -20,11 +23,11 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.0 // indirect + github.com/emicklei/go-restful/v3 v3.12.1 // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/fogleman/gg v1.3.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect @@ -35,7 +38,7 @@ require ( github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/imdario/mergo v0.3.16 // indirect + github.com/imdario/mergo v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect @@ -43,31 +46,32 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_golang v1.18.0 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.46.0 // indirect - github.com/prometheus/procfs v0.15.0 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tidwall/gjson v1.14.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect - golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/image v0.14.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/oauth2 v0.20.0 // indirect - golang.org/x/sys v0.21.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sys v0.22.0 // indirect golang.org/x/term v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/protobuf v1.34.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.30.1 // indirect + k8s.io/apiextensions-apiserver v0.30.2 // indirect k8s.io/klog/v2 v2.120.1 // indirect - k8s.io/kube-openapi v0.0.0-20240430033511-f0e62f92d13f // indirect - sigs.k8s.io/controller-runtime v0.18.2 // indirect + k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a // indirect + sigs.k8s.io/controller-runtime v0.18.4 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/go.sum b/go.sum index 52da611..e12d3b6 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,10 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk= -github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= +github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/gateway v0.5.0-rc.1.0.20240712105350-9e155c2f4ee2 h1:xZ+U+q15wMil+JJ83iX/vrHg2e/LdgX7kpeR76P7lUo= +github.com/envoyproxy/gateway v0.5.0-rc.1.0.20240712105350-9e155c2f4ee2/go.mod h1:zEygKCyX8U5OVO5A5vWg6Hy4u2gsFW5ymg67/ELWk9Y= github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= @@ -22,8 +24,8 @@ github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= @@ -89,14 +91,14 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= -github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= -github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= -github.com/prometheus/procfs v0.15.0 h1:A82kmvXJq2jTu5YUhSGNlYoxh85zLnKgPz4bMZgI5Ek= -github.com/prometheus/procfs v0.15.0/go.mod h1:Y0RJ/Y5g5wJpkTisOtqwDSo4HwhGmLB4VQSw2sQJLHk= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= @@ -124,8 +126,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -136,16 +138,16 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= -golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -158,16 +160,18 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= +google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -180,22 +184,26 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +istio.io/api v1.22.3 h1:V59wgcCm2fK2r137QBsddCDHNg0efg/DauIWEB9DFz8= +istio.io/api v1.22.3/go.mod h1:S3l8LWqNYS9yT+d4bH+jqzH2lMencPkW7SKM1Cu9EyM= +istio.io/client-go v1.22.3 h1:4WocGQYVTASpfn7tj1yGE8f0sgxzbxOkg56HX1LJQ5U= +istio.io/client-go v1.22.3/go.mod h1:D/vNne1n5586423NgGXMnPgshE/99mQgnjnxK/Vw2yM= k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI= k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI= -k8s.io/apiextensions-apiserver v0.30.1 h1:4fAJZ9985BmpJG6PkoxVRpXv9vmPUOVzl614xarePws= -k8s.io/apiextensions-apiserver v0.30.1/go.mod h1:R4GuSrlhgq43oRY9sF2IToFh7PVlF1JjfWdoG3pixk4= +k8s.io/apiextensions-apiserver v0.30.2 h1:l7Eue2t6QiLHErfn2vwK4KgF4NeDgjQkCXtEbOocKIE= +k8s.io/apiextensions-apiserver v0.30.2/go.mod h1:lsJFLYyK40iguuinsb3nt+Sj6CmodSI4ACDLep1rgjw= k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg= k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= k8s.io/client-go v0.30.2 h1:sBIVJdojUNPDU/jObC+18tXWcTJVcwyqS9diGdWHk50= k8s.io/client-go v0.30.2/go.mod h1:JglKSWULm9xlJLx4KCkfLLQ7XwtlbflV6uFFSHTMgVs= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240430033511-f0e62f92d13f h1:0LQagt0gDpKqvIkAMPaRGcXawNMouPECM1+F9BVxEaM= -k8s.io/kube-openapi v0.0.0-20240430033511-f0e62f92d13f/go.mod h1:S9tOR0FxgyusSNR+MboCuiDpVWkAifZvaYI1Q2ubgro= +k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a h1:zD1uj3Jf+mD4zmA7W+goE5TxDkI7OGJjBNBzq5fJtLA= +k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a/go.mod h1:UxDHUPsUwTOOxSU+oXURfFBcAS6JwiRXTYqYwfuGowc= k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 h1:jgGTlFYnhF1PM1Ax/lAlxUPE+KfCIXHaathvJg1C3ak= k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.18.2 h1:RqVW6Kpeaji67CY5nPEfRz6ZfFMk0lWQlNrLqlNpx+Q= -sigs.k8s.io/controller-runtime v0.18.2/go.mod h1:tuAt1+wbVsXIT8lPtk5RURxqAnq7xkpv2Mhttslg7Hw= +sigs.k8s.io/controller-runtime v0.18.4 h1:87+guW1zhvuPLh1PHybKdYFLU0YJp4FhJRmiHvm5BZw= +sigs.k8s.io/controller-runtime v0.18.4/go.mod h1:TVoGrfdpbA9VRFaRnKgk9P5/atA0pMwq+f+msb9M8Sg= sigs.k8s.io/gateway-api v1.1.0 h1:DsLDXCi6jR+Xz8/xd0Z1PYl2Pn0TyaFMOPPZIj4inDM= sigs.k8s.io/gateway-api v1.1.0/go.mod h1:ZH4lHrL2sDi0FHZ9jjneb8kKnGzFWyrTya35sWUTrRs= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= diff --git a/machinery/gateway_api_topology.go b/machinery/gateway_api_topology.go index 97fa925..01a6cdb 100644 --- a/machinery/gateway_api_topology.go +++ b/machinery/gateway_api_topology.go @@ -16,6 +16,8 @@ type GatewayAPITopologyOptions struct { HTTPRoutes []*HTTPRoute Services []*Service Policies []Policy + Objects []Object + Links []LinkFunc ExpandGatewayListeners bool ExpandHTTPRouteRules bool @@ -67,6 +69,22 @@ func WithGatewayAPITopologyPolicies(policies ...Policy) GatewayAPITopologyOption } } +// WithGatewayAPITopologyObjects adds objects to the options to initialize a new Gateway API topology. +// Do not use this function to add targetables or policies. +// Use WithGatewayAPITopologyLinks to define the relationships between objects of any kind. +func WithGatewayAPITopologyObjects(objects ...Object) GatewayAPITopologyOptionsFunc { + return func(o *GatewayAPITopologyOptions) { + o.Objects = append(o.Objects, objects...) + } +} + +// WithLinks adds link functions to the options to initialize a new Gateway API topology. +func WithGatewayAPITopologyLinks(links ...LinkFunc) GatewayAPITopologyOptionsFunc { + return func(o *GatewayAPITopologyOptions) { + o.Links = append(o.Links, links...) + } +} + // ExpandGatewayListeners adds targetable gateway listeners to the options to initialize a new Gateway API topology. func ExpandGatewayListeners() GatewayAPITopologyOptionsFunc { return func(o *GatewayAPITopologyOptions) { @@ -105,11 +123,13 @@ func NewGatewayAPITopology(options ...GatewayAPITopologyOptionsFunc) *Topology { } opts := []TopologyOptionsFunc{ + WithObjects(o.Objects...), WithPolicies(o.Policies...), WithTargetables(o.GatewayClasses...), WithTargetables(o.Gateways...), WithTargetables(o.HTTPRoutes...), WithTargetables(o.Services...), + WithLinks(o.Links...), WithLinks(LinkGatewayClassToGatewayFunc(o.GatewayClasses)), // GatewayClass -> Gateway } @@ -194,13 +214,13 @@ func LinkGatewayClassToGatewayFunc(gatewayClasses []*GatewayClass) LinkFunc { return LinkFunc{ From: schema.GroupKind{Group: gwapiv1.GroupVersion.Group, Kind: "GatewayClass"}, To: schema.GroupKind{Group: gwapiv1.GroupVersion.Group, Kind: "Gateway"}, - Func: func(child Targetable) []Targetable { + Func: func(child Object) []Object { gateway := child.(*Gateway) gatewayClass, ok := lo.Find(gatewayClasses, func(gc *GatewayClass) bool { return gc.Name == string(gateway.Spec.GatewayClassName) }) if ok { - return []Targetable{gatewayClass} + return []Object{gatewayClass} } return nil }, @@ -213,9 +233,9 @@ func LinkGatewayToHTTPRouteFunc(gateways []*Gateway) LinkFunc { return LinkFunc{ From: schema.GroupKind{Group: gwapiv1.GroupVersion.Group, Kind: "Gateway"}, To: schema.GroupKind{Group: gwapiv1.GroupVersion.Group, Kind: "HTTPRoute"}, - Func: func(child Targetable) []Targetable { + Func: func(child Object) []Object { httpRoute := child.(*HTTPRoute) - return lo.FilterMap(httpRoute.Spec.ParentRefs, func(parentRef gwapiv1.ParentReference, _ int) (Targetable, bool) { + return lo.FilterMap(httpRoute.Spec.ParentRefs, func(parentRef gwapiv1.ParentReference, _ int) (Object, bool) { parentRefGroup := ptr.Deref(parentRef.Group, gwapiv1.Group(gwapiv1.GroupName)) parentRefKind := ptr.Deref(parentRef.Kind, gwapiv1.Kind("Gateway")) if parentRefGroup != gwapiv1.GroupName || parentRefKind != "Gateway" { @@ -236,9 +256,9 @@ func LinkGatewayToListenerFunc() LinkFunc { return LinkFunc{ From: schema.GroupKind{Group: gwapiv1.GroupVersion.Group, Kind: "Gateway"}, To: schema.GroupKind{Group: gwapiv1.GroupVersion.Group, Kind: "Listener"}, - Func: func(child Targetable) []Targetable { + Func: func(child Object) []Object { listener := child.(*Listener) - return []Targetable{listener.Gateway} + return []Object{listener.Gateway} }, } } @@ -251,9 +271,9 @@ func LinkListenerToHTTPRouteFunc(gateways []*Gateway, listeners []*Listener) Lin return LinkFunc{ From: schema.GroupKind{Group: gwapiv1.GroupVersion.Group, Kind: "Listener"}, To: schema.GroupKind{Group: gwapiv1.GroupVersion.Group, Kind: "HTTPRoute"}, - Func: func(child Targetable) []Targetable { + Func: func(child Object) []Object { httpRoute := child.(*HTTPRoute) - return lo.FlatMap(httpRoute.Spec.ParentRefs, func(parentRef gwapiv1.ParentReference, _ int) []Targetable { + return lo.FlatMap(httpRoute.Spec.ParentRefs, func(parentRef gwapiv1.ParentReference, _ int) []Object { parentRefGroup := ptr.Deref(parentRef.Group, gwapiv1.Group(gwapiv1.GroupName)) parentRefKind := ptr.Deref(parentRef.Kind, gwapiv1.Kind("Gateway")) if parentRefGroup != gwapiv1.GroupName || parentRefKind != "Gateway" { @@ -273,9 +293,9 @@ func LinkListenerToHTTPRouteFunc(gateways []*Gateway, listeners []*Listener) Lin if !ok { return nil } - return []Targetable{listener} + return []Object{listener} } - return lo.FilterMap(listeners, func(l *Listener, _ int) (Targetable, bool) { + return lo.FilterMap(listeners, func(l *Listener, _ int) (Object, bool) { return l, l.Gateway.GetURL() == gateway.GetURL() }) }) @@ -289,9 +309,9 @@ func LinkHTTPRouteToHTTPRouteRuleFunc() LinkFunc { return LinkFunc{ From: schema.GroupKind{Group: gwapiv1.GroupVersion.Group, Kind: "HTTPRoute"}, To: schema.GroupKind{Group: gwapiv1.GroupVersion.Group, Kind: "HTTPRouteRule"}, - Func: func(child Targetable) []Targetable { + Func: func(child Object) []Object { httpRouteRule := child.(*HTTPRouteRule) - return []Targetable{httpRouteRule.HTTPRoute} + return []Object{httpRouteRule.HTTPRoute} }, } } @@ -303,9 +323,9 @@ func LinkHTTPRouteToServiceFunc(httpRoutes []*HTTPRoute, strict bool) LinkFunc { return LinkFunc{ From: schema.GroupKind{Group: gwapiv1.GroupVersion.Group, Kind: "HTTPRoute"}, To: schema.GroupKind{Kind: "Service"}, - Func: func(child Targetable) []Targetable { + Func: func(child Object) []Object { service := child.(*Service) - return lo.FilterMap(httpRoutes, func(httpRoute *HTTPRoute, _ int) (Targetable, bool) { + return lo.FilterMap(httpRoutes, func(httpRoute *HTTPRoute, _ int) (Object, bool) { return httpRoute, lo.ContainsBy(httpRoute.Spec.Rules, func(rule gwapiv1.HTTPRouteRule) bool { backendRefs := lo.FilterMap(rule.BackendRefs, func(backendRef gwapiv1.HTTPBackendRef, _ int) (gwapiv1.BackendRef, bool) { return backendRef.BackendRef, !strict || backendRef.Port == nil @@ -324,9 +344,9 @@ func LinkHTTPRouteToServicePortFunc(httpRoutes []*HTTPRoute) LinkFunc { return LinkFunc{ From: schema.GroupKind{Group: gwapiv1.GroupVersion.Group, Kind: "HTTPRoute"}, To: schema.GroupKind{Kind: "ServicePort"}, - Func: func(child Targetable) []Targetable { + Func: func(child Object) []Object { servicePort := child.(*ServicePort) - return lo.FilterMap(httpRoutes, func(httpRoute *HTTPRoute, _ int) (Targetable, bool) { + return lo.FilterMap(httpRoutes, func(httpRoute *HTTPRoute, _ int) (Object, bool) { return httpRoute, lo.ContainsBy(httpRoute.Spec.Rules, func(rule gwapiv1.HTTPRouteRule) bool { backendRefs := lo.FilterMap(rule.BackendRefs, func(backendRef gwapiv1.HTTPBackendRef, _ int) (gwapiv1.BackendRef, bool) { return backendRef.BackendRef, backendRef.Port != nil && int32(*backendRef.Port) == servicePort.Port @@ -345,9 +365,9 @@ func LinkHTTPRouteRuleToServiceFunc(httpRouteRules []*HTTPRouteRule, strict bool return LinkFunc{ From: schema.GroupKind{Group: gwapiv1.GroupVersion.Group, Kind: "HTTPRouteRule"}, To: schema.GroupKind{Kind: "Service"}, - Func: func(child Targetable) []Targetable { + Func: func(child Object) []Object { service := child.(*Service) - return lo.FilterMap(httpRouteRules, func(httpRouteRule *HTTPRouteRule, _ int) (Targetable, bool) { + return lo.FilterMap(httpRouteRules, func(httpRouteRule *HTTPRouteRule, _ int) (Object, bool) { backendRefs := lo.FilterMap(httpRouteRule.BackendRefs, func(backendRef gwapiv1.HTTPBackendRef, _ int) (gwapiv1.BackendRef, bool) { return backendRef.BackendRef, !strict || backendRef.Port == nil }) @@ -364,9 +384,9 @@ func LinkHTTPRouteRuleToServicePortFunc(httpRouteRules []*HTTPRouteRule) LinkFun return LinkFunc{ From: schema.GroupKind{Group: gwapiv1.GroupVersion.Group, Kind: "HTTPRouteRule"}, To: schema.GroupKind{Kind: "ServicePort"}, - Func: func(child Targetable) []Targetable { + Func: func(child Object) []Object { servicePort := child.(*ServicePort) - return lo.FilterMap(httpRouteRules, func(httpRouteRule *HTTPRouteRule, _ int) (Targetable, bool) { + return lo.FilterMap(httpRouteRules, func(httpRouteRule *HTTPRouteRule, _ int) (Object, bool) { backendRefs := lo.FilterMap(httpRouteRule.BackendRefs, func(backendRef gwapiv1.HTTPBackendRef, _ int) (gwapiv1.BackendRef, bool) { return backendRef.BackendRef, backendRef.Port != nil && int32(*backendRef.Port) == servicePort.Port }) @@ -382,9 +402,9 @@ func LinkServiceToServicePortFunc() LinkFunc { return LinkFunc{ From: schema.GroupKind{Kind: "Service"}, To: schema.GroupKind{Kind: "ServicePort"}, - Func: func(child Targetable) []Targetable { + Func: func(child Object) []Object { servicePort := child.(*ServicePort) - return []Targetable{servicePort.Service} + return []Object{servicePort.Service} }, } } diff --git a/machinery/gateway_api_topology_test.go b/machinery/gateway_api_topology_test.go index 10eafc7..d8240a2 100644 --- a/machinery/gateway_api_topology_test.go +++ b/machinery/gateway_api_topology_test.go @@ -95,8 +95,8 @@ func TestGatewayAPITopology(t *testing.T) { ) links := make(map[string][]string) - for _, root := range topology.Roots() { - linksFromNode(topology, root, links) + for _, root := range topology.Targetables().Roots() { + linksFromTargetable(topology, root, links) } for from, tos := range links { expectedTos := tc.expectedLinks[from] @@ -245,8 +245,8 @@ func TestGatewayAPITopologyWithSectionNames(t *testing.T) { ) links := make(map[string][]string) - for _, root := range topology.Roots() { - linksFromNode(topology, root, links) + for _, root := range topology.Targetables().Roots() { + linksFromTargetable(topology, root, links) } for from, tos := range links { expectedTos := tc.expectedLinks[from] diff --git a/machinery/test_helper.go b/machinery/test_helper.go index 4792228..046b72d 100644 --- a/machinery/test_helper.go +++ b/machinery/test_helper.go @@ -28,14 +28,14 @@ func SaveToOutputDir(t *testing.T, out *bytes.Buffer, outDir, ext string) { } } -func linksFromNode(topology *Topology, node Targetable, edges map[string][]string) { - if _, ok := edges[node.GetName()]; ok { +func linksFromTargetable(topology *Topology, targetable Targetable, edges map[string][]string) { + if _, ok := edges[targetable.GetName()]; ok { return } - children := topology.Children(node) - edges[node.GetName()] = lo.Map(children, func(child Targetable, _ int) string { return child.GetName() }) + children := topology.Targetables().Children(targetable) + edges[targetable.GetName()] = lo.Map(children, func(child Targetable, _ int) string { return child.GetName() }) for _, child := range children { - linksFromNode(topology, child, edges) + linksFromTargetable(topology, child, edges) } } @@ -158,9 +158,9 @@ func LinkApplesToOranges(apples []*Apple) LinkFunc { return LinkFunc{ From: schema.GroupKind{Group: TestGroupName, Kind: "Apple"}, To: schema.GroupKind{Group: TestGroupName, Kind: "Orange"}, - Func: func(child Targetable) []Targetable { + Func: func(child Object) []Object { orange := child.(*Orange) - return lo.FilterMap(apples, func(apple *Apple, _ int) (Targetable, bool) { + return lo.FilterMap(apples, func(apple *Apple, _ int) (Object, bool) { return apple, lo.Contains(orange.AppleParents, apple.Name) }) }, @@ -171,15 +171,57 @@ func LinkOrangesToBananas(oranges []*Orange) LinkFunc { return LinkFunc{ From: schema.GroupKind{Group: TestGroupName, Kind: "Orange"}, To: schema.GroupKind{Group: TestGroupName, Kind: "Banana"}, - Func: func(child Targetable) []Targetable { + Func: func(child Object) []Object { banana := child.(*Banana) - return lo.FilterMap(oranges, func(orange *Orange, _ int) (Targetable, bool) { + return lo.FilterMap(oranges, func(orange *Orange, _ int) (Object, bool) { return orange, lo.Contains(orange.ChildBananas, banana.Name) }) }, } } +type Info struct { + Name string + Ref string +} + +var _ Object = &Info{} + +func (i *Info) GroupVersionKind() schema.GroupVersionKind { + return schema.GroupVersionKind{ + Group: TestGroupName, + Version: "v1", + Kind: "Info", + } +} + +func (i *Info) SetGroupVersionKind(schema.GroupVersionKind) {} + +func (i *Info) GetNamespace() string { + return "" +} + +func (i *Info) GetName() string { + return i.Name +} + +func (i *Info) GetURL() string { + return UrlFromObject(i) +} + +func LinkInfoFrom(kind string, objects []Object) LinkFunc { + return LinkFunc{ + From: schema.GroupKind{Group: TestGroupName, Kind: kind}, + To: schema.GroupKind{Group: TestGroupName, Kind: "Info"}, + Func: func(child Object) []Object { + info := child.(*Info) + return lo.Filter(objects, func(obj Object, _ int) bool { + return obj.GetURL() == info.Ref + }) + }, + } +} + type FruitPolicy struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/machinery/topology.go b/machinery/topology.go index de48e49..66cb4fd 100644 --- a/machinery/topology.go +++ b/machinery/topology.go @@ -14,14 +14,15 @@ import ( type TopologyOptions struct { Targetables []Targetable - Links []LinkFunc Policies []Policy + Objects []Object + Links []LinkFunc } type LinkFunc struct { From schema.GroupKind To schema.GroupKind - Func func(child Targetable) (parents []Targetable) + Func func(child Object) (parents []Object) } type TopologyOptionsFunc func(*TopologyOptions) @@ -35,23 +36,37 @@ func WithTargetables[T Targetable](targetables ...T) TopologyOptionsFunc { } } -// WithLinks adds link functions to the options to initialize a new topology. -func WithLinks(links ...LinkFunc) TopologyOptionsFunc { +// WithPolicies adds policies to the options to initialize a new topology. +func WithPolicies[T Policy](policies ...T) TopologyOptionsFunc { return func(o *TopologyOptions) { - o.Links = append(o.Links, links...) + o.Policies = append(o.Policies, lo.Map(policies, func(policy T, _ int) Policy { + return policy + })...) } } -// WithPolicies adds policies to the options to initialize a new topology. -func WithPolicies(policies ...Policy) TopologyOptionsFunc { +// WithObjects adds generic objects to the options to initialize a new topology. +// Do not use this function to add targetables or policies. +// Use WithLinks to define the relationships between objects of any kind. +func WithObjects[T Object](objects ...T) TopologyOptionsFunc { return func(o *TopologyOptions) { - o.Policies = append(o.Policies, policies...) + o.Objects = append(o.Objects, lo.Map(objects, func(object T, _ int) Object { + return object + })...) + } +} + +// WithLinks adds link functions to the options to initialize a new topology. +func WithLinks(links ...LinkFunc) TopologyOptionsFunc { + return func(o *TopologyOptions) { + o.Links = append(o.Links, links...) } } -// NewTopology returns a network of targetable resources and attached policies. +// NewTopology returns a network of targetable resources, attached policies, and other kinds of objects. // The topology is represented as a directed acyclic graph (DAG) with the structure given by link functions. -// The targetables, policies and link functions are provided as options. +// The links between policies to targteables are inferred from the policies' target references. +// The targetables, policies, objects and link functions are provided as options. func NewTopology(options ...TopologyOptionsFunc) *Topology { o := &TopologyOptions{} for _, f := range options { @@ -78,16 +93,20 @@ func NewTopology(options ...TopologyOptionsFunc) *Topology { gz := graphviz.New() graph, _ := gz.Graph(graphviz.StrictDirected) + addObjectsToGraph(graph, o.Objects) addTargetablesToGraph(graph, targetables) + linkables := append(o.Objects, lo.Map(targetables, AsObject[Targetable])...) + linkables = append(linkables, lo.Map(policies, AsObject[Policy])...) + for _, link := range o.Links { - children := lo.Filter(targetables, func(t Targetable, _ int) bool { - return t.GroupVersionKind().GroupKind() == link.To + children := lo.Filter(linkables, func(l Object, _ int) bool { + return l.GroupVersionKind().GroupKind() == link.To }) for _, child := range children { for _, parent := range link.Func(child) { if parent != nil { - addTargetablesEdgeToGraph(graph, fmt.Sprintf("%s -> %s", link.From.Kind, link.To.Kind), parent, child) + addEdgeToGraph(graph, fmt.Sprintf("%s -> %s", link.From.Kind, link.To.Kind), parent, child) } } } @@ -97,6 +116,7 @@ func NewTopology(options ...TopologyOptionsFunc) *Topology { return &Topology{ graph: graph, + objects: lo.SliceToMap(o.Objects, associateURL[Object]), targetables: lo.SliceToMap(targetables, associateURL[Targetable]), policies: lo.SliceToMap(policies, associateURL[Policy]), } @@ -107,29 +127,128 @@ type Topology struct { graph *cgraph.Graph targetables map[string]Targetable policies map[string]Policy + objects map[string]Object } -type FilterFunc func(Object) bool +// Targetables returns all targetable nodes in the topology. +// The list can be filtered by providing one or more filter functions. +func (t *Topology) Targetables() *collection[Targetable] { + return &collection[Targetable]{ + topology: t, + items: t.targetables, + } +} -// Targetables returns all targetable nodes of a given kind in the topology. -func (t *Topology) Targetables(filters ...FilterFunc) []Targetable { - return lo.Filter(lo.Values(t.targetables), func(targetable Targetable, _ int) bool { - o := targetable.(Object) - for _, f := range filters { - if !f(o) { - return false +// Policies returns all policies in the topology. +// The list can be filtered by providing one or more filter functions. +func (t *Topology) Policies() *collection[Policy] { + return &collection[Policy]{ + topology: t, + items: t.policies, + } +} + +// Objects returns all non-targetable, non-policy object nodes in the topology. +// The list can be filtered by providing one or more filter functions. +func (t *Topology) Objects() *collection[Object] { + return &collection[Object]{ + topology: t, + items: t.objects, + } +} + +func (t *Topology) ToDot() *bytes.Buffer { + gz := graphviz.New() + var buf bytes.Buffer + gz.Render(t.graph, "dot", &buf) + return &buf +} + +func addObjectsToGraph[T Object](graph *cgraph.Graph, objects []T) []*cgraph.Node { + return lo.Map(objects, func(object T, _ int) *cgraph.Node { + name := strings.TrimPrefix(namespacedName(object.GetNamespace(), object.GetName()), string(k8stypes.Separator)) + n, _ := graph.CreateNode(string(object.GetURL())) + n.SetLabel(fmt.Sprintf("%s\\n%s", object.GroupVersionKind().Kind, name)) + n.SetShape(cgraph.EllipseShape) + return n + }) +} + +func addTargetablesToGraph[T Targetable](graph *cgraph.Graph, targetables []T) { + for _, node := range addObjectsToGraph(graph, targetables) { + node.SetShape(cgraph.BoxShape) + node.SetStyle(cgraph.FilledNodeStyle) + node.SetFillColor("#e5e5e5") + } +} + +func addPoliciesToGraph[T Policy](graph *cgraph.Graph, policies []T) { + for i, policyNode := range addObjectsToGraph(graph, policies) { + policyNode.SetShape(cgraph.NoteShape) + policyNode.SetStyle(cgraph.DashedNodeStyle) + // Policy -> Target edges + for _, targetRef := range policies[i].GetTargetRefs() { + targetNode, _ := graph.Node(string(targetRef.GetURL())) + if targetNode != nil { + edge, _ := graph.CreateEdge("Policy -> Target", policyNode, targetNode) + edge.SetStyle(cgraph.DashedEdgeStyle) } } - return true - }) + } +} + +func addEdgeToGraph(graph *cgraph.Graph, name string, parent, child Object) { + p, _ := graph.Node(string(parent.GetURL())) + c, _ := graph.Node(string(child.GetURL())) + if p != nil && c != nil { + graph.CreateEdge(name, p, c) + } +} + +func associateURL[T Object](obj T) (string, T) { + return obj.GetURL(), obj +} + +type collection[T Object] struct { + topology *Topology + items map[string]T +} + +type FilterFunc func(Object) bool + +// Targetables returns all targetable nodes in the collection. +// The list can be filtered by providing one or more filter functions. +func (c *collection[T]) Targetables() *collection[Targetable] { + return &collection[Targetable]{ + topology: c.topology, + items: c.topology.targetables, + } } -// Policies returns all policies of a given kind in the topology. -func (t *Topology) Policies(filters ...FilterFunc) []Policy { - return lo.Filter(lo.Values(t.policies), func(policy Policy, _ int) bool { - o := policy.(Object) +// Policies returns all policies in the collection. +// The list can be filtered by providing one or more filter functions. +func (c *collection[T]) Policies() *collection[Policy] { + return &collection[Policy]{ + topology: c.topology, + items: c.topology.policies, + } +} + +// Objects returns all non-targetable, non-policy object nodes in the collection. +// The list can be filtered by providing one or more filter functions. +func (c *collection[T]) Objects() *collection[Object] { + return &collection[Object]{ + topology: c.topology, + items: c.topology.objects, + } +} + +// List returns all items nodes in the collection. +// The list can be filtered by providing one or more filter functions. +func (c *collection[T]) Items(filters ...FilterFunc) []T { + return lo.Filter(lo.Values(c.items), func(item T, _ int) bool { for _, f := range filters { - if !f(o) { + if !f(item) { return false } } @@ -137,130 +256,85 @@ func (t *Topology) Policies(filters ...FilterFunc) []Policy { }) } -// Roots returns all targetables that have no parents in the topology. -func (t *Topology) Roots() []Targetable { - return lo.Filter(lo.Values(t.targetables), func(targetable Targetable, _ int) bool { - return len(t.Parents(targetable)) == 0 +// Roots returns all items that have no parents in the collection. +func (c *collection[T]) Roots() []T { + return lo.Filter(lo.Values(c.items), func(item T, _ int) bool { + return len(c.Parents(item)) == 0 }) } -// Parents returns all parents of a given targetable in the topology. -func (t *Topology) Parents(targetable Targetable) []Targetable { - var parents []Targetable - n, err := t.graph.Node(targetable.GetURL()) +// Parents returns all parents of a given item in the collection. +func (c *collection[T]) Parents(item Object) []T { + var parents []T + n, err := c.topology.graph.Node(item.GetURL()) if err != nil { return nil } - edge := t.graph.FirstIn(n) + edge := c.topology.graph.FirstIn(n) for { if edge == nil { break } - _, ok := t.targetables[edge.Node().Name()] + _, ok := c.items[edge.Node().Name()] if ok { - parents = append(parents, t.targetables[edge.Node().Name()]) + parents = append(parents, c.items[edge.Node().Name()]) } - edge = t.graph.NextIn(edge) + edge = c.topology.graph.NextIn(edge) } return parents } -// Children returns all children of a given targetable in the topology. -func (t *Topology) Children(targetable Targetable) []Targetable { - var children []Targetable - n, err := t.graph.Node(targetable.GetURL()) +// Children returns all children of a given item in the collection. +func (c *collection[T]) Children(item Object) []T { + var children []T + n, err := c.topology.graph.Node(item.GetURL()) if err != nil { return nil } - edge := t.graph.FirstOut(n) + edge := c.topology.graph.FirstOut(n) for { if edge == nil { break } - _, ok := t.targetables[edge.Node().Name()] + _, ok := c.items[edge.Node().Name()] if ok { - children = append(children, t.targetables[edge.Node().Name()]) + children = append(children, c.items[edge.Node().Name()]) } - edge = t.graph.NextOut(edge) + edge = c.topology.graph.NextOut(edge) } return children } -// Paths returns all paths from a source targetable to a destination targetable in the topology. +// Paths returns all paths from a source item to a destination item in the collection. // The order of the elements in the inner slices represents a path from the source to the destination. -func (t *Topology) Paths(from, to Targetable) [][]Targetable { - if from == nil || to == nil { +func (c *collection[T]) Paths(from, to Object) [][]T { + if &from == nil || &to == nil { return nil } - var paths [][]Targetable - var path []Targetable + var paths [][]T + var path []T visited := make(map[string]bool) - t.dfs(from, to, path, &paths, visited) + c.dfs(from, to, path, &paths, visited) return paths } -func (t *Topology) ToDot() *bytes.Buffer { - gz := graphviz.New() - var buf bytes.Buffer - gz.Render(t.graph, "dot", &buf) - return &buf -} - -func (t *Topology) dfs(current, to Targetable, path []Targetable, paths *[][]Targetable, visited map[string]bool) { +// dfs performs a depth-first search to find all paths from a source item to a destination item in the collection. +func (c *collection[T]) dfs(current, to Object, path []T, paths *[][]T, visited map[string]bool) { currentURL := current.GetURL() if visited[currentURL] { return } - path = append(path, t.targetables[currentURL]) + path = append(path, c.items[currentURL]) visited[currentURL] = true if currentURL == to.GetURL() { - pathCopy := make([]Targetable, len(path)) + pathCopy := make([]T, len(path)) copy(pathCopy, path) *paths = append(*paths, pathCopy) } else { - for _, child := range t.Children(current) { - t.dfs(child, to, path, paths, visited) + for _, child := range c.Children(current) { + c.dfs(child, to, path, paths, visited) } } path = path[:len(path)-1] visited[currentURL] = false } - -func associateURL[T Object](obj T) (string, T) { - return obj.GetURL(), obj -} - -func addObjectsToGraph[T Object](graph *cgraph.Graph, objects []T, shape cgraph.Shape) []*cgraph.Node { - return lo.Map(objects, func(object T, _ int) *cgraph.Node { - name := strings.TrimPrefix(namespacedName(object.GetNamespace(), object.GetName()), string(k8stypes.Separator)) - n, _ := graph.CreateNode(string(object.GetURL())) - n.SetLabel(fmt.Sprintf("%s\\n%s", object.GroupVersionKind().Kind, name)) - n.SetShape(shape) - return n - }) -} - -func addTargetablesToGraph[T Targetable](graph *cgraph.Graph, targetables []T) { - addObjectsToGraph(graph, targetables, cgraph.BoxShape) -} - -func addPoliciesToGraph[T Policy](graph *cgraph.Graph, policies []T) { - for i, policyNode := range addObjectsToGraph(graph, policies, cgraph.EllipseShape) { - // Policy -> Target edges - for _, targetRef := range policies[i].GetTargetRefs() { - targetNode, _ := graph.Node(string(targetRef.GetURL())) - if targetNode != nil { - edge, _ := graph.CreateEdge("Policy -> Target", policyNode, targetNode) - edge.SetStyle(cgraph.DashedEdgeStyle) - } - } - } -} - -func addTargetablesEdgeToGraph(graph *cgraph.Graph, name string, parent, child Targetable) { - p, _ := graph.Node(string(parent.GetURL())) - c, _ := graph.Node(string(child.GetURL())) - if p != nil && c != nil { - graph.CreateEdge(name, p, c) - } -} diff --git a/machinery/topology_test.go b/machinery/topology_test.go index a95d9cd..bfa37de 100644 --- a/machinery/topology_test.go +++ b/machinery/topology_test.go @@ -38,7 +38,7 @@ func TestTopologyRoots(t *testing.T) { }), ), ) - roots := topology.Roots() + roots := topology.Targetables().Roots() if expected := len(apples); len(roots) != expected { t.Errorf("expected %d roots, got %d", expected, len(roots)) } @@ -71,7 +71,7 @@ func TestTopologyParents(t *testing.T) { ), ) // orange-1 - parents := topology.Parents(orange1) + parents := topology.Targetables().Parents(orange1) if expected := 2; len(parents) != expected { t.Errorf("expected %d parent, got %d", expected, len(parents)) } @@ -83,7 +83,7 @@ func TestTopologyParents(t *testing.T) { t.Errorf("expected parent %s not found", apple2.GetURL()) } // orange-2 - parents = topology.Parents(orange2) + parents = topology.Targetables().Parents(orange2) if expected := 1; len(parents) != expected { t.Errorf("expected %d parent, got %d", expected, len(parents)) } @@ -114,7 +114,7 @@ func TestTopologyChildren(t *testing.T) { ), ) // apple-1 - children := topology.Children(apple1) + children := topology.Targetables().Children(apple1) if expected := 1; len(children) != expected { t.Errorf("expected %d child, got %d", expected, len(children)) } @@ -123,7 +123,7 @@ func TestTopologyChildren(t *testing.T) { t.Errorf("expected child %s not found", orange1.GetURL()) } // apple-2 - children = topology.Children(apple2) + children = topology.Targetables().Children(apple2) if expected := 2; len(children) != expected { t.Errorf("expected %d child, got %d", expected, len(children)) } @@ -203,7 +203,7 @@ func TestTopologyPaths(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - paths := topology.Paths(tc.from, tc.to) + paths := topology.Targetables().Paths(tc.from, tc.to) if len(paths) != len(tc.expectedPaths) { t.Errorf("expected %d paths, got %d", len(tc.expectedPaths), len(paths)) } @@ -297,8 +297,8 @@ func TestFruitTopology(t *testing.T) { ) links := make(map[string][]string) - for _, root := range topology.Roots() { - linksFromNode(topology, root, links) + for _, root := range topology.Targetables().Roots() { + linksFromTargetable(topology, root, links) } for from, tos := range links { expectedTos := tc.expectedLinks[from] @@ -313,3 +313,59 @@ func TestFruitTopology(t *testing.T) { }) } } + +func TestTopologyWithGenericObjects(t *testing.T) { + objects := []*Info{ + {Name: "info-1", Ref: "apple.example.test:apple-1"}, + {Name: "info-2", Ref: "orange.example.test:my-namespace/orange-1"}, + } + apples := []*Apple{{Name: "apple-1"}} + oranges := []*Orange{ + {Name: "orange-1", Namespace: "my-namespace", AppleParents: []string{"apple-1"}}, + {Name: "orange-2", Namespace: "my-namespace", AppleParents: []string{"apple-1"}}, + } + + topology := NewTopology( + WithObjects(objects...), + WithTargetables(apples...), + WithTargetables(oranges...), + WithPolicies( + buildFruitPolicy(func(policy *FruitPolicy) { + policy.Name = "policy-1" + policy.Spec.TargetRef.Kind = "Apple" + policy.Spec.TargetRef.Name = "apple-1" + }), + buildFruitPolicy(func(policy *FruitPolicy) { + policy.Name = "policy-2" + policy.Spec.TargetRef.Kind = "Orange" + policy.Spec.TargetRef.Name = "orange-1" + }), + ), + WithLinks( + LinkApplesToOranges(apples), + LinkInfoFrom("Apple", lo.Map(apples, AsObject[*Apple])), + LinkInfoFrom("Orange", lo.Map(oranges, AsObject[*Orange])), + ), + ) + + expectedLinks := map[string][]string{ + "apple-1": {"orange-1", "orange-2"}, + "info-1": {"apple-1"}, + "info-2": {"orange-1"}, + } + + links := make(map[string][]string) + for _, root := range topology.Targetables().Roots() { + linksFromTargetable(topology, root, links) + } + for from, tos := range links { + expectedTos := expectedLinks[from] + slices.Sort(expectedTos) + slices.Sort(tos) + if !slices.Equal(expectedTos, tos) { + t.Errorf("expected links from %s to be %v, got %v", from, expectedTos, tos) + } + } + + SaveToOutputDir(t, topology.ToDot(), "../tests/out", ".dot") +} diff --git a/machinery/types.go b/machinery/types.go index a8e7570..3deb6f3 100644 --- a/machinery/types.go +++ b/machinery/types.go @@ -23,6 +23,10 @@ func UrlFromObject(obj Object) string { return fmt.Sprintf("%s%s%s", strings.ToLower(obj.GroupVersionKind().GroupKind().String()), string(kindNameURLSeparator), name) } +func AsObject[T Object](t T, _ int) Object { + return t +} + func namespacedName(namespace, name string) string { return k8stypes.NamespacedName{Namespace: namespace, Name: name}.String() }