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()
}