diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 224a6b6..6c1829d 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -41,7 +41,7 @@ jobs: with: go-version: '1.22.x' - name: Test with the Go CLI - run: go test -tags=intetration -v ./... + run: go test -tags=integration -v ./... - name: Upload artifacts uses: actions/upload-artifact@v4 with: diff --git a/examples/kuadrant/topology_test.go b/examples/kuadrant/integration_test.go similarity index 88% rename from examples/kuadrant/topology_test.go rename to examples/kuadrant/integration_test.go index be21ead..41e9311 100644 --- a/examples/kuadrant/topology_test.go +++ b/examples/kuadrant/integration_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/samber/lo" + core "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" @@ -17,7 +18,7 @@ import ( "github.com/kuadrant/policy-machinery/machinery" ) -// TestMergeBasedOnTopology tests ColorPolicy's merge strategies for painting a house, based on network traffic +// TestKuadrantMergeBasedOnTopology tests ColorPolicy's merge strategies for painting a house, based on network traffic // flowing through the following topology of Gateway API resources: // // ┌────────────┐ @@ -42,24 +43,23 @@ import ( // ┌────────────┐ // │ my-service │ // └────────────┘ -func TestMergeBasedOnTopology(t *testing.T) { - gateway := &machinery.Gateway{Gateway: machinery.BuildGateway()} - httpRoutes := []*machinery.HTTPRoute{ - {HTTPRoute: machinery.BuildHTTPRoute(func(r *gwapiv1.HTTPRoute) { +func TestKuadrantMergeBasedOnTopology(t *testing.T) { + gateway := machinery.BuildGateway() + httpRoutes := []*gwapiv1.HTTPRoute{ + machinery.BuildHTTPRoute(func(r *gwapiv1.HTTPRoute) { r.Name = "my-route-1" r.Spec.Rules = append(r.Spec.Rules, gwapiv1.HTTPRouteRule{ BackendRefs: []gwapiv1.HTTPBackendRef{machinery.BuildHTTPBackendRef()}, }) - })}, - {HTTPRoute: machinery.BuildHTTPRoute(func(r *gwapiv1.HTTPRoute) { + }), + machinery.BuildHTTPRoute(func(r *gwapiv1.HTTPRoute) { r.Name = "my-route-2" r.Spec.Rules = append(r.Spec.Rules, gwapiv1.HTTPRouteRule{ BackendRefs: []gwapiv1.HTTPBackendRef{machinery.BuildHTTPBackendRef()}, }) - })}, + }), } - httpRouteRules := lo.FlatMap(httpRoutes, machinery.HTTPRouteRulesFromHTTPRouteFunc) - services := []*machinery.Service{{Service: machinery.BuildService()}} + services := []*core.Service{machinery.BuildService()} policies := []machinery.Policy{ buildPolicy(func(p *ColorPolicy) { // atomic defaults p.Name = "house-colors-gw" @@ -141,23 +141,29 @@ func TestMergeBasedOnTopology(t *testing.T) { }), } - topology := machinery.NewTopology( - machinery.WithTargetables(gateway), - machinery.WithTargetables(httpRoutes...), - machinery.WithTargetables(httpRouteRules...), - machinery.WithTargetables(services...), - machinery.WithLinks( - machinery.LinkGatewayToHTTPRouteFunc([]*machinery.Gateway{gateway}), - machinery.LinkHTTPRouteToHTTPRouteRuleFunc(), - machinery.LinkHTTPRouteRuleToServiceFunc(httpRouteRules, false), - ), - machinery.WithPolicies(policies...), + topology := machinery.NewGatewayAPITopology( + machinery.WithGateways(gateway), + machinery.WithHTTPRoutes(httpRoutes...), + machinery.ExpandHTTPRouteRules(), + machinery.WithServices(services...), + machinery.WithGatewayAPITopologyPolicies(policies...), ) + machinery.SaveToOutputDir(t, topology.ToDot(), "../../tests/out", ".dot") + + gateways := topology.Targetables(func(o machinery.Object) bool { + _, ok := o.(*machinery.Gateway) + return ok + }) + httpRouteRules := topology.Targetables(func(o machinery.Object) bool { + _, ok := o.(*machinery.HTTPRouteRule) + return ok + }) + effectivePoliciesByPath := make(map[string]ColorPolicy) for _, httpRouteRule := range httpRouteRules { - for _, path := range topology.Paths(gateway, httpRouteRule) { + for _, path := range topology.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/machinery/gateway_api_test_helper.go b/machinery/gateway_api_test_helper.go new file mode 100644 index 0000000..adbb44e --- /dev/null +++ b/machinery/gateway_api_test_helper.go @@ -0,0 +1,439 @@ +//go:build unit || integration + +package machinery + +import ( + core "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gwapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +func BuildGatewayClass(f ...func(*gwapiv1.GatewayClass)) *gwapiv1.GatewayClass { + gc := &gwapiv1.GatewayClass{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gwapiv1.GroupVersion.String(), + Kind: "GatewayClass", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-gateway-class", + }, + Spec: gwapiv1.GatewayClassSpec{ + ControllerName: gwapiv1.GatewayController("my-gateway-controller"), + }, + } + for _, fn := range f { + fn(gc) + } + return gc +} + +func BuildGateway(f ...func(*gwapiv1.Gateway)) *gwapiv1.Gateway { + g := &gwapiv1.Gateway{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gwapiv1.GroupVersion.String(), + Kind: "Gateway", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-gateway", + Namespace: "my-namespace", + }, + Spec: gwapiv1.GatewaySpec{ + GatewayClassName: "my-gateway-class", + Listeners: []gwapiv1.Listener{ + { + Name: "my-listener", + Port: 80, + Protocol: "HTTP", + }, + }, + }, + } + for _, fn := range f { + fn(g) + } + return g +} + +func BuildHTTPRoute(f ...func(*gwapiv1.HTTPRoute)) *gwapiv1.HTTPRoute { + r := &gwapiv1.HTTPRoute{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gwapiv1.GroupVersion.String(), + Kind: "HTTPRoute", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-http-route", + Namespace: "my-namespace", + }, + Spec: gwapiv1.HTTPRouteSpec{ + CommonRouteSpec: gwapiv1.CommonRouteSpec{ + ParentRefs: []gwapiv1.ParentReference{ + { + Name: "my-gateway", + }, + }, + }, + Rules: []gwapiv1.HTTPRouteRule{ + { + BackendRefs: []gwapiv1.HTTPBackendRef{BuildHTTPBackendRef()}, + }, + }, + }, + } + for _, fn := range f { + fn(r) + } + return r +} + +func BuildHTTPBackendRef(f ...func(*gwapiv1.BackendObjectReference)) gwapiv1.HTTPBackendRef { + bor := &gwapiv1.BackendObjectReference{ + Name: "my-service", + } + for _, fn := range f { + fn(bor) + } + return gwapiv1.HTTPBackendRef{ + BackendRef: gwapiv1.BackendRef{ + BackendObjectReference: *bor, + }, + } +} + +func BuildService(f ...func(*core.Service)) *core.Service { + s := &core.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: core.SchemeGroupVersion.String(), + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-service", + Namespace: "my-namespace", + }, + Spec: core.ServiceSpec{ + Ports: []core.ServicePort{ + { + Name: "http", + Port: 80, + }, + }, + Selector: map[string]string{ + "app": "my-app", + }, + }, + } + for _, fn := range f { + fn(s) + } + return s +} + +type GatewayAPIResources struct { + GatewayClasses []*gwapiv1.GatewayClass + Gateways []*gwapiv1.Gateway + HTTPRoutes []*gwapiv1.HTTPRoute + Services []*core.Service +} + +// BuildComplexGatewayAPITopology returns a set of Gateway API resources organized : +// +// ┌────────────────┐ ┌────────────────┐ +// │ gatewayclass-1 │ │ gatewayclass-2 │ +// └────────────────┘ └────────────────┘ +// ▲ ▲ +// │ │ +// ┌─────────────────────────┼──────────────────────────┐ ┌────────────┴─────────────┐ +// │ │ │ │ │ +// ┌───────────────┴───────────────┐ ┌───────┴────────┐ ┌───────────────┴───────────────┐ ┌──────────────┴────────────────┐ ┌───────┴────────┐ +// │ gateway-1 │ │ gateway-2 │ │ gateway-3 │ │ gateway-4 │ │ gateway-5 │ +// │ │ │ │ │ │ │ │ │ │ +// │ ┌────────────┐ ┌────────────┐ │ │ ┌────────────┐ │ │ ┌────────────┐ ┌────────────┐ │ │ ┌────────────┐ ┌────────────┐ │ │ ┌────────────┐ │ +// │ │ listener-1 │ │ listener-2 │ │ │ │ listener-1 │ │ │ │ listener-1 │ │ listener-2 │ │ │ │ listener-1 │ │ listener-2 │ │ │ │ listener-1 │ │ +// │ └────────────┘ └────────────┘ │ │ └────────────┘ │ │ └────────────┘ └────────────┘ │ │ └────────────┘ └────────────┘ │ │ └────────────┘ │ +// │ ▲ │ │ ▲ │ │ │ │ │ │ │ +// └────────────────────────┬──────┘ └──────┬─────────┘ └───────────────────────────────┘ └───────────────────────────────┘ └────────────────┘ +// ▲ │ │ ▲ ▲ ▲ ▲ ▲ ▲ +// │ │ │ │ │ │ │ │ │ +// │ └───────┬───────┘ │ │ └────────────┬─────────────┘ │ │ +// │ │ │ │ │ │ │ +// ┌───────────┴───────────┐ ┌──────┴─────┐ ┌─────┴──────┐ ┌───────────┴───────────┐ ┌───────────┴───────────┐ ┌───────────┴───────────┐ ┌─────┴──────┐ +// │ route-1 │ │ route-2 │ │ route-3 │ │ route-4 │ │ route-5 │ │ route-6 │ │ route-7 │ +// │ │ │ │ │ │ │ │ │ │ │ │ │ │ +// │ ┌────────┐ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ ┌────────┐ │ │ ┌────────┐ ┌────────┐ │ │ ┌────────┐ ┌────────┐ │ │ ┌────────┐ │ +// │ │ rule-1 │ │ rule-2 │ │ │ │ rule-1 │ │ │ │ rule-1 │ │ │ │ rule-1 │ │ rule-2 │ │ │ │ rule-1 │ │ rule-2 │ │ │ │ rule-1 │ │ rule-2 │ │ │ │ rule-1 │ │ +// │ └────┬───┘ └────┬───┘ │ │ └────┬───┘ │ │ └───┬────┘ │ │ └─┬──────┘ └───┬────┘ │ │ └───┬────┘ └────┬───┘ │ │ └─┬────┬─┘ └────┬───┘ │ │ └────┬───┘ │ +// │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ +// └──────┼──────────┼─────┘ └──────┼─────┘ └─────┼──────┘ └───┼────────────┼──────┘ └─────┼───────────┼─────┘ └───┼────┼────────┼─────┘ └──────┼─────┘ +// │ │ │ │ │ │ │ │ │ │ │ │ +// │ │ └─────────────┤ │ │ └───────────┴───────────┘ │ │ │ +// ▼ ▼ │ │ │ ▼ ▼ │ ▼ +// ┌───────────────────────┐ ┌────────────┐ ┌──────┴────────────┴───┐ ┌─────┴──────┐ ┌────────────┐ ┌─────────┴──┐ ┌────────────┐ +// │ │ │ │ │ ▼ ▼ │ │ ▼ │ │ │ │ ▼ │ │ │ +// │ ┌────────┐ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │ +// │ │ port-1 │ │ port-2 │ │ │ │ port-1 │ │ │ │ port-1 │ │ port-2 │ │ │ │ port-1 │ │ │ │ port-1 │ │ │ │ port-1 │ │ │ │ port-1 │ │ +// │ └────────┘ └────────┘ │ │ └────────┘ │ │ └────────┘ └────────┘ │ │ └────────┘ │ │ └────────┘ │ │ └────────┘ │ │ └────────┘ │ +// │ │ │ │ │ │ │ │ │ │ │ │ │ │ +// │ service-1 │ │ service-2 │ │ service-3 │ │ service-4 │ │ service-5 │ │ service-6 │ │ service-7 │ +// └───────────────────────┘ └────────────┘ └───────────────────────┘ └────────────┘ └────────────┘ └────────────┘ └────────────┘ +func BuildComplexGatewayAPITopology(funcs ...func(*GatewayAPIResources)) GatewayAPIResources { + t := GatewayAPIResources{ + GatewayClasses: []*gwapiv1.GatewayClass{ + BuildGatewayClass(func(gc *gwapiv1.GatewayClass) { gc.Name = "gatewayclass-1" }), + BuildGatewayClass(func(gc *gwapiv1.GatewayClass) { gc.Name = "gatewayclass-2" }), + }, + Gateways: []*gwapiv1.Gateway{ + BuildGateway(func(g *gwapiv1.Gateway) { + g.Name = "gateway-1" + g.Spec.GatewayClassName = "gatewayclass-1" + g.Spec.Listeners[0].Name = "listener-1" + g.Spec.Listeners = append(g.Spec.Listeners, gwapiv1.Listener{ + Name: "listener-2", + Port: 443, + Protocol: "HTTPS", + }) + }), + BuildGateway(func(g *gwapiv1.Gateway) { + g.Name = "gateway-2" + g.Spec.GatewayClassName = "gatewayclass-1" + g.Spec.Listeners[0].Name = "listener-1" + }), + BuildGateway(func(g *gwapiv1.Gateway) { + g.Name = "gateway-3" + g.Spec.GatewayClassName = "gatewayclass-1" + g.Spec.Listeners[0].Name = "listener-1" + g.Spec.Listeners = append(g.Spec.Listeners, gwapiv1.Listener{ + Name: "listener-2", + Port: 443, + Protocol: "HTTPS", + }) + }), + BuildGateway(func(g *gwapiv1.Gateway) { + g.Name = "gateway-4" + g.Spec.GatewayClassName = "gatewayclass-2" + g.Spec.Listeners[0].Name = "listener-1" + g.Spec.Listeners = append(g.Spec.Listeners, gwapiv1.Listener{ + Name: "listener-2", + Port: 443, + Protocol: "HTTPS", + }) + }), + BuildGateway(func(g *gwapiv1.Gateway) { + g.Name = "gateway-5" + g.Spec.GatewayClassName = "gatewayclass-2" + g.Spec.Listeners[0].Name = "listener-1" + }), + }, + HTTPRoutes: []*gwapiv1.HTTPRoute{ + BuildHTTPRoute(func(r *gwapiv1.HTTPRoute) { + r.Name = "route-1" + r.Spec.ParentRefs[0].Name = "gateway-1" + r.Spec.Rules = []gwapiv1.HTTPRouteRule{ + { // rule-1 + BackendRefs: []gwapiv1.HTTPBackendRef{BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { + backendRef.Name = "service-1" + })}, + }, + { // rule-2 + BackendRefs: []gwapiv1.HTTPBackendRef{BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { + backendRef.Name = "service-2" + })}, + }, + } + }), + BuildHTTPRoute(func(r *gwapiv1.HTTPRoute) { + r.Name = "route-2" + r.Spec.ParentRefs = []gwapiv1.ParentReference{ + { + Name: "gateway-1", + SectionName: ptr.To(gwapiv1.SectionName("listener-2")), + }, + { + Name: "gateway-2", + SectionName: ptr.To(gwapiv1.SectionName("listener-1")), + }, + } + r.Spec.Rules[0].BackendRefs[0] = BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { + backendRef.Name = "service-3" + backendRef.Port = ptr.To(gwapiv1.PortNumber(80)) // port-1 + }) + }), + BuildHTTPRoute(func(r *gwapiv1.HTTPRoute) { + r.Name = "route-3" + r.Spec.ParentRefs[0].Name = "gateway-2" + r.Spec.Rules[0].BackendRefs[0] = BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { + backendRef.Name = "service-3" + backendRef.Port = ptr.To(gwapiv1.PortNumber(80)) // port-1 + }) + }), + BuildHTTPRoute(func(r *gwapiv1.HTTPRoute) { + r.Name = "route-4" + r.Spec.ParentRefs[0].Name = "gateway-3" + r.Spec.Rules = []gwapiv1.HTTPRouteRule{ + { // rule-1 + BackendRefs: []gwapiv1.HTTPBackendRef{BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { + backendRef.Name = "service-3" + backendRef.Port = ptr.To(gwapiv1.PortNumber(443)) // port-2 + })}, + }, + { // rule-2 + BackendRefs: []gwapiv1.HTTPBackendRef{BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { + backendRef.Name = "service-4" + backendRef.Port = ptr.To(gwapiv1.PortNumber(80)) // port-1 + })}, + }, + } + }), + BuildHTTPRoute(func(r *gwapiv1.HTTPRoute) { + r.Name = "route-5" + r.Spec.ParentRefs[0].Name = "gateway-3" + r.Spec.ParentRefs = append(r.Spec.ParentRefs, gwapiv1.ParentReference{Name: "gateway-4"}) + r.Spec.Rules = []gwapiv1.HTTPRouteRule{ + { // rule-1 + BackendRefs: []gwapiv1.HTTPBackendRef{BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { + backendRef.Name = "service-5" + })}, + }, + { // rule-2 + BackendRefs: []gwapiv1.HTTPBackendRef{BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { + backendRef.Name = "service-5" + })}, + }, + } + }), + BuildHTTPRoute(func(r *gwapiv1.HTTPRoute) { + r.Name = "route-6" + r.Spec.ParentRefs[0].Name = "gateway-4" + r.Spec.Rules = []gwapiv1.HTTPRouteRule{ + { // rule-1 + BackendRefs: []gwapiv1.HTTPBackendRef{ + BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { + backendRef.Name = "service-5" + }), + BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { + backendRef.Name = "service-6" + }), + }, + }, + { // rule-2 + BackendRefs: []gwapiv1.HTTPBackendRef{BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { + backendRef.Name = "service-6" + backendRef.Port = ptr.To(gwapiv1.PortNumber(80)) // port-1 + })}, + }, + } + }), + BuildHTTPRoute(func(r *gwapiv1.HTTPRoute) { + r.Name = "route-7" + r.Spec.ParentRefs[0].Name = "gateway-5" + r.Spec.Rules[0].BackendRefs[0] = BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { + backendRef.Name = "service-7" + }) + }), + }, + Services: []*core.Service{ + BuildService(func(s *core.Service) { + s.Name = "service-1" + s.Spec.Ports[0].Name = "port-1" + s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{ + Name: "port-2", + Port: 443, + }) + }), + BuildService(func(s *core.Service) { + s.Name = "service-2" + s.Spec.Ports[0].Name = "port-1" + }), + BuildService(func(s *core.Service) { + s.Name = "service-3" + s.Spec.Ports[0].Name = "port-1" + s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{ + Name: "port-2", + Port: 443, + }) + }), + BuildService(func(s *core.Service) { + s.Name = "service-4" + s.Spec.Ports[0].Name = "port-1" + }), + BuildService(func(s *core.Service) { + s.Name = "service-5" + s.Spec.Ports[0].Name = "port-1" + }), + BuildService(func(s *core.Service) { + s.Name = "service-6" + s.Spec.Ports[0].Name = "port-1" + }), + BuildService(func(s *core.Service) { + s.Name = "service-7" + s.Spec.Ports[0].Name = "port-1" + }), + }, + } + for _, f := range funcs { + f(&t) + } + return t +} + +type TestPolicy struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec TestPolicySpec `json:"spec"` +} + +type TestPolicySpec struct { + TargetRef gwapiv1alpha2.LocalPolicyTargetReferenceWithSectionName `json:"targetRef"` +} + +var _ Policy = &TestPolicy{} + +func (p *TestPolicy) GetURL() string { + return UrlFromObject(p) +} + +func (p *TestPolicy) GetTargetRefs() []PolicyTargetReference { + return []PolicyTargetReference{ + LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReferenceWithSectionName: p.Spec.TargetRef, + PolicyNamespace: p.Namespace, + }, + } +} + +func (p *TestPolicy) GetMergeStrategy() MergeStrategy { + return DefaultMergeStrategy +} + +func (p *TestPolicy) Merge(policy Policy) Policy { + return &TestPolicy{ + Spec: p.Spec, + } +} + +func buildPolicy(f ...func(*TestPolicy)) *TestPolicy { + p := &TestPolicy{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "test/v1", + Kind: "TestPolicy", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-policy", + Namespace: "my-namespace", + }, + Spec: TestPolicySpec{ + TargetRef: gwapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1alpha2.LocalPolicyTargetReference{ + Group: gwapiv1.Group(core.SchemeGroupVersion.Group), + Kind: "Service", + Name: "my-service", + }, + }, + }, + } + for _, fn := range f { + fn(p) + } + return p +} diff --git a/machinery/gateway_api.go b/machinery/gateway_api_topology.go similarity index 66% rename from machinery/gateway_api.go rename to machinery/gateway_api_topology.go index 030300d..97fa925 100644 --- a/machinery/gateway_api.go +++ b/machinery/gateway_api_topology.go @@ -10,6 +10,153 @@ import ( gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" ) +type GatewayAPITopologyOptions struct { + GatewayClasses []*GatewayClass + Gateways []*Gateway + HTTPRoutes []*HTTPRoute + Services []*Service + Policies []Policy + + ExpandGatewayListeners bool + ExpandHTTPRouteRules bool + ExpandServicePorts bool +} + +type GatewayAPITopologyOptionsFunc func(*GatewayAPITopologyOptions) + +// WithGatewayClasses adds gateway classes to the options to initialize a new Gateway API topology. +func WithGatewayClasses(gatewayClasses ...*gwapiv1.GatewayClass) GatewayAPITopologyOptionsFunc { + return func(o *GatewayAPITopologyOptions) { + o.GatewayClasses = append(o.GatewayClasses, lo.Map(gatewayClasses, func(gatewayClass *gwapiv1.GatewayClass, _ int) *GatewayClass { + return &GatewayClass{GatewayClass: gatewayClass} + })...) + } +} + +// WithGateways adds gateways to the options to initialize a new Gateway API topology. +func WithGateways(gateways ...*gwapiv1.Gateway) GatewayAPITopologyOptionsFunc { + return func(o *GatewayAPITopologyOptions) { + o.Gateways = append(o.Gateways, lo.Map(gateways, func(gateway *gwapiv1.Gateway, _ int) *Gateway { + return &Gateway{Gateway: gateway} + })...) + } +} + +// WithHTTPRoutes adds HTTP routes to the options to initialize a new Gateway API topology. +func WithHTTPRoutes(httpRoutes ...*gwapiv1.HTTPRoute) GatewayAPITopologyOptionsFunc { + return func(o *GatewayAPITopologyOptions) { + o.HTTPRoutes = append(o.HTTPRoutes, lo.Map(httpRoutes, func(httpRoute *gwapiv1.HTTPRoute, _ int) *HTTPRoute { + return &HTTPRoute{HTTPRoute: httpRoute} + })...) + } +} + +// WithServices adds services to the options to initialize a new Gateway API topology. +func WithServices(services ...*core.Service) GatewayAPITopologyOptionsFunc { + return func(o *GatewayAPITopologyOptions) { + o.Services = append(o.Services, lo.Map(services, func(service *core.Service, _ int) *Service { + return &Service{Service: service} + })...) + } +} + +// WithGatewayAPITopologyPolicies adds policies to the options to initialize a new Gateway API topology. +func WithGatewayAPITopologyPolicies(policies ...Policy) GatewayAPITopologyOptionsFunc { + return func(o *GatewayAPITopologyOptions) { + o.Policies = append(o.Policies, policies...) + } +} + +// ExpandGatewayListeners adds targetable gateway listeners to the options to initialize a new Gateway API topology. +func ExpandGatewayListeners() GatewayAPITopologyOptionsFunc { + return func(o *GatewayAPITopologyOptions) { + o.ExpandGatewayListeners = true + } +} + +// ExpandHTTPRouteRules adds targetable HTTP route rules to the options to initialize a new Gateway API topology. +func ExpandHTTPRouteRules() GatewayAPITopologyOptionsFunc { + return func(o *GatewayAPITopologyOptions) { + o.ExpandHTTPRouteRules = true + } +} + +// ExpandServicePorts adds targetable service ports to the options to initialize a new Gateway API topology. +func ExpandServicePorts() GatewayAPITopologyOptionsFunc { + return func(o *GatewayAPITopologyOptions) { + o.ExpandServicePorts = true + } +} + +// NewGatewayAPITopology returns a topology of Gateway API objects and attached policies. +// +// The links between the targetables are established based on the relationships defined by Gateway API. +// +// Principal objects like Gateways, HTTPRoutes and Services can be expanded to automatically include their targetable +// sections (listeners, route rules, service ports) as independent objects in the topology, by supplying the +// corresponding options ExpandGatewayListeners(), ExpandHTTPRouteRules(), and ExpandServicePorts(). +// The links will then be established accordingly. E.g.: +// - Without expanding Gateway listeners (default): Gateway -> HTTPRoute links. +// - Expanding Gateway listeners: Gateway -> Listener and Listener -> HTTPRoute links. +func NewGatewayAPITopology(options ...GatewayAPITopologyOptionsFunc) *Topology { + o := &GatewayAPITopologyOptions{} + for _, f := range options { + f(o) + } + + opts := []TopologyOptionsFunc{ + WithPolicies(o.Policies...), + WithTargetables(o.GatewayClasses...), + WithTargetables(o.Gateways...), + WithTargetables(o.HTTPRoutes...), + WithTargetables(o.Services...), + WithLinks(LinkGatewayClassToGatewayFunc(o.GatewayClasses)), // GatewayClass -> Gateway + } + + if o.ExpandGatewayListeners { + listeners := lo.FlatMap(o.Gateways, ListenersFromGatewayFunc) + opts = append(opts, WithTargetables(listeners...)) + opts = append(opts, WithLinks( + LinkGatewayToListenerFunc(), // Gateway -> Listener + LinkListenerToHTTPRouteFunc(o.Gateways, listeners), // Listener -> HTTPRoute + )) + } else { + opts = append(opts, WithLinks(LinkGatewayToHTTPRouteFunc(o.Gateways))) // Gateway -> HTTPRoute + } + + if o.ExpandHTTPRouteRules { + httpRouteRules := lo.FlatMap(o.HTTPRoutes, HTTPRouteRulesFromHTTPRouteFunc) + opts = append(opts, WithTargetables(httpRouteRules...)) + opts = append(opts, WithLinks(LinkHTTPRouteToHTTPRouteRuleFunc())) // HTTPRoute -> HTTPRouteRule + + if o.ExpandServicePorts { + servicePorts := lo.FlatMap(o.Services, ServicePortsFromBackendFunc) + opts = append(opts, WithTargetables(servicePorts...)) + opts = append(opts, WithLinks( + LinkHTTPRouteRuleToServicePortFunc(httpRouteRules), // HTTPRouteRule -> ServicePort + LinkHTTPRouteRuleToServiceFunc(httpRouteRules, true), // HTTPRouteRule -> Service + )) + } else { + opts = append(opts, WithLinks(LinkHTTPRouteRuleToServiceFunc(httpRouteRules, false))) // HTTPRouteRule -> Service + } + } else { + if o.ExpandServicePorts { + opts = append(opts, WithLinks( + LinkHTTPRouteToServicePortFunc(o.HTTPRoutes), // HTTPRoute -> ServicePort + LinkHTTPRouteToServiceFunc(o.HTTPRoutes, true), // HTTPRoute -> Service + )) + } else { + opts = append(opts, WithLinks(LinkHTTPRouteToServiceFunc(o.HTTPRoutes, false))) // HTTPRoute -> Service + } + } + + if o.ExpandServicePorts { + opts = append(opts, WithLinks(LinkServiceToServicePortFunc())) // Service -> ServicePort + } + + return NewTopology(opts...) +} + // ListenersFromGatewayFunc returns a list of targetable listeners from a targetable gateway. func ListenersFromGatewayFunc(gateway *Gateway, _ int) []*Listener { return lo.Map(gateway.Spec.Listeners, func(listener gwapiv1.Listener, _ int) *Listener { diff --git a/machinery/gateway_api_topology_test.go b/machinery/gateway_api_topology_test.go new file mode 100644 index 0000000..10eafc7 --- /dev/null +++ b/machinery/gateway_api_topology_test.go @@ -0,0 +1,263 @@ +//go:build unit + +package machinery + +import ( + "slices" + "testing" + + "github.com/samber/lo" + core "k8s.io/api/core/v1" + "k8s.io/utils/ptr" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gwapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +// TestGatewayAPITopology tests for a simplified topology of Gateway API resources without section names, +// i.e. where HTTPRoutes are not expanded to link from Listeners, and Policy TargetRefs are not of +// LocalPolicyTargetReferenceWithSectionName kind. +// +// This results in a topology with the following scheme: +// +// GatewayClass -> Gateway -> HTTPRoute -> Service +func TestGatewayAPITopology(t *testing.T) { + testCases := []struct { + name string + targetables GatewayAPIResources + policies []Policy + expectedLinks map[string][]string + }{ + { + name: "empty", + }, + { + name: "single node", + targetables: GatewayAPIResources{ + GatewayClasses: []*gwapiv1.GatewayClass{BuildGatewayClass()}, + }, + }, + { + name: "one of each kind", + targetables: GatewayAPIResources{ + GatewayClasses: []*gwapiv1.GatewayClass{BuildGatewayClass()}, + Gateways: []*gwapiv1.Gateway{BuildGateway()}, + HTTPRoutes: []*gwapiv1.HTTPRoute{BuildHTTPRoute()}, + Services: []*core.Service{BuildService()}, + }, + policies: []Policy{buildPolicy()}, + expectedLinks: map[string][]string{ + "my-gateway-class": {"my-gateway"}, + "my-gateway": {"my-http-route"}, + "my-http-route": {"my-service"}, + }, + }, + { + name: "complex topology", + targetables: BuildComplexGatewayAPITopology(), + expectedLinks: map[string][]string{ + "gatewayclass-1": {"gateway-1", "gateway-2", "gateway-3"}, + "gatewayclass-2": {"gateway-4", "gateway-5"}, + "gateway-1": {"route-1", "route-2"}, + "gateway-2": {"route-2", "route-3"}, + "gateway-3": {"route-4", "route-5"}, + "gateway-4": {"route-5", "route-6"}, + "gateway-5": {"route-7"}, + "route-1": {"service-1", "service-2"}, + "route-2": {"service-3"}, + "route-3": {"service-3"}, + "route-4": {"service-3", "service-4"}, + "route-5": {"service-5"}, + "route-6": {"service-5", "service-6"}, + "route-7": {"service-7"}, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + gatewayClasses := lo.Map(tc.targetables.GatewayClasses, func(gatewayClass *gwapiv1.GatewayClass, _ int) *GatewayClass { + return &GatewayClass{GatewayClass: gatewayClass} + }) + gateways := lo.Map(tc.targetables.Gateways, func(gateway *gwapiv1.Gateway, _ int) *Gateway { return &Gateway{Gateway: gateway} }) + httpRoutes := lo.Map(tc.targetables.HTTPRoutes, func(httpRoute *gwapiv1.HTTPRoute, _ int) *HTTPRoute { return &HTTPRoute{HTTPRoute: httpRoute} }) + services := lo.Map(tc.targetables.Services, func(service *core.Service, _ int) *Service { return &Service{Service: service} }) + + topology := NewTopology( + WithTargetables(gatewayClasses...), + WithTargetables(gateways...), + WithTargetables(httpRoutes...), + WithTargetables(services...), + WithLinks( + LinkGatewayClassToGatewayFunc(gatewayClasses), + LinkGatewayToHTTPRouteFunc(gateways), + LinkHTTPRouteToServiceFunc(httpRoutes, false), + ), + WithPolicies(tc.policies...), + ) + + links := make(map[string][]string) + for _, root := range topology.Roots() { + linksFromNode(topology, root, links) + } + for from, tos := range links { + expectedTos := tc.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") + }) + } +} + +// TestGatewayAPITopologyWithSectionNames tests for a topology of Gateway API resources where Gateways, HTTPRoutes +// and Services are expanded to include their named sections as targetables in the topology. +// +// This results in a topology with the following scheme: +// +// GatewayClass -> Gateway -> Listener -> HTTPRoute -> HTTPRouteRule -> Service -> ServicePort +// ∟> ServicePort <- Service +func TestGatewayAPITopologyWithSectionNames(t *testing.T) { + testCases := []struct { + name string + targetables GatewayAPIResources + policies []Policy + expectedLinks map[string][]string + }{ + { + name: "one of each kind", + targetables: GatewayAPIResources{ + GatewayClasses: []*gwapiv1.GatewayClass{BuildGatewayClass()}, + Gateways: []*gwapiv1.Gateway{BuildGateway()}, + HTTPRoutes: []*gwapiv1.HTTPRoute{BuildHTTPRoute()}, + Services: []*core.Service{BuildService()}, + }, + policies: []Policy{buildPolicy()}, + expectedLinks: map[string][]string{ + "my-gateway-class": {"my-gateway"}, + "my-gateway": {"my-gateway#my-listener"}, + "my-gateway#my-listener": {"my-http-route"}, + "my-http-route": {"my-http-route#rule-1"}, + "my-http-route#rule-1": {"my-service"}, + "my-service": {"my-service#http"}, + }, + }, + { + name: "policies with section names", + targetables: GatewayAPIResources{ + Gateways: []*gwapiv1.Gateway{BuildGateway(func(gateway *gwapiv1.Gateway) { + gateway.Spec.Listeners[0].Name = "http" + gateway.Spec.Listeners = append(gateway.Spec.Listeners, gwapiv1.Listener{ + Name: "https", + Port: 443, + Protocol: "HTTPS", + }) + })}, + }, + policies: []Policy{ + buildPolicy(func(policy *TestPolicy) { + policy.Name = "my-policy-1" + policy.Spec.TargetRef = gwapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1alpha2.LocalPolicyTargetReference{ + Group: gwapiv1.GroupName, + Kind: "Gateway", + Name: "my-gateway", + }, + SectionName: ptr.To(gwapiv1.SectionName("http")), + } + }), + buildPolicy(func(policy *TestPolicy) { + policy.Name = "my-policy-2" + policy.Spec.TargetRef = gwapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + LocalPolicyTargetReference: gwapiv1alpha2.LocalPolicyTargetReference{ + Group: gwapiv1.GroupName, + Kind: "Gateway", + Name: "my-gateway", + }, + SectionName: ptr.To(gwapiv1.SectionName("https")), + } + }), + }, + expectedLinks: map[string][]string{ + "my-gateway": {"my-gateway#http", "my-gateway#https"}, + }, + }, + { + name: "complex topology", + targetables: BuildComplexGatewayAPITopology(), + expectedLinks: map[string][]string{ + "gatewayclass-1": {"gateway-1", "gateway-2", "gateway-3"}, + "gatewayclass-2": {"gateway-4", "gateway-5"}, + "gateway-1": {"gateway-1#listener-1", "gateway-1#listener-2"}, + "gateway-2": {"gateway-2#listener-1"}, + "gateway-3": {"gateway-3#listener-1", "gateway-3#listener-2"}, + "gateway-4": {"gateway-4#listener-1", "gateway-4#listener-2"}, + "gateway-5": {"gateway-5#listener-1"}, + "gateway-1#listener-1": {"route-1"}, + "gateway-1#listener-2": {"route-1", "route-2"}, + "gateway-2#listener-1": {"route-2", "route-3"}, + "gateway-3#listener-1": {"route-4", "route-5"}, + "gateway-3#listener-2": {"route-4", "route-5"}, + "gateway-4#listener-1": {"route-5", "route-6"}, + "gateway-4#listener-2": {"route-5", "route-6"}, + "gateway-5#listener-1": {"route-7"}, + "route-1": {"route-1#rule-1", "route-1#rule-2"}, + "route-2": {"route-2#rule-1"}, + "route-3": {"route-3#rule-1"}, + "route-4": {"route-4#rule-1", "route-4#rule-2"}, + "route-5": {"route-5#rule-1", "route-5#rule-2"}, + "route-6": {"route-6#rule-1", "route-6#rule-2"}, + "route-7": {"route-7#rule-1"}, + "route-1#rule-1": {"service-1"}, + "route-1#rule-2": {"service-2"}, + "route-2#rule-1": {"service-3#port-1"}, + "route-3#rule-1": {"service-3#port-1"}, + "route-4#rule-1": {"service-3#port-2"}, + "route-4#rule-2": {"service-4#port-1"}, + "route-5#rule-1": {"service-5"}, + "route-5#rule-2": {"service-5"}, + "route-6#rule-1": {"service-5", "service-6"}, + "route-6#rule-2": {"service-6#port-1"}, + "route-7#rule-1": {"service-7"}, + "service-1": {"service-1#port-1", "service-1#port-2"}, + "service-2": {"service-2#port-1"}, + "service-3": {"service-3#port-1", "service-3#port-2"}, + "service-4": {"service-4#port-1"}, + "service-5": {"service-5#port-1"}, + "service-6": {"service-6#port-1"}, + "service-7": {"service-7#port-1"}, + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + topology := NewGatewayAPITopology( + WithGatewayClasses(tc.targetables.GatewayClasses...), + WithGateways(tc.targetables.Gateways...), + ExpandGatewayListeners(), + WithHTTPRoutes(tc.targetables.HTTPRoutes...), + ExpandHTTPRouteRules(), + WithServices(tc.targetables.Services...), + ExpandServicePorts(), + WithGatewayAPITopologyPolicies(tc.policies...), + ) + + links := make(map[string][]string) + for _, root := range topology.Roots() { + linksFromNode(topology, root, links) + } + for from, tos := range links { + expectedTos := tc.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/targetable.go b/machinery/gateway_api_types.go similarity index 51% rename from machinery/targetable.go rename to machinery/gateway_api_types.go index 1fbb344..bd720fd 100644 --- a/machinery/targetable.go +++ b/machinery/gateway_api_types.go @@ -1,22 +1,21 @@ package machinery import ( + "fmt" + core "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/ptr" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gwapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" ) -// Targetable is an interface that represents an object that can be targeted by policies. -type Targetable interface { - Object - - SetPolicies([]Policy) - Policies() []Policy -} +const nameSectionNameURLSeparator = '#' -func MapTargetableToURLFunc(t Targetable, _ int) string { - return t.GetURL() -} +// These are wrappers for Gateway API types so instances can be used as targetables in the topology. +// Targateables typically store back references to the policies that are attached to them. +// The implementation of GetURL() must return a unique identifier for the wrapped object that matches the one +// generated by policy targetRefs that implement the PolicyTargetReference interface for values pointing to the object. type GatewayClass struct { *gwapiv1.GatewayClass @@ -213,3 +212,109 @@ func (p *ServicePort) SetPolicies(policies []Policy) { func (p *ServicePort) Policies() []Policy { return p.attachedPolicies } + +// These are Gateway API target reference types that implement the PolicyTargetReference interface, so policies' +// targetRef instances can be treated as Objects whose GetURL() functions return the unique identifier of the +// corresponding targetable the reference points to. +// This is the reason why GetURL() was adopted to get the unique identifiers of topology objects instead of more +// obvious Kubernetes objects' GetUID() (k8s.io/apimachinery/pkg/apis/meta/v1). + +type NamespacedPolicyTargetReference struct { + gwapiv1alpha2.NamespacedPolicyTargetReference + PolicyNamespace string +} + +var _ PolicyTargetReference = NamespacedPolicyTargetReference{} + +func (t NamespacedPolicyTargetReference) GroupVersionKind() schema.GroupVersionKind { + return schema.GroupVersionKind{ + Group: string(t.Group), + Kind: string(t.Kind), + } +} + +func (t NamespacedPolicyTargetReference) SetGroupVersionKind(gvk schema.GroupVersionKind) { + t.Group = gwapiv1alpha2.Group(gvk.Group) + t.Kind = gwapiv1alpha2.Kind(gvk.Kind) +} + +func (t NamespacedPolicyTargetReference) GetURL() string { + return UrlFromObject(t) +} + +func (t NamespacedPolicyTargetReference) GetNamespace() string { + return string(ptr.Deref(t.Namespace, gwapiv1alpha2.Namespace(t.PolicyNamespace))) +} + +func (t NamespacedPolicyTargetReference) GetName() string { + return string(t.NamespacedPolicyTargetReference.Name) +} + +type LocalPolicyTargetReference struct { + gwapiv1alpha2.LocalPolicyTargetReference + PolicyNamespace string +} + +var _ PolicyTargetReference = LocalPolicyTargetReference{} + +func (t LocalPolicyTargetReference) GroupVersionKind() schema.GroupVersionKind { + return schema.GroupVersionKind{ + Group: string(t.Group), + Kind: string(t.Kind), + } +} + +func (t LocalPolicyTargetReference) SetGroupVersionKind(gvk schema.GroupVersionKind) { + t.Group = gwapiv1alpha2.Group(gvk.Group) + t.Kind = gwapiv1alpha2.Kind(gvk.Kind) +} + +func (t LocalPolicyTargetReference) GetURL() string { + return UrlFromObject(t) +} + +func (t LocalPolicyTargetReference) GetNamespace() string { + return t.PolicyNamespace +} + +func (t LocalPolicyTargetReference) GetName() string { + return string(t.LocalPolicyTargetReference.Name) +} + +type LocalPolicyTargetReferenceWithSectionName struct { + gwapiv1alpha2.LocalPolicyTargetReferenceWithSectionName + PolicyNamespace string +} + +var _ PolicyTargetReference = LocalPolicyTargetReferenceWithSectionName{} + +func (t LocalPolicyTargetReferenceWithSectionName) GroupVersionKind() schema.GroupVersionKind { + return schema.GroupVersionKind{ + Group: string(t.Group), + Kind: string(t.Kind), + } +} + +func (t LocalPolicyTargetReferenceWithSectionName) SetGroupVersionKind(gvk schema.GroupVersionKind) { + t.Group = gwapiv1alpha2.Group(gvk.Group) + t.Kind = gwapiv1alpha2.Kind(gvk.Kind) +} + +func (t LocalPolicyTargetReferenceWithSectionName) GetURL() string { + return UrlFromObject(t) +} + +func (t LocalPolicyTargetReferenceWithSectionName) GetNamespace() string { + return t.PolicyNamespace +} + +func (t LocalPolicyTargetReferenceWithSectionName) GetName() string { + if t.SectionName == nil { + return string(t.LocalPolicyTargetReference.Name) + } + return namespacedSectionName(string(t.LocalPolicyTargetReference.Name), *t.SectionName) +} + +func namespacedSectionName(namespace string, sectionName gwapiv1.SectionName) string { + return fmt.Sprintf("%s%s%s", namespace, string(nameSectionNameURLSeparator), sectionName) +} diff --git a/machinery/object.go b/machinery/object.go deleted file mode 100644 index a3273c3..0000000 --- a/machinery/object.go +++ /dev/null @@ -1,36 +0,0 @@ -package machinery - -import ( - "fmt" - "strings" - - "k8s.io/apimachinery/pkg/runtime/schema" - k8stypes "k8s.io/apimachinery/pkg/types" - gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" -) - -const ( - kindNameURLSeparator = ':' - nameSectionNameURLSeparator = '#' -) - -type Object interface { - schema.ObjectKind - - GetNamespace() string - GetName() string - GetURL() string -} - -func namespacedName(namespace, name string) string { - return k8stypes.NamespacedName{Namespace: namespace, Name: name}.String() -} - -func namespacedSectionName(namespace string, sectionName gwapiv1.SectionName) string { - return fmt.Sprintf("%s%s%s", namespace, string(nameSectionNameURLSeparator), sectionName) -} - -func UrlFromObject(obj Object) string { - name := strings.TrimPrefix(namespacedName(obj.GetNamespace(), obj.GetName()), string(k8stypes.Separator)) - return fmt.Sprintf("%s%s%s", strings.ToLower(obj.GroupVersionKind().GroupKind().String()), string(kindNameURLSeparator), name) -} diff --git a/machinery/policy.go b/machinery/policy.go deleted file mode 100644 index 6cb2fd6..0000000 --- a/machinery/policy.go +++ /dev/null @@ -1,130 +0,0 @@ -package machinery - -import ( - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/utils/ptr" - gwapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" -) - -// Policy targets objects and can be merged with another Policy based on a given MergeStrategy. -type Policy interface { - Object - - GetTargetRefs() []PolicyTargetReference - GetMergeStrategy() MergeStrategy - - Merge(Policy) Policy -} - -// PolicyTargetReference is a generic interface for all kinds of Gateway API policy target references. -// It implements the Object interface for the referent. -type PolicyTargetReference interface { - Object -} - -type NamespacedPolicyTargetReference struct { - gwapiv1alpha2.NamespacedPolicyTargetReference - PolicyNamespace string -} - -var _ PolicyTargetReference = NamespacedPolicyTargetReference{} - -func (t NamespacedPolicyTargetReference) GroupVersionKind() schema.GroupVersionKind { - return schema.GroupVersionKind{ - Group: string(t.Group), - Kind: string(t.Kind), - } -} - -func (t NamespacedPolicyTargetReference) SetGroupVersionKind(gvk schema.GroupVersionKind) { - t.Group = gwapiv1alpha2.Group(gvk.Group) - t.Kind = gwapiv1alpha2.Kind(gvk.Kind) -} - -func (t NamespacedPolicyTargetReference) GetURL() string { - return UrlFromObject(t) -} - -func (t NamespacedPolicyTargetReference) GetNamespace() string { - return string(ptr.Deref(t.Namespace, gwapiv1alpha2.Namespace(t.PolicyNamespace))) -} - -func (t NamespacedPolicyTargetReference) GetName() string { - return string(t.NamespacedPolicyTargetReference.Name) -} - -type LocalPolicyTargetReference struct { - gwapiv1alpha2.LocalPolicyTargetReference - PolicyNamespace string -} - -var _ PolicyTargetReference = LocalPolicyTargetReference{} - -func (t LocalPolicyTargetReference) GroupVersionKind() schema.GroupVersionKind { - return schema.GroupVersionKind{ - Group: string(t.Group), - Kind: string(t.Kind), - } -} - -func (t LocalPolicyTargetReference) SetGroupVersionKind(gvk schema.GroupVersionKind) { - t.Group = gwapiv1alpha2.Group(gvk.Group) - t.Kind = gwapiv1alpha2.Kind(gvk.Kind) -} - -func (t LocalPolicyTargetReference) GetURL() string { - return UrlFromObject(t) -} - -func (t LocalPolicyTargetReference) GetNamespace() string { - return t.PolicyNamespace -} - -func (t LocalPolicyTargetReference) GetName() string { - return string(t.LocalPolicyTargetReference.Name) -} - -type LocalPolicyTargetReferenceWithSectionName struct { - gwapiv1alpha2.LocalPolicyTargetReferenceWithSectionName - PolicyNamespace string -} - -var _ PolicyTargetReference = LocalPolicyTargetReferenceWithSectionName{} - -func (t LocalPolicyTargetReferenceWithSectionName) GroupVersionKind() schema.GroupVersionKind { - return schema.GroupVersionKind{ - Group: string(t.Group), - Kind: string(t.Kind), - } -} - -func (t LocalPolicyTargetReferenceWithSectionName) SetGroupVersionKind(gvk schema.GroupVersionKind) { - t.Group = gwapiv1alpha2.Group(gvk.Group) - t.Kind = gwapiv1alpha2.Kind(gvk.Kind) -} - -func (t LocalPolicyTargetReferenceWithSectionName) GetURL() string { - return UrlFromObject(t) -} - -func (t LocalPolicyTargetReferenceWithSectionName) GetNamespace() string { - return t.PolicyNamespace -} - -func (t LocalPolicyTargetReferenceWithSectionName) GetName() string { - if t.SectionName == nil { - return string(t.LocalPolicyTargetReference.Name) - } - return namespacedSectionName(string(t.LocalPolicyTargetReference.Name), *t.SectionName) -} - -// MergeStrategy is a function that merges two Policy objects into a new Policy object. -type MergeStrategy func(Policy, Policy) Policy - -var DefaultMergeStrategy = NoMergeStrategy - -func NoMergeStrategy(_, target Policy) Policy { - return target -} - -var _ MergeStrategy = NoMergeStrategy diff --git a/machinery/test_helper.go b/machinery/test_helper.go index bdd9297..4792228 100644 --- a/machinery/test_helper.go +++ b/machinery/test_helper.go @@ -3,374 +3,280 @@ package machinery import ( - core "k8s.io/api/core/v1" + "bytes" + "fmt" + "os" + "strings" + "testing" + + "github.com/samber/lo" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/utils/ptr" - gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" ) -func BuildGatewayClass(f ...func(*gwapiv1.GatewayClass)) *gwapiv1.GatewayClass { - gc := &gwapiv1.GatewayClass{ - TypeMeta: metav1.TypeMeta{ - APIVersion: gwapiv1.GroupVersion.String(), - Kind: "GatewayClass", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "my-gateway-class", - }, - Spec: gwapiv1.GatewayClassSpec{ - ControllerName: gwapiv1.GatewayController("my-gateway-controller"), - }, +// SaveToOutputDir saves the output of a test case to a file in the output directory. +func SaveToOutputDir(t *testing.T, out *bytes.Buffer, outDir, ext string) { + file, err := os.Create(fmt.Sprintf("%s/%s%s", outDir, strings.ReplaceAll(t.Name(), "/", "__"), ext)) + if err != nil { + t.Fatal(err) } - for _, fn := range f { - fn(gc) + defer file.Close() + _, err = file.Write(out.Bytes()) + if err != nil { + t.Fatal(err) } - return gc } -func BuildGateway(f ...func(*gwapiv1.Gateway)) *gwapiv1.Gateway { - g := &gwapiv1.Gateway{ - TypeMeta: metav1.TypeMeta{ - APIVersion: gwapiv1.GroupVersion.String(), - Kind: "Gateway", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "my-gateway", - Namespace: "my-namespace", - }, - Spec: gwapiv1.GatewaySpec{ - GatewayClassName: "my-gateway-class", - Listeners: []gwapiv1.Listener{ - { - Name: "my-listener", - Port: 80, - Protocol: "HTTP", - }, - }, - }, +func linksFromNode(topology *Topology, node Targetable, edges map[string][]string) { + if _, ok := edges[node.GetName()]; ok { + return } - for _, fn := range f { - fn(g) + children := topology.Children(node) + edges[node.GetName()] = lo.Map(children, func(child Targetable, _ int) string { return child.GetName() }) + for _, child := range children { + linksFromNode(topology, child, edges) } - return g } -func BuildHTTPRoute(f ...func(*gwapiv1.HTTPRoute)) *gwapiv1.HTTPRoute { - r := &gwapiv1.HTTPRoute{ - TypeMeta: metav1.TypeMeta{ - APIVersion: gwapiv1.GroupVersion.String(), - Kind: "HTTPRoute", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "my-http-route", - Namespace: "my-namespace", - }, - Spec: gwapiv1.HTTPRouteSpec{ - CommonRouteSpec: gwapiv1.CommonRouteSpec{ - ParentRefs: []gwapiv1.ParentReference{ - { - Name: "my-gateway", - }, - }, - }, - Rules: []gwapiv1.HTTPRouteRule{ - { - BackendRefs: []gwapiv1.HTTPBackendRef{BuildHTTPBackendRef()}, - }, - }, - }, +const TestGroupName = "example.test" + +type Apple struct { + Name string + + policies []Policy +} + +var _ Targetable = &Apple{} + +func (a *Apple) GetName() string { + return a.Name +} + +func (a *Apple) GetNamespace() string { + return "" +} + +func (a *Apple) GetURL() string { + return UrlFromObject(a) +} + +func (a *Apple) GroupVersionKind() schema.GroupVersionKind { + return schema.GroupVersionKind{ + Group: TestGroupName, + Version: "v1", + Kind: "Apple", } - for _, fn := range f { - fn(r) +} + +func (a *Apple) SetGroupVersionKind(schema.GroupVersionKind) {} + +func (a *Apple) Policies() []Policy { + return a.policies +} + +func (a *Apple) SetPolicies(policies []Policy) { + a.policies = policies +} + +type Orange struct { + Name string + Namespace string + AppleParents []string + ChildBananas []string + + policies []Policy +} + +var _ Targetable = &Orange{} + +func (o *Orange) GetName() string { + return o.Name +} + +func (o *Orange) GetNamespace() string { + return o.Namespace +} + +func (o *Orange) GetURL() string { + return UrlFromObject(o) +} + +func (o *Orange) GroupVersionKind() schema.GroupVersionKind { + return schema.GroupVersionKind{ + Group: TestGroupName, + Version: "v1beta1", + Kind: "Orange", } - return r } -func BuildHTTPBackendRef(f ...func(*gwapiv1.BackendObjectReference)) gwapiv1.HTTPBackendRef { - bor := &gwapiv1.BackendObjectReference{ - Name: "my-service", +func (o *Orange) SetGroupVersionKind(schema.GroupVersionKind) {} + +func (o *Orange) Policies() []Policy { + return o.policies +} + +func (o *Orange) SetPolicies(policies []Policy) { + o.policies = policies +} + +type Banana struct { + Name string +} + +var _ Targetable = &Banana{} + +func (b *Banana) GetName() string { + return b.Name +} + +func (b *Banana) GetNamespace() string { + return "" +} + +func (b *Banana) GetURL() string { + return UrlFromObject(b) +} + +func (b *Banana) GroupVersionKind() schema.GroupVersionKind { + return schema.GroupVersionKind{ + Group: TestGroupName, + Version: "v1beta1", + Kind: "Banana", } - for _, fn := range f { - fn(bor) +} + +func (b *Banana) SetGroupVersionKind(schema.GroupVersionKind) {} + +func (b *Banana) Policies() []Policy { + return nil +} + +func (b *Banana) SetPolicies(policies []Policy) {} + +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 { + orange := child.(*Orange) + return lo.FilterMap(apples, func(apple *Apple, _ int) (Targetable, bool) { + return apple, lo.Contains(orange.AppleParents, apple.Name) + }) + }, } - return gwapiv1.HTTPBackendRef{ - BackendRef: gwapiv1.BackendRef{ - BackendObjectReference: *bor, +} + +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 { + banana := child.(*Banana) + return lo.FilterMap(oranges, func(orange *Orange, _ int) (Targetable, bool) { + return orange, lo.Contains(orange.ChildBananas, banana.Name) + }) }, } } -func BuildService(f ...func(*core.Service)) *core.Service { - s := &core.Service{ +type FruitPolicy struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FruitPolicySpec `json:"spec"` +} + +type FruitPolicySpec struct { + TargetRef FruitPolicyTargetReference `json:"targetRef"` +} + +var _ Policy = &FruitPolicy{} + +func (p *FruitPolicy) GetURL() string { + return UrlFromObject(p) +} + +func (p *FruitPolicy) GetTargetRefs() []PolicyTargetReference { + var namespace *string + group := p.Spec.TargetRef.Group + kind := p.Spec.TargetRef.Kind + if group == TestGroupName && kind == "Orange" { + namespace = ptr.To(ptr.Deref(p.Spec.TargetRef.Namespace, p.Namespace)) + } + return []PolicyTargetReference{ + FruitPolicyTargetReference{ + Group: group, + Kind: kind, + Name: p.Spec.TargetRef.Name, + Namespace: namespace, + }, + } +} + +func (p *FruitPolicy) GetMergeStrategy() MergeStrategy { + return DefaultMergeStrategy +} + +func (p *FruitPolicy) Merge(policy Policy) Policy { + return &FruitPolicy{ + Spec: p.Spec, + } +} + +type FruitPolicyTargetReference struct { + Group string + Kind string + Name string + Namespace *string +} + +var _ PolicyTargetReference = FruitPolicyTargetReference{} + +func (t FruitPolicyTargetReference) GroupVersionKind() schema.GroupVersionKind { + return schema.GroupVersionKind{ + Group: t.Group, + Kind: t.Kind, + } +} + +func (t FruitPolicyTargetReference) SetGroupVersionKind(gvk schema.GroupVersionKind) { + t.Group = gvk.Group + t.Kind = gvk.Kind +} + +func (t FruitPolicyTargetReference) GetURL() string { + return UrlFromObject(t) +} + +func (t FruitPolicyTargetReference) GetNamespace() string { + return ptr.Deref(t.Namespace, "") +} + +func (t FruitPolicyTargetReference) GetName() string { + return t.Name +} + +func buildFruitPolicy(f ...func(*FruitPolicy)) *FruitPolicy { + p := &FruitPolicy{ TypeMeta: metav1.TypeMeta{ - APIVersion: core.SchemeGroupVersion.String(), - Kind: "Service", + APIVersion: "test/v1", + Kind: "FruitPolicy", }, ObjectMeta: metav1.ObjectMeta{ - Name: "my-service", + Name: "my-policy", Namespace: "my-namespace", }, - Spec: core.ServiceSpec{ - Ports: []core.ServicePort{ - { - Name: "http", - Port: 80, - }, - }, - Selector: map[string]string{ - "app": "my-app", + Spec: FruitPolicySpec{ + TargetRef: FruitPolicyTargetReference{ + Group: TestGroupName, + Kind: "Orange", + Name: "my-orange", }, }, } for _, fn := range f { - fn(s) - } - return s -} - -type GatewayAPIResources struct { - GatewayClasses []*gwapiv1.GatewayClass - Gateways []*gwapiv1.Gateway - HTTPRoutes []*gwapiv1.HTTPRoute - Services []*core.Service -} - -// BuildComplexGatewayAPITopology returns a set of Gateway API resources organized : -// -// ┌────────────────┐ ┌────────────────┐ -// │ gatewayclass-1 │ │ gatewayclass-2 │ -// └────────────────┘ └────────────────┘ -// ▲ ▲ -// │ │ -// ┌─────────────────────────┼──────────────────────────┐ ┌────────────┴─────────────┐ -// │ │ │ │ │ -// ┌───────────────┴───────────────┐ ┌───────┴────────┐ ┌───────────────┴───────────────┐ ┌──────────────┴────────────────┐ ┌───────┴────────┐ -// │ gateway-1 │ │ gateway-2 │ │ gateway-3 │ │ gateway-4 │ │ gateway-5 │ -// │ │ │ │ │ │ │ │ │ │ -// │ ┌────────────┐ ┌────────────┐ │ │ ┌────────────┐ │ │ ┌────────────┐ ┌────────────┐ │ │ ┌────────────┐ ┌────────────┐ │ │ ┌────────────┐ │ -// │ │ listener-1 │ │ listener-2 │ │ │ │ listener-1 │ │ │ │ listener-1 │ │ listener-2 │ │ │ │ listener-1 │ │ listener-2 │ │ │ │ listener-1 │ │ -// │ └────────────┘ └────────────┘ │ │ └────────────┘ │ │ └────────────┘ └────────────┘ │ │ └────────────┘ └────────────┘ │ │ └────────────┘ │ -// │ ▲ │ │ ▲ │ │ │ │ │ │ │ -// └────────────────────────┬──────┘ └──────┬─────────┘ └───────────────────────────────┘ └───────────────────────────────┘ └────────────────┘ -// ▲ │ │ ▲ ▲ ▲ ▲ ▲ ▲ -// │ │ │ │ │ │ │ │ │ -// │ └───────┬───────┘ │ │ └────────────┬─────────────┘ │ │ -// │ │ │ │ │ │ │ -// ┌───────────┴───────────┐ ┌──────┴─────┐ ┌─────┴──────┐ ┌───────────┴───────────┐ ┌───────────┴───────────┐ ┌───────────┴───────────┐ ┌─────┴──────┐ -// │ route-1 │ │ route-2 │ │ route-3 │ │ route-4 │ │ route-5 │ │ route-6 │ │ route-7 │ -// │ │ │ │ │ │ │ │ │ │ │ │ │ │ -// │ ┌────────┐ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ ┌────────┐ │ │ ┌────────┐ ┌────────┐ │ │ ┌────────┐ ┌────────┐ │ │ ┌────────┐ │ -// │ │ rule-1 │ │ rule-2 │ │ │ │ rule-1 │ │ │ │ rule-1 │ │ │ │ rule-1 │ │ rule-2 │ │ │ │ rule-1 │ │ rule-2 │ │ │ │ rule-1 │ │ rule-2 │ │ │ │ rule-1 │ │ -// │ └────┬───┘ └────┬───┘ │ │ └────┬───┘ │ │ └───┬────┘ │ │ └─┬──────┘ └───┬────┘ │ │ └───┬────┘ └────┬───┘ │ │ └─┬────┬─┘ └────┬───┘ │ │ └────┬───┘ │ -// │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ -// └──────┼──────────┼─────┘ └──────┼─────┘ └─────┼──────┘ └───┼────────────┼──────┘ └─────┼───────────┼─────┘ └───┼────┼────────┼─────┘ └──────┼─────┘ -// │ │ │ │ │ │ │ │ │ │ │ │ -// │ │ └─────────────┤ │ │ └───────────┴───────────┘ │ │ │ -// ▼ ▼ │ │ │ ▼ ▼ │ ▼ -// ┌───────────────────────┐ ┌────────────┐ ┌──────┴────────────┴───┐ ┌─────┴──────┐ ┌────────────┐ ┌─────────┴──┐ ┌────────────┐ -// │ │ │ │ │ ▼ ▼ │ │ ▼ │ │ │ │ ▼ │ │ │ -// │ ┌────────┐ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │ -// │ │ port-1 │ │ port-2 │ │ │ │ port-1 │ │ │ │ port-1 │ │ port-2 │ │ │ │ port-1 │ │ │ │ port-1 │ │ │ │ port-1 │ │ │ │ port-1 │ │ -// │ └────────┘ └────────┘ │ │ └────────┘ │ │ └────────┘ └────────┘ │ │ └────────┘ │ │ └────────┘ │ │ └────────┘ │ │ └────────┘ │ -// │ │ │ │ │ │ │ │ │ │ │ │ │ │ -// │ service-1 │ │ service-2 │ │ service-3 │ │ service-4 │ │ service-5 │ │ service-6 │ │ service-7 │ -// └───────────────────────┘ └────────────┘ └───────────────────────┘ └────────────┘ └────────────┘ └────────────┘ └────────────┘ -func BuildComplexGatewayAPITopology(funcs ...func(*GatewayAPIResources)) GatewayAPIResources { - t := GatewayAPIResources{ - GatewayClasses: []*gwapiv1.GatewayClass{ - BuildGatewayClass(func(gc *gwapiv1.GatewayClass) { gc.Name = "gatewayclass-1" }), - BuildGatewayClass(func(gc *gwapiv1.GatewayClass) { gc.Name = "gatewayclass-2" }), - }, - Gateways: []*gwapiv1.Gateway{ - BuildGateway(func(g *gwapiv1.Gateway) { - g.Name = "gateway-1" - g.Spec.GatewayClassName = "gatewayclass-1" - g.Spec.Listeners[0].Name = "listener-1" - g.Spec.Listeners = append(g.Spec.Listeners, gwapiv1.Listener{ - Name: "listener-2", - Port: 443, - Protocol: "HTTPS", - }) - }), - BuildGateway(func(g *gwapiv1.Gateway) { - g.Name = "gateway-2" - g.Spec.GatewayClassName = "gatewayclass-1" - g.Spec.Listeners[0].Name = "listener-1" - }), - BuildGateway(func(g *gwapiv1.Gateway) { - g.Name = "gateway-3" - g.Spec.GatewayClassName = "gatewayclass-1" - g.Spec.Listeners[0].Name = "listener-1" - g.Spec.Listeners = append(g.Spec.Listeners, gwapiv1.Listener{ - Name: "listener-2", - Port: 443, - Protocol: "HTTPS", - }) - }), - BuildGateway(func(g *gwapiv1.Gateway) { - g.Name = "gateway-4" - g.Spec.GatewayClassName = "gatewayclass-2" - g.Spec.Listeners[0].Name = "listener-1" - g.Spec.Listeners = append(g.Spec.Listeners, gwapiv1.Listener{ - Name: "listener-2", - Port: 443, - Protocol: "HTTPS", - }) - }), - BuildGateway(func(g *gwapiv1.Gateway) { - g.Name = "gateway-5" - g.Spec.GatewayClassName = "gatewayclass-2" - g.Spec.Listeners[0].Name = "listener-1" - }), - }, - HTTPRoutes: []*gwapiv1.HTTPRoute{ - BuildHTTPRoute(func(r *gwapiv1.HTTPRoute) { - r.Name = "route-1" - r.Spec.ParentRefs[0].Name = "gateway-1" - r.Spec.Rules = []gwapiv1.HTTPRouteRule{ - { // rule-1 - BackendRefs: []gwapiv1.HTTPBackendRef{BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { - backendRef.Name = "service-1" - })}, - }, - { // rule-2 - BackendRefs: []gwapiv1.HTTPBackendRef{BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { - backendRef.Name = "service-2" - })}, - }, - } - }), - BuildHTTPRoute(func(r *gwapiv1.HTTPRoute) { - r.Name = "route-2" - r.Spec.ParentRefs = []gwapiv1.ParentReference{ - { - Name: "gateway-1", - SectionName: ptr.To(gwapiv1.SectionName("listener-2")), - }, - { - Name: "gateway-2", - SectionName: ptr.To(gwapiv1.SectionName("listener-1")), - }, - } - r.Spec.Rules[0].BackendRefs[0] = BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { - backendRef.Name = "service-3" - backendRef.Port = ptr.To(gwapiv1.PortNumber(80)) // port-1 - }) - }), - BuildHTTPRoute(func(r *gwapiv1.HTTPRoute) { - r.Name = "route-3" - r.Spec.ParentRefs[0].Name = "gateway-2" - r.Spec.Rules[0].BackendRefs[0] = BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { - backendRef.Name = "service-3" - backendRef.Port = ptr.To(gwapiv1.PortNumber(80)) // port-1 - }) - }), - BuildHTTPRoute(func(r *gwapiv1.HTTPRoute) { - r.Name = "route-4" - r.Spec.ParentRefs[0].Name = "gateway-3" - r.Spec.Rules = []gwapiv1.HTTPRouteRule{ - { // rule-1 - BackendRefs: []gwapiv1.HTTPBackendRef{BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { - backendRef.Name = "service-3" - backendRef.Port = ptr.To(gwapiv1.PortNumber(443)) // port-2 - })}, - }, - { // rule-2 - BackendRefs: []gwapiv1.HTTPBackendRef{BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { - backendRef.Name = "service-4" - backendRef.Port = ptr.To(gwapiv1.PortNumber(80)) // port-1 - })}, - }, - } - }), - BuildHTTPRoute(func(r *gwapiv1.HTTPRoute) { - r.Name = "route-5" - r.Spec.ParentRefs[0].Name = "gateway-3" - r.Spec.ParentRefs = append(r.Spec.ParentRefs, gwapiv1.ParentReference{Name: "gateway-4"}) - r.Spec.Rules = []gwapiv1.HTTPRouteRule{ - { // rule-1 - BackendRefs: []gwapiv1.HTTPBackendRef{BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { - backendRef.Name = "service-5" - })}, - }, - { // rule-2 - BackendRefs: []gwapiv1.HTTPBackendRef{BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { - backendRef.Name = "service-5" - })}, - }, - } - }), - BuildHTTPRoute(func(r *gwapiv1.HTTPRoute) { - r.Name = "route-6" - r.Spec.ParentRefs[0].Name = "gateway-4" - r.Spec.Rules = []gwapiv1.HTTPRouteRule{ - { // rule-1 - BackendRefs: []gwapiv1.HTTPBackendRef{ - BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { - backendRef.Name = "service-5" - }), - BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { - backendRef.Name = "service-6" - }), - }, - }, - { // rule-2 - BackendRefs: []gwapiv1.HTTPBackendRef{BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { - backendRef.Name = "service-6" - backendRef.Port = ptr.To(gwapiv1.PortNumber(80)) // port-1 - })}, - }, - } - }), - BuildHTTPRoute(func(r *gwapiv1.HTTPRoute) { - r.Name = "route-7" - r.Spec.ParentRefs[0].Name = "gateway-5" - r.Spec.Rules[0].BackendRefs[0] = BuildHTTPBackendRef(func(backendRef *gwapiv1.BackendObjectReference) { - backendRef.Name = "service-7" - }) - }), - }, - Services: []*core.Service{ - BuildService(func(s *core.Service) { - s.Name = "service-1" - s.Spec.Ports[0].Name = "port-1" - s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{ - Name: "port-2", - Port: 443, - }) - }), - BuildService(func(s *core.Service) { - s.Name = "service-2" - s.Spec.Ports[0].Name = "port-1" - }), - BuildService(func(s *core.Service) { - s.Name = "service-3" - s.Spec.Ports[0].Name = "port-1" - s.Spec.Ports = append(s.Spec.Ports, core.ServicePort{ - Name: "port-2", - Port: 443, - }) - }), - BuildService(func(s *core.Service) { - s.Name = "service-4" - s.Spec.Ports[0].Name = "port-1" - }), - BuildService(func(s *core.Service) { - s.Name = "service-5" - s.Spec.Ports[0].Name = "port-1" - }), - BuildService(func(s *core.Service) { - s.Name = "service-6" - s.Spec.Ports[0].Name = "port-1" - }), - BuildService(func(s *core.Service) { - s.Name = "service-7" - s.Spec.Ports[0].Name = "port-1" - }), - }, - } - for _, f := range funcs { - f(&t) + fn(p) } - return t + return p } diff --git a/machinery/topology_test.go b/machinery/topology_test.go index 388afae..a95d9cd 100644 --- a/machinery/topology_test.go +++ b/machinery/topology_test.go @@ -3,162 +3,165 @@ package machinery import ( - "bytes" - "fmt" - "os" "slices" "strings" "testing" "github.com/samber/lo" - core "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/ptr" - gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" - gwapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" ) -const outDir = "../tests/out" - func TestTopologyRoots(t *testing.T) { - gateways := []*Gateway{ - {Gateway: BuildGateway()}, - {Gateway: BuildGateway(func(g *gwapiv1.Gateway) { g.Name = "my-gateway-2" })}, + apples := []*Apple{ + {Name: "apple-1"}, + {Name: "apple-2"}, } - httpRoute := &HTTPRoute{HTTPRoute: BuildHTTPRoute()} - httpRouteRules := HTTPRouteRulesFromHTTPRouteFunc(httpRoute, 0) topology := NewTopology( - WithTargetables(gateways...), - WithTargetables(httpRoute), - WithTargetables(httpRouteRules...), - WithLinks( - LinkGatewayToHTTPRouteFunc(gateways), - LinkHTTPRouteToHTTPRouteRuleFunc(), - ), + WithTargetables(apples...), + WithTargetables(&Orange{Name: "orange-1", Namespace: "my-namespace", AppleParents: []string{"apple-1"}}), + WithLinks(LinkApplesToOranges(apples)), WithPolicies( - buildPolicy(func(policy *TestPolicy) { - policy.Name = "my-policy-1" - policy.Spec.TargetRef = gwapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ - LocalPolicyTargetReference: gwapiv1alpha2.LocalPolicyTargetReference{ - Group: gwapiv1.GroupName, - Kind: "HTTPRoute", - Name: "my-http-route", - }, + buildFruitPolicy(func(policy *FruitPolicy) { + policy.Name = "policy-1" + policy.Spec.TargetRef = FruitPolicyTargetReference{ + Group: TestGroupName, + Kind: "Apple", + Name: "apple-2", } }), - buildPolicy(func(policy *TestPolicy) { - policy.Name = "my-policy-2" - policy.Spec.TargetRef = gwapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ - LocalPolicyTargetReference: gwapiv1alpha2.LocalPolicyTargetReference{ - Group: gwapiv1.GroupName, - Kind: "Gateway", - Name: "my-gateway-2", - }, + buildFruitPolicy(func(policy *FruitPolicy) { + policy.Name = "policy-2" + policy.Spec.TargetRef = FruitPolicyTargetReference{ + Group: TestGroupName, + Kind: "Orange", + Name: "orange-1", } }), ), ) roots := topology.Roots() - if expected := len(gateways); len(roots) != expected { + if expected := len(apples); len(roots) != expected { t.Errorf("expected %d roots, got %d", expected, len(roots)) } rootURLs := lo.Map(roots, MapTargetableToURLFunc) - for _, gateway := range gateways { - if !lo.Contains(rootURLs, gateway.GetURL()) { - t.Errorf("expected root %s not found", gateway.GetURL()) + for _, apple := range apples { + if !lo.Contains(rootURLs, apple.GetURL()) { + t.Errorf("expected root %s not found", apple.GetURL()) } } } func TestTopologyParents(t *testing.T) { - gateways := []*Gateway{{Gateway: BuildGateway()}} - httpRoute := &HTTPRoute{HTTPRoute: BuildHTTPRoute()} - httpRouteRules := HTTPRouteRulesFromHTTPRouteFunc(httpRoute, 0) + apple1 := &Apple{Name: "apple-1"} + apple2 := &Apple{Name: "apple-2"} + orange1 := &Orange{Name: "orange-1", Namespace: "my-namespace", AppleParents: []string{"apple-1", "apple-2"}} + orange2 := &Orange{Name: "orange-2", Namespace: "my-namespace", AppleParents: []string{"apple-2"}} topology := NewTopology( - WithTargetables(gateways...), - WithTargetables(httpRoute), - WithTargetables(httpRouteRules...), - WithLinks( - LinkGatewayToHTTPRouteFunc(gateways), - LinkHTTPRouteToHTTPRouteRuleFunc(), - ), + WithTargetables(apple1, apple2), + WithTargetables(orange1, orange2), + WithLinks(LinkApplesToOranges([]*Apple{apple1, apple2})), WithPolicies( - buildPolicy(func(policy *TestPolicy) { - policy.Spec.TargetRef = gwapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ - LocalPolicyTargetReference: gwapiv1alpha2.LocalPolicyTargetReference{ - Group: gwapiv1.GroupName, - Kind: "HTTPRoute", - Name: "my-http-route", - }, + buildFruitPolicy(func(policy *FruitPolicy) { + policy.Name = "policy-2" + policy.Spec.TargetRef = FruitPolicyTargetReference{ + Group: TestGroupName, + Kind: "Orange", + Name: "orange-1", } }), ), ) - parents := topology.Parents(httpRoute) - if expected := 1; len(parents) != expected { + // orange-1 + parents := topology.Parents(orange1) + if expected := 2; len(parents) != expected { t.Errorf("expected %d parent, got %d", expected, len(parents)) } parentURLs := lo.Map(parents, MapTargetableToURLFunc) - for _, gateway := range gateways { - if !lo.Contains(parentURLs, gateway.GetURL()) { - t.Errorf("expected parent %s not found", gateway.GetURL()) - } + if !lo.Contains(parentURLs, apple1.GetURL()) { + t.Errorf("expected parent %s not found", apple1.GetURL()) + } + if !lo.Contains(parentURLs, apple2.GetURL()) { + t.Errorf("expected parent %s not found", apple2.GetURL()) + } + // orange-2 + parents = topology.Parents(orange2) + if expected := 1; len(parents) != expected { + t.Errorf("expected %d parent, got %d", expected, len(parents)) + } + parentURLs = lo.Map(parents, MapTargetableToURLFunc) + if !lo.Contains(parentURLs, apple2.GetURL()) { + t.Errorf("expected parent %s not found", apple2.GetURL()) } } func TestTopologyChildren(t *testing.T) { - gateways := []*Gateway{{Gateway: BuildGateway()}} - httpRoute := &HTTPRoute{HTTPRoute: BuildHTTPRoute()} - httpRouteRules := HTTPRouteRulesFromHTTPRouteFunc(httpRoute, 0) + apple1 := &Apple{Name: "apple-1"} + apple2 := &Apple{Name: "apple-2"} + orange1 := &Orange{Name: "orange-1", Namespace: "my-namespace", AppleParents: []string{"apple-1", "apple-2"}} + orange2 := &Orange{Name: "orange-2", Namespace: "my-namespace", AppleParents: []string{"apple-2"}} topology := NewTopology( - WithTargetables(gateways...), - WithTargetables(httpRoute), - WithTargetables(httpRouteRules...), - WithLinks( - LinkGatewayToHTTPRouteFunc(gateways), - LinkHTTPRouteToHTTPRouteRuleFunc(), - ), + WithTargetables(apple1, apple2), + WithTargetables(orange1, orange2), + WithLinks(LinkApplesToOranges([]*Apple{apple1, apple2})), WithPolicies( - buildPolicy(func(policy *TestPolicy) { - policy.Spec.TargetRef = gwapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ - LocalPolicyTargetReference: gwapiv1alpha2.LocalPolicyTargetReference{ - Group: gwapiv1.GroupName, - Kind: "HTTPRoute", - Name: "my-http-route", - }, + buildFruitPolicy(func(policy *FruitPolicy) { + policy.Name = "policy-2" + policy.Spec.TargetRef = FruitPolicyTargetReference{ + Group: TestGroupName, + Kind: "Orange", + Name: "orange-1", } }), ), ) - children := topology.Children(httpRoute) + // apple-1 + children := topology.Children(apple1) if expected := 1; len(children) != expected { t.Errorf("expected %d child, got %d", expected, len(children)) } childURLs := lo.Map(children, MapTargetableToURLFunc) - for _, httpRouteRule := range httpRouteRules { - if !lo.Contains(childURLs, httpRouteRule.GetURL()) { - t.Errorf("expected child %s not found", httpRouteRule.GetURL()) - } + if !lo.Contains(childURLs, orange1.GetURL()) { + t.Errorf("expected child %s not found", orange1.GetURL()) + } + // apple-2 + children = topology.Children(apple2) + if expected := 2; len(children) != expected { + t.Errorf("expected %d child, got %d", expected, len(children)) + } + childURLs = lo.Map(children, MapTargetableToURLFunc) + if !lo.Contains(childURLs, orange1.GetURL()) { + t.Errorf("expected child %s not found", orange1.GetURL()) + } + if !lo.Contains(childURLs, orange2.GetURL()) { + t.Errorf("expected child %s not found", orange2.GetURL()) } } func TestTopologyPaths(t *testing.T) { - gateways := []*Gateway{{Gateway: BuildGateway()}} - httpRoutes := []*HTTPRoute{ - {HTTPRoute: BuildHTTPRoute(func(r *gwapiv1.HTTPRoute) { r.Name = "route-1" })}, - {HTTPRoute: BuildHTTPRoute(func(r *gwapiv1.HTTPRoute) { r.Name = "route-2" })}, + apples := []*Apple{{Name: "apple-1"}} + oranges := []*Orange{ + {Name: "orange-1", Namespace: "my-namespace", AppleParents: []string{"apple-1"}, ChildBananas: []string{"banana-1"}}, + {Name: "orange-2", Namespace: "my-namespace", AppleParents: []string{"apple-1"}, ChildBananas: []string{"banana-1"}}, } - services := []*Service{{Service: BuildService()}} + bananas := []*Banana{{Name: "banana-1"}} topology := NewTopology( - WithTargetables(gateways...), - WithTargetables(httpRoutes...), - WithTargetables(services...), + WithTargetables(apples...), + WithTargetables(oranges...), + WithTargetables(bananas...), WithLinks( - LinkGatewayToHTTPRouteFunc(gateways), - LinkHTTPRouteToServiceFunc(httpRoutes, false), + LinkApplesToOranges(apples), + LinkOrangesToBananas(oranges), + ), + WithPolicies( + buildFruitPolicy(func(policy *FruitPolicy) { + policy.Name = "policy-2" + policy.Spec.TargetRef = FruitPolicyTargetReference{ + Group: TestGroupName, + Kind: "Orange", + Name: "orange-1", + } + }), ), ) - testCases := []struct { name string from Targetable @@ -167,33 +170,33 @@ func TestTopologyPaths(t *testing.T) { }{ { name: "single path", - from: httpRoutes[0], - to: services[0], + from: oranges[0], + to: bananas[0], expectedPaths: [][]Targetable{ - {httpRoutes[0], services[0]}, + {oranges[0], bananas[0]}, }, }, { name: "multiple paths", - from: gateways[0], - to: services[0], + from: apples[0], + to: bananas[0], expectedPaths: [][]Targetable{ - {gateways[0], httpRoutes[0], services[0]}, - {gateways[0], httpRoutes[1], services[0]}, + {apples[0], oranges[0], bananas[0]}, + {apples[0], oranges[1], bananas[0]}, }, }, { name: "trivial path", - from: gateways[0], - to: gateways[0], + from: apples[0], + to: apples[0], expectedPaths: [][]Targetable{ - {gateways[0]}, + {apples[0]}, }, }, { name: "no path", - from: services[0], - to: gateways[0], + from: bananas[0], + to: apples[0], expectedPaths: [][]Targetable{}, }, } @@ -217,14 +220,16 @@ func TestTopologyPaths(t *testing.T) { } } -// TestGatewayAPITopology tests for a topology of Gateway API resources with the following scheme: -// -// GatewayClass -> Gateway -> Listener -> HTTPRoute -> HTTPRouteRule -> Service -> ServicePort -// ∟> ServicePort <- Service -func TestGatewayAPITopology(t *testing.T) { +type fruits struct { + apples []*Apple + oranges []*Orange + bananas []*Banana +} + +func TestFruitTopology(t *testing.T) { testCases := []struct { name string - targetables GatewayAPIResources + targetables fruits policies []Policy expectedLinks map[string][]string }{ @@ -233,143 +238,60 @@ func TestGatewayAPITopology(t *testing.T) { }, { name: "single node", - targetables: GatewayAPIResources{ - GatewayClasses: []*gwapiv1.GatewayClass{BuildGatewayClass()}, + targetables: fruits{ + apples: []*Apple{{Name: "my-apple"}}, }, }, { - name: "one of each kind", - targetables: GatewayAPIResources{ - GatewayClasses: []*gwapiv1.GatewayClass{BuildGatewayClass()}, - Gateways: []*gwapiv1.Gateway{BuildGateway()}, - HTTPRoutes: []*gwapiv1.HTTPRoute{BuildHTTPRoute()}, - Services: []*core.Service{BuildService()}, + name: "multiple gvk", + targetables: fruits{ + apples: []*Apple{{Name: "my-apple"}}, + oranges: []*Orange{{Name: "my-orange", Namespace: "my-namespace", AppleParents: []string{"my-apple"}}}, }, - policies: []Policy{buildPolicy()}, + policies: []Policy{buildFruitPolicy()}, expectedLinks: map[string][]string{ - "my-gateway-class": {"my-gateway"}, - "my-gateway": {"my-gateway#my-listener"}, - "my-gateway#my-listener": {"my-http-route"}, - "my-http-route": {"my-http-route#rule-1"}, - "my-http-route#rule-1": {"my-service"}, - "my-service": {"my-service#http"}, + "my-apple": {"my-orange"}, }, }, { - name: "policies with section names", - targetables: GatewayAPIResources{ - Gateways: []*gwapiv1.Gateway{BuildGateway(func(gateway *gwapiv1.Gateway) { - gateway.Spec.Listeners[0].Name = "http" - gateway.Spec.Listeners = append(gateway.Spec.Listeners, gwapiv1.Listener{ - Name: "https", - Port: 443, - Protocol: "HTTPS", - }) - })}, + name: "complex topology", + targetables: fruits{ + apples: []*Apple{{Name: "apple-1"}, {Name: "apple-2"}}, + oranges: []*Orange{ + {Name: "orange-1", Namespace: "my-namespace", AppleParents: []string{"apple-1", "apple-2"}, ChildBananas: []string{"banana-1", "banana-2"}}, + {Name: "orange-2", Namespace: "my-namespace", AppleParents: []string{"apple-2"}, ChildBananas: []string{"banana-2", "banana-3"}}, + }, + bananas: []*Banana{{Name: "banana-1"}, {Name: "banana-2"}, {Name: "banana-3"}}, }, policies: []Policy{ - buildPolicy(func(policy *TestPolicy) { - policy.Name = "my-policy-1" - policy.Spec.TargetRef = gwapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ - LocalPolicyTargetReference: gwapiv1alpha2.LocalPolicyTargetReference{ - Group: gwapiv1.GroupName, - Kind: "Gateway", - Name: "my-gateway", - }, - SectionName: ptr.To(gwapiv1.SectionName("http")), - } + buildFruitPolicy(func(policy *FruitPolicy) { + policy.Name = "policy-1" + policy.Spec.TargetRef.Kind = "Apple" + policy.Spec.TargetRef.Name = "apple-1" }), - buildPolicy(func(policy *TestPolicy) { - policy.Name = "my-policy-2" - policy.Spec.TargetRef = gwapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ - LocalPolicyTargetReference: gwapiv1alpha2.LocalPolicyTargetReference{ - Group: gwapiv1.GroupName, - Kind: "Gateway", - Name: "my-gateway", - }, - SectionName: ptr.To(gwapiv1.SectionName("https")), - } + buildFruitPolicy(func(policy *FruitPolicy) { + policy.Name = "policy-2" + policy.Spec.TargetRef.Kind = "Orange" + policy.Spec.TargetRef.Name = "orange-2" }), }, expectedLinks: map[string][]string{ - "my-gateway": {"my-gateway#http", "my-gateway#https"}, - }, - }, - { - name: "complex topology", - targetables: BuildComplexGatewayAPITopology(), - expectedLinks: map[string][]string{ - "gatewayclass-1": {"gateway-1", "gateway-2", "gateway-3"}, - "gatewayclass-2": {"gateway-4", "gateway-5"}, - "gateway-1": {"gateway-1#listener-1", "gateway-1#listener-2"}, - "gateway-2": {"gateway-2#listener-1"}, - "gateway-3": {"gateway-3#listener-1", "gateway-3#listener-2"}, - "gateway-4": {"gateway-4#listener-1", "gateway-4#listener-2"}, - "gateway-5": {"gateway-5#listener-1"}, - "gateway-1#listener-1": {"route-1"}, - "gateway-1#listener-2": {"route-1", "route-2"}, - "gateway-2#listener-1": {"route-2", "route-3"}, - "gateway-3#listener-1": {"route-4", "route-5"}, - "gateway-3#listener-2": {"route-4", "route-5"}, - "gateway-4#listener-1": {"route-5", "route-6"}, - "gateway-4#listener-2": {"route-5", "route-6"}, - "gateway-5#listener-1": {"route-7"}, - "route-1": {"route-1#rule-1", "route-1#rule-2"}, - "route-2": {"route-2#rule-1"}, - "route-3": {"route-3#rule-1"}, - "route-4": {"route-4#rule-1", "route-4#rule-2"}, - "route-5": {"route-5#rule-1", "route-5#rule-2"}, - "route-6": {"route-6#rule-1", "route-6#rule-2"}, - "route-7": {"route-7#rule-1"}, - "route-1#rule-1": {"service-1"}, - "route-1#rule-2": {"service-2"}, - "route-2#rule-1": {"service-3#port-1"}, - "route-3#rule-1": {"service-3#port-1"}, - "route-4#rule-1": {"service-3#port-2"}, - "route-4#rule-2": {"service-4#port-1"}, - "route-5#rule-1": {"service-5"}, - "route-5#rule-2": {"service-5"}, - "route-6#rule-1": {"service-5", "service-6"}, - "route-6#rule-2": {"service-6#port-1"}, - "route-7#rule-1": {"service-7"}, - "service-1": {"service-1#port-1", "service-1#port-2"}, - "service-2": {"service-2#port-1"}, - "service-3": {"service-3#port-1", "service-3#port-2"}, - "service-4": {"service-4#port-1"}, - "service-5": {"service-5#port-1"}, - "service-6": {"service-6#port-1"}, - "service-7": {"service-7#port-1"}, + "apple-1": {"orange-1"}, + "apple-2": {"orange-1", "orange-2"}, + "orange-1": {"banana-1", "banana-2"}, + "orange-2": {"banana-2", "banana-3"}, }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - gatewayClasses := lo.Map(tc.targetables.GatewayClasses, func(gatewayClass *gwapiv1.GatewayClass, _ int) *GatewayClass { - return &GatewayClass{GatewayClass: gatewayClass} - }) - gateways := lo.Map(tc.targetables.Gateways, func(gateway *gwapiv1.Gateway, _ int) *Gateway { return &Gateway{Gateway: gateway} }) - listeners := lo.FlatMap(gateways, ListenersFromGatewayFunc) - httpRoutes := lo.Map(tc.targetables.HTTPRoutes, func(httpRoute *gwapiv1.HTTPRoute, _ int) *HTTPRoute { return &HTTPRoute{HTTPRoute: httpRoute} }) - httpRouteRules := lo.FlatMap(httpRoutes, HTTPRouteRulesFromHTTPRouteFunc) - services := lo.Map(tc.targetables.Services, func(service *core.Service, _ int) *Service { return &Service{Service: service} }) - servicePorts := lo.FlatMap(services, ServicePortsFromBackendFunc) - topology := NewTopology( - WithTargetables(gatewayClasses...), - WithTargetables(gateways...), - WithTargetables(listeners...), - WithTargetables(httpRoutes...), - WithTargetables(httpRouteRules...), - WithTargetables(services...), - WithTargetables(servicePorts...), + WithTargetables(tc.targetables.apples...), + WithTargetables(tc.targetables.oranges...), + WithTargetables(tc.targetables.bananas...), WithLinks( - LinkGatewayClassToGatewayFunc(gatewayClasses), - LinkGatewayToListenerFunc(), - LinkListenerToHTTPRouteFunc(gateways, listeners), - LinkHTTPRouteToHTTPRouteRuleFunc(), - LinkHTTPRouteRuleToServiceFunc(httpRouteRules, true), - LinkHTTPRouteRuleToServicePortFunc(httpRouteRules), - LinkServiceToServicePortFunc(), + LinkApplesToOranges(tc.targetables.apples), + LinkOrangesToBananas(tc.targetables.oranges), ), WithPolicies(tc.policies...), ) @@ -387,175 +309,7 @@ func TestGatewayAPITopology(t *testing.T) { } } - saveTestCaseOutput(t, topology.ToDot()) + SaveToOutputDir(t, topology.ToDot(), "../tests/out", ".dot") }) } } - -// TestGatewayAPITopologyWithoutSectionName tests for a simplified topology of Gateway API resources without -// section names, i.e. where HTTPRoutes are not expanded to link to specific Listeners, and Policy TargetRefs -// are not of LocalPolicyTargetReferenceWithSectionName kind. This results in the following scheme: -// -// GatewayClass -> Gateway -> HTTPRoute -> Service -func TestGatewayAPITopologyWithoutSectionName(t *testing.T) { - testCases := []struct { - name string - targetables GatewayAPIResources - policies []Policy - expectedLinks map[string][]string - }{ - { - name: "one of each kind", - targetables: GatewayAPIResources{ - GatewayClasses: []*gwapiv1.GatewayClass{BuildGatewayClass()}, - Gateways: []*gwapiv1.Gateway{BuildGateway()}, - HTTPRoutes: []*gwapiv1.HTTPRoute{BuildHTTPRoute()}, - Services: []*core.Service{BuildService()}, - }, - policies: []Policy{buildPolicy()}, - expectedLinks: map[string][]string{ - "my-gateway-class": {"my-gateway"}, - "my-gateway": {"my-http-route"}, - "my-http-route": {"my-service"}, - }, - }, - { - name: "complex topology", - targetables: BuildComplexGatewayAPITopology(), - expectedLinks: map[string][]string{ - "gatewayclass-1": {"gateway-1", "gateway-2", "gateway-3"}, - "gatewayclass-2": {"gateway-4", "gateway-5"}, - "gateway-1": {"route-1", "route-2"}, - "gateway-2": {"route-2", "route-3"}, - "gateway-3": {"route-4", "route-5"}, - "gateway-4": {"route-5", "route-6"}, - "gateway-5": {"route-7"}, - "route-1": {"service-1", "service-2"}, - "route-2": {"service-3"}, - "route-3": {"service-3"}, - "route-4": {"service-3", "service-4"}, - "route-5": {"service-5"}, - "route-6": {"service-5", "service-6"}, - "route-7": {"service-7"}, - }, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - gatewayClasses := lo.Map(tc.targetables.GatewayClasses, func(gatewayClass *gwapiv1.GatewayClass, _ int) *GatewayClass { - return &GatewayClass{GatewayClass: gatewayClass} - }) - gateways := lo.Map(tc.targetables.Gateways, func(gateway *gwapiv1.Gateway, _ int) *Gateway { return &Gateway{Gateway: gateway} }) - httpRoutes := lo.Map(tc.targetables.HTTPRoutes, func(httpRoute *gwapiv1.HTTPRoute, _ int) *HTTPRoute { return &HTTPRoute{HTTPRoute: httpRoute} }) - services := lo.Map(tc.targetables.Services, func(service *core.Service, _ int) *Service { return &Service{Service: service} }) - - topology := NewTopology( - WithTargetables(gatewayClasses...), - WithTargetables(gateways...), - WithTargetables(httpRoutes...), - WithTargetables(services...), - WithLinks( - LinkGatewayClassToGatewayFunc(gatewayClasses), - LinkGatewayToHTTPRouteFunc(gateways), - LinkHTTPRouteToServiceFunc(httpRoutes, false), - ), - WithPolicies(tc.policies...), - ) - - links := make(map[string][]string) - for _, root := range topology.Roots() { - linksFromNode(topology, root, links) - } - for from, tos := range links { - expectedTos := tc.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) - } - } - - saveTestCaseOutput(t, topology.ToDot()) - }) - } -} - -type TestPolicy struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec TestPolicySpec `json:"spec"` -} - -var _ Policy = &TestPolicy{} - -func (p *TestPolicy) GetURL() string { - return UrlFromObject(p) -} - -func (p *TestPolicy) GetTargetRefs() []PolicyTargetReference { - return []PolicyTargetReference{LocalPolicyTargetReferenceWithSectionName{LocalPolicyTargetReferenceWithSectionName: p.Spec.TargetRef, PolicyNamespace: p.Namespace}} -} - -func (p *TestPolicy) GetMergeStrategy() MergeStrategy { - return DefaultMergeStrategy -} - -func (p *TestPolicy) Merge(policy Policy) Policy { - return &TestPolicy{ - Spec: p.Spec, - } -} - -type TestPolicySpec struct { - TargetRef gwapiv1alpha2.LocalPolicyTargetReferenceWithSectionName `json:"targetRef"` -} - -func buildPolicy(f ...func(*TestPolicy)) *TestPolicy { - p := &TestPolicy{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "test/v1", - Kind: "TestPolicy", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "my-policy", - Namespace: "my-namespace", - }, - Spec: TestPolicySpec{ - TargetRef: gwapiv1alpha2.LocalPolicyTargetReferenceWithSectionName{ - LocalPolicyTargetReference: gwapiv1alpha2.LocalPolicyTargetReference{ - Kind: "Service", - Name: "my-service", - }, - }, - }, - } - for _, fn := range f { - fn(p) - } - return p -} - -func linksFromNode(topology *Topology, node Targetable, edges map[string][]string) { - if _, ok := edges[node.GetName()]; ok { - return - } - children := topology.Children(node) - edges[node.GetName()] = lo.Map(children, func(child Targetable, _ int) string { return child.GetName() }) - for _, child := range children { - linksFromNode(topology, child, edges) - } -} - -// saveTestCaseOutput saves the output of a test case to a file in the output directory. -func saveTestCaseOutput(t *testing.T, out *bytes.Buffer) { - file, err := os.Create(fmt.Sprintf("%s/%s.dot", outDir, strings.ReplaceAll(t.Name(), "/", "__"))) - if err != nil { - t.Fatal(err) - } - defer file.Close() - _, err = file.Write(out.Bytes()) - if err != nil { - t.Fatal(err) - } -} diff --git a/machinery/types.go b/machinery/types.go new file mode 100644 index 0000000..a8e7570 --- /dev/null +++ b/machinery/types.go @@ -0,0 +1,67 @@ +package machinery + +import ( + "fmt" + "strings" + + "k8s.io/apimachinery/pkg/runtime/schema" + k8stypes "k8s.io/apimachinery/pkg/types" +) + +const kindNameURLSeparator = ':' + +type Object interface { + schema.ObjectKind + + GetNamespace() string + GetName() string + GetURL() string +} + +func UrlFromObject(obj Object) string { + name := strings.TrimPrefix(namespacedName(obj.GetNamespace(), obj.GetName()), string(k8stypes.Separator)) + return fmt.Sprintf("%s%s%s", strings.ToLower(obj.GroupVersionKind().GroupKind().String()), string(kindNameURLSeparator), name) +} + +func namespacedName(namespace, name string) string { + return k8stypes.NamespacedName{Namespace: namespace, Name: name}.String() +} + +// Targetable is an interface that represents an object that can be targeted by policies. +type Targetable interface { + Object + + SetPolicies([]Policy) + Policies() []Policy +} + +func MapTargetableToURLFunc(t Targetable, _ int) string { + return t.GetURL() +} + +// Policy targets objects and can be merged with another Policy based on a given MergeStrategy. +type Policy interface { + Object + + GetTargetRefs() []PolicyTargetReference + GetMergeStrategy() MergeStrategy + + Merge(Policy) Policy +} + +// PolicyTargetReference is a generic interface for all kinds of Gateway API policy target references. +// It implements the Object interface for the referent. +type PolicyTargetReference interface { + Object +} + +// MergeStrategy is a function that merges two Policy objects into a new Policy object. +type MergeStrategy func(Policy, Policy) Policy + +var DefaultMergeStrategy = NoMergeStrategy + +func NoMergeStrategy(_, target Policy) Policy { + return target +} + +var _ MergeStrategy = NoMergeStrategy