diff --git a/apis/v1alpha1/clientsettingspolicy_types.go b/apis/v1alpha1/clientsettingspolicy_types.go index a01032af4e..541b496bbc 100644 --- a/apis/v1alpha1/clientsettingspolicy_types.go +++ b/apis/v1alpha1/clientsettingspolicy_types.go @@ -48,8 +48,11 @@ type ClientSettingsPolicySpec struct { // TargetRef identifies an API object to apply the policy to. // Object must be in the same namespace as the policy. + // Support: Gateway, HTTPRoute, GRPCRoute. // - // Support: Gateway, HTTPRoute + // +kubebuilder:validation:XValidation:message="TargetRef Kind must be one of: Gateway, HTTPRoute, or GRPCRoute",rule="(self.kind=='Gateway' || self.kind=='HTTPRoute' || self.kind=='GRPCRoute')" + // +kubebuilder:validation:XValidation:message="TargetRef Group must be gateway.networking.k8s.io.",rule="(self.group=='gateway.networking.k8s.io')" + //nolint:lll TargetRef gatewayv1alpha2.LocalPolicyTargetReference `json:"targetRef"` } @@ -95,7 +98,11 @@ type ClientKeepAlive struct { // Timeout defines the keep-alive timeouts for clients. // + // +kubebuilder:validation:XValidation:message="header can only be specified if server is specified",rule="!(has(self.header) && !has(self.server))" + // + // // +optional + //nolint:lll Timeout *ClientKeepAliveTimeout `json:"timeout,omitempty"` } diff --git a/apis/v1alpha1/policy_methods.go b/apis/v1alpha1/policy_methods.go new file mode 100644 index 0000000000..e4bb439112 --- /dev/null +++ b/apis/v1alpha1/policy_methods.go @@ -0,0 +1,21 @@ +package v1alpha1 + +import ( + "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +// FIXME(kate-osborn): https://github.com/nginxinc/nginx-gateway-fabric/issues/1939. +// Figure out a way to generate these methods for all our policies. +// These methods implement the policies.Policy interface which extends client.Object to add the following methods. + +func (p *ClientSettingsPolicy) GetTargetRef() v1alpha2.LocalPolicyTargetReference { + return p.Spec.TargetRef +} + +func (p *ClientSettingsPolicy) GetPolicyStatus() v1alpha2.PolicyStatus { + return p.Status +} + +func (p *ClientSettingsPolicy) SetPolicyStatus(status v1alpha2.PolicyStatus) { + p.Status = status +} diff --git a/charts/nginx-gateway-fabric/templates/deployment.yaml b/charts/nginx-gateway-fabric/templates/deployment.yaml index 83aeafde21..1e5fdf3145 100644 --- a/charts/nginx-gateway-fabric/templates/deployment.yaml +++ b/charts/nginx-gateway-fabric/templates/deployment.yaml @@ -124,6 +124,8 @@ spec: mountPath: /etc/nginx/secrets - name: nginx-run mountPath: /var/run/nginx + - name: nginx-includes + mountPath: /etc/nginx/includes {{- with .Values.nginxGateway.extraVolumeMounts -}} {{ toYaml . | nindent 8 }} {{- end }} @@ -161,6 +163,8 @@ spec: mountPath: /var/cache/nginx - name: nginx-lib mountPath: /var/lib/nginx + - name: nginx-includes + mountPath: /etc/nginx/includes {{- with .Values.nginx.extraVolumeMounts -}} {{ toYaml . | nindent 8 }} {{- end }} @@ -195,6 +199,8 @@ spec: emptyDir: {} - name: nginx-lib emptyDir: {} + - name: nginx-includes + emptyDir: {} {{- with .Values.extraVolumes -}} {{ toYaml . | nindent 6 }} {{- end }} diff --git a/charts/nginx-gateway-fabric/templates/rbac.yaml b/charts/nginx-gateway-fabric/templates/rbac.yaml index 56e3adb3f2..cc12735ae7 100644 --- a/charts/nginx-gateway-fabric/templates/rbac.yaml +++ b/charts/nginx-gateway-fabric/templates/rbac.yaml @@ -121,6 +121,7 @@ rules: - gateway.nginx.org resources: - nginxproxies + - clientsettingspolicies verbs: - list - watch @@ -128,6 +129,7 @@ rules: - gateway.nginx.org resources: - nginxgateways/status + - clientsettingspolicies/status verbs: - update {{- if .Values.nginxGateway.leaderElection.enable }} diff --git a/config/crd/bases/gateway.nginx.org_clientsettingspolicies.yaml b/config/crd/bases/gateway.nginx.org_clientsettingspolicies.yaml index 3f5728a7dc..13b5cf9107 100644 --- a/config/crd/bases/gateway.nginx.org_clientsettingspolicies.yaml +++ b/config/crd/bases/gateway.nginx.org_clientsettingspolicies.yaml @@ -108,14 +108,15 @@ spec: pattern: ^\d{1,4}(ms|s)?$ type: string type: object + x-kubernetes-validations: + - message: header can only be specified if server is specified + rule: '!(has(self.header) && !has(self.server))' type: object targetRef: description: |- TargetRef identifies an API object to apply the policy to. Object must be in the same namespace as the policy. - - - Support: Gateway, HTTPRoute + Support: Gateway, HTTPRoute, GRPCRoute. properties: group: description: Group is the group of the target resource. @@ -138,6 +139,12 @@ spec: - kind - name type: object + x-kubernetes-validations: + - message: 'TargetRef Kind must be one of: Gateway, HTTPRoute, or + GRPCRoute' + rule: (self.kind=='Gateway' || self.kind=='HTTPRoute' || self.kind=='GRPCRoute') + - message: TargetRef Group must be gateway.networking.k8s.io. + rule: (self.group=='gateway.networking.k8s.io') required: - targetRef type: object diff --git a/conformance/provisioner/static-deployment.yaml b/conformance/provisioner/static-deployment.yaml index dc59c6a929..7f5fed615d 100644 --- a/conformance/provisioner/static-deployment.yaml +++ b/conformance/provisioner/static-deployment.yaml @@ -76,6 +76,8 @@ spec: mountPath: /etc/nginx/secrets - name: nginx-run mountPath: /var/run/nginx + - name: nginx-includes + mountPath: /etc/nginx/includes - image: ghcr.io/nginxinc/nginx-gateway-fabric/nginx:edge imagePullPolicy: Always name: nginx @@ -106,6 +108,8 @@ spec: mountPath: /var/cache/nginx - name: nginx-lib mountPath: /var/lib/nginx + - name: nginx-includes + mountPath: /etc/nginx/includes terminationGracePeriodSeconds: 30 serviceAccountName: nginx-gateway shareProcessNamespace: true @@ -125,3 +129,5 @@ spec: emptyDir: {} - name: nginx-lib emptyDir: {} + - name: nginx-includes + emptyDir: {} diff --git a/deploy/crds.yaml b/deploy/crds.yaml index 5289c855f4..82e793e4b8 100644 --- a/deploy/crds.yaml +++ b/deploy/crds.yaml @@ -107,14 +107,15 @@ spec: pattern: ^\d{1,4}(ms|s)?$ type: string type: object + x-kubernetes-validations: + - message: header can only be specified if server is specified + rule: '!(has(self.header) && !has(self.server))' type: object targetRef: description: |- TargetRef identifies an API object to apply the policy to. Object must be in the same namespace as the policy. - - - Support: Gateway, HTTPRoute + Support: Gateway, HTTPRoute, GRPCRoute. properties: group: description: Group is the group of the target resource. @@ -137,6 +138,12 @@ spec: - kind - name type: object + x-kubernetes-validations: + - message: 'TargetRef Kind must be one of: Gateway, HTTPRoute, or + GRPCRoute' + rule: (self.kind=='Gateway' || self.kind=='HTTPRoute' || self.kind=='GRPCRoute') + - message: TargetRef Group must be gateway.networking.k8s.io. + rule: (self.group=='gateway.networking.k8s.io') required: - targetRef type: object diff --git a/deploy/manifests/nginx-gateway-experimental.yaml b/deploy/manifests/nginx-gateway-experimental.yaml index 15db4a72b3..b0e068ebb7 100644 --- a/deploy/manifests/nginx-gateway-experimental.yaml +++ b/deploy/manifests/nginx-gateway-experimental.yaml @@ -103,6 +103,7 @@ rules: - gateway.nginx.org resources: - nginxproxies + - clientsettingspolicies verbs: - list - watch @@ -110,6 +111,7 @@ rules: - gateway.nginx.org resources: - nginxgateways/status + - clientsettingspolicies/status verbs: - update - apiGroups: @@ -228,6 +230,8 @@ spec: mountPath: /etc/nginx/secrets - name: nginx-run mountPath: /var/run/nginx + - name: nginx-includes + mountPath: /etc/nginx/includes - image: ghcr.io/nginxinc/nginx-gateway-fabric/nginx:edge imagePullPolicy: Always name: nginx @@ -258,6 +262,8 @@ spec: mountPath: /var/cache/nginx - name: nginx-lib mountPath: /var/lib/nginx + - name: nginx-includes + mountPath: /etc/nginx/includes terminationGracePeriodSeconds: 30 serviceAccountName: nginx-gateway shareProcessNamespace: true @@ -277,6 +283,8 @@ spec: emptyDir: {} - name: nginx-lib emptyDir: {} + - name: nginx-includes + emptyDir: {} --- # Source: nginx-gateway-fabric/templates/gatewayclass.yaml apiVersion: gateway.networking.k8s.io/v1 diff --git a/deploy/manifests/nginx-gateway.yaml b/deploy/manifests/nginx-gateway.yaml index 8dc5b26591..021b24088d 100644 --- a/deploy/manifests/nginx-gateway.yaml +++ b/deploy/manifests/nginx-gateway.yaml @@ -100,6 +100,7 @@ rules: - gateway.nginx.org resources: - nginxproxies + - clientsettingspolicies verbs: - list - watch @@ -107,6 +108,7 @@ rules: - gateway.nginx.org resources: - nginxgateways/status + - clientsettingspolicies/status verbs: - update - apiGroups: @@ -224,6 +226,8 @@ spec: mountPath: /etc/nginx/secrets - name: nginx-run mountPath: /var/run/nginx + - name: nginx-includes + mountPath: /etc/nginx/includes - image: ghcr.io/nginxinc/nginx-gateway-fabric/nginx:edge imagePullPolicy: Always name: nginx @@ -254,6 +258,8 @@ spec: mountPath: /var/cache/nginx - name: nginx-lib mountPath: /var/lib/nginx + - name: nginx-includes + mountPath: /etc/nginx/includes terminationGracePeriodSeconds: 30 serviceAccountName: nginx-gateway shareProcessNamespace: true @@ -273,6 +279,8 @@ spec: emptyDir: {} - name: nginx-lib emptyDir: {} + - name: nginx-includes + emptyDir: {} --- # Source: nginx-gateway-fabric/templates/gatewayclass.yaml apiVersion: gateway.networking.k8s.io/v1 diff --git a/deploy/manifests/nginx-plus-gateway-experimental.yaml b/deploy/manifests/nginx-plus-gateway-experimental.yaml index 81743dcee2..c5b61cdc6d 100644 --- a/deploy/manifests/nginx-plus-gateway-experimental.yaml +++ b/deploy/manifests/nginx-plus-gateway-experimental.yaml @@ -109,6 +109,7 @@ rules: - gateway.nginx.org resources: - nginxproxies + - clientsettingspolicies verbs: - list - watch @@ -116,6 +117,7 @@ rules: - gateway.nginx.org resources: - nginxgateways/status + - clientsettingspolicies/status verbs: - update - apiGroups: @@ -235,6 +237,8 @@ spec: mountPath: /etc/nginx/secrets - name: nginx-run mountPath: /var/run/nginx + - name: nginx-includes + mountPath: /etc/nginx/includes - image: nginx-gateway-fabric/nginx-plus:edge imagePullPolicy: Always name: nginx @@ -265,6 +269,8 @@ spec: mountPath: /var/cache/nginx - name: nginx-lib mountPath: /var/lib/nginx + - name: nginx-includes + mountPath: /etc/nginx/includes terminationGracePeriodSeconds: 30 serviceAccountName: nginx-gateway shareProcessNamespace: true @@ -284,6 +290,8 @@ spec: emptyDir: {} - name: nginx-lib emptyDir: {} + - name: nginx-includes + emptyDir: {} --- # Source: nginx-gateway-fabric/templates/gatewayclass.yaml apiVersion: gateway.networking.k8s.io/v1 diff --git a/deploy/manifests/nginx-plus-gateway.yaml b/deploy/manifests/nginx-plus-gateway.yaml index 04736bb305..b2f0a1e3e8 100644 --- a/deploy/manifests/nginx-plus-gateway.yaml +++ b/deploy/manifests/nginx-plus-gateway.yaml @@ -106,6 +106,7 @@ rules: - gateway.nginx.org resources: - nginxproxies + - clientsettingspolicies verbs: - list - watch @@ -113,6 +114,7 @@ rules: - gateway.nginx.org resources: - nginxgateways/status + - clientsettingspolicies/status verbs: - update - apiGroups: @@ -231,6 +233,8 @@ spec: mountPath: /etc/nginx/secrets - name: nginx-run mountPath: /var/run/nginx + - name: nginx-includes + mountPath: /etc/nginx/includes - image: nginx-gateway-fabric/nginx-plus:edge imagePullPolicy: Always name: nginx @@ -261,6 +265,8 @@ spec: mountPath: /var/cache/nginx - name: nginx-lib mountPath: /var/lib/nginx + - name: nginx-includes + mountPath: /etc/nginx/includes terminationGracePeriodSeconds: 30 serviceAccountName: nginx-gateway shareProcessNamespace: true @@ -280,6 +286,8 @@ spec: emptyDir: {} - name: nginx-lib emptyDir: {} + - name: nginx-includes + emptyDir: {} --- # Source: nginx-gateway-fabric/templates/gatewayclass.yaml apiVersion: gateway.networking.k8s.io/v1 diff --git a/docs/proposals/client-settings.md b/docs/proposals/client-settings.md index 35ca6913de..5437c88f50 100644 --- a/docs/proposals/client-settings.md +++ b/docs/proposals/client-settings.md @@ -1,7 +1,7 @@ # Enhancement Proposal-1632: Client Settings Policy - Issue: https://github.com/nginxinc/nginx-gateway-fabric/issues/1632 -- Status: Implementable +- Status: Completed ## Summary diff --git a/examples/client-settings-policy/README.md b/examples/client-settings-policy/README.md new file mode 100644 index 0000000000..d3d7c87f0f --- /dev/null +++ b/examples/client-settings-policy/README.md @@ -0,0 +1,3 @@ +# Client Settings Policy + +This directory contains YAML files of ClientSettingsPolicies. diff --git a/examples/client-settings-policy/cafe-routes.yaml b/examples/client-settings-policy/cafe-routes.yaml new file mode 100644 index 0000000000..67927335cb --- /dev/null +++ b/examples/client-settings-policy/cafe-routes.yaml @@ -0,0 +1,37 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: coffee +spec: + parentRefs: + - name: gateway + sectionName: http + hostnames: + - "cafe.example.com" + rules: + - matches: + - path: + type: PathPrefix + value: /coffee + backendRefs: + - name: coffee + port: 80 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: tea +spec: + parentRefs: + - name: gateway + sectionName: http + hostnames: + - "cafe.example.com" + rules: + - matches: + - path: + type: Exact + value: /tea + backendRefs: + - name: tea + port: 80 diff --git a/examples/client-settings-policy/cafe.yaml b/examples/client-settings-policy/cafe.yaml new file mode 100644 index 0000000000..2d03ae59ff --- /dev/null +++ b/examples/client-settings-policy/cafe.yaml @@ -0,0 +1,65 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: coffee +spec: + replicas: 1 + selector: + matchLabels: + app: coffee + template: + metadata: + labels: + app: coffee + spec: + containers: + - name: coffee + image: nginxdemos/nginx-hello:plain-text + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: coffee +spec: + ports: + - port: 80 + targetPort: 8080 + protocol: TCP + name: http + selector: + app: coffee +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tea +spec: + replicas: 1 + selector: + matchLabels: + app: tea + template: + metadata: + labels: + app: tea + spec: + containers: + - name: tea + image: nginxdemos/nginx-hello:plain-text + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: tea +spec: + ports: + - port: 80 + targetPort: 8080 + protocol: TCP + name: http + selector: + app: tea diff --git a/examples/client-settings-policy/csp-gateway.yaml b/examples/client-settings-policy/csp-gateway.yaml new file mode 100644 index 0000000000..991d2b9604 --- /dev/null +++ b/examples/client-settings-policy/csp-gateway.yaml @@ -0,0 +1,19 @@ +apiVersion: gateway.nginx.org/v1alpha1 +kind: ClientSettingsPolicy +metadata: + name: gw + namespace: default +spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway + body: + maxSize: 10m + timeout: 30s + keepAlive: + requests: 100 + time: 5s + timeout: + server: 2s + header: 1s diff --git a/examples/client-settings-policy/csp-grpcroute.yaml b/examples/client-settings-policy/csp-grpcroute.yaml new file mode 100644 index 0000000000..24139aa7ab --- /dev/null +++ b/examples/client-settings-policy/csp-grpcroute.yaml @@ -0,0 +1,14 @@ +# This example should be used in conjunction with +# the GRPC example: https://github.com/nginxinc/nginx-gateway-fabric/tree/main/examples/grpc-routing example. +apiVersion: gateway.nginx.org/v1alpha1 +kind: ClientSettingsPolicy +metadata: + name: grcp-backend-v1-route + namespace: default +spec: + targetRef: + group: gateway.networking.k8s.io + kind: GRPCRoute + name: backend-v1 + body: + maxSize: "0" # setting to 0 disables checking of the body size diff --git a/examples/client-settings-policy/csp-httproutes.yaml b/examples/client-settings-policy/csp-httproutes.yaml new file mode 100644 index 0000000000..1419c7b278 --- /dev/null +++ b/examples/client-settings-policy/csp-httproutes.yaml @@ -0,0 +1,38 @@ +apiVersion: gateway.nginx.org/v1alpha1 +kind: ClientSettingsPolicy +metadata: + name: tea-route-max-body-size + namespace: default +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: tea + body: + maxSize: 800m +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: ClientSettingsPolicy +metadata: + name: coffee-route-max-body-size + namespace: default +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: coffee + body: + maxSize: 5m +--- +apiVersion: gateway.nginx.org/v1alpha1 +kind: ClientSettingsPolicy +metadata: + name: coffee-route-keepalive-requests + namespace: default +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: coffee + keepAlive: + requests: 100 diff --git a/examples/client-settings-policy/gateway.yaml b/examples/client-settings-policy/gateway.yaml new file mode 100644 index 0000000000..5404ac397c --- /dev/null +++ b/examples/client-settings-policy/gateway.yaml @@ -0,0 +1,15 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: gateway +spec: + gatewayClassName: nginx + listeners: + - name: http + port: 80 + protocol: HTTP + hostname: "*.example.com" + - name: http2 + port: 8080 + protocol: HTTP + hostname: "*.example.org" diff --git a/internal/framework/controller/register_test.go b/internal/framework/controller/register_test.go index d038e44bd1..3f6f01fe21 100644 --- a/internal/framework/controller/register_test.go +++ b/internal/framework/controller/register_test.go @@ -23,6 +23,7 @@ import ( "github.com/nginxinc/nginx-gateway-fabric/internal/framework/controller/controllerfakes" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/controller/index" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/controller/predicate" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" ) func TestRegister(t *testing.T) { @@ -54,7 +55,7 @@ func TestRegister(t *testing.T) { objectTypeWithGVK := &v1.HTTPRoute{} objectTypeWithGVK.SetGroupVersionKind( - schema.GroupVersionKind{Group: v1.GroupName, Version: "v1", Kind: "HTTPRoute"}, + schema.GroupVersionKind{Group: v1.GroupName, Version: "v1", Kind: kinds.HTTPRoute}, ) objectTypeNoGVK := &v1.HTTPRoute{} diff --git a/internal/framework/helpers/helpers.go b/internal/framework/helpers/helpers.go index 05d2333493..e7e950a103 100644 --- a/internal/framework/helpers/helpers.go +++ b/internal/framework/helpers/helpers.go @@ -2,7 +2,9 @@ package helpers import ( + "bytes" "fmt" + "text/template" "github.com/google/go-cmp/cmp" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -51,3 +53,37 @@ func MustCastObject[T client.Object](object client.Object) T { panic(fmt.Errorf("unexpected object type %T", object)) } + +// EqualPointers returns whether two pointers are equal. +// Pointers are considered equal if one of the following is true: +// - They are both nil. +// - One is nil and the other is empty (e.g. nil string and ""). +// - They are both non-nil, and their values are the same. +func EqualPointers[T comparable](p1, p2 *T) bool { + if p1 == nil && p2 == nil { + return true + } + + var p1Val, p2Val T + + if p1 != nil { + p1Val = *p1 + } + + if p2 != nil { + p2Val = *p2 + } + + return p1Val == p2Val +} + +// MustExecuteTemplate executes the template with the given data. +func MustExecuteTemplate(template *template.Template, data interface{}) []byte { + var buf bytes.Buffer + + if err := template.Execute(&buf, data); err != nil { + panic(err) + } + + return buf.Bytes() +} diff --git a/internal/framework/helpers/helpers_test.go b/internal/framework/helpers/helpers_test.go index c983832960..fbd1498b0b 100644 --- a/internal/framework/helpers/helpers_test.go +++ b/internal/framework/helpers/helpers_test.go @@ -1,13 +1,15 @@ -package helpers +package helpers_test import ( "testing" + "text/template" . "github.com/onsi/gomega" - "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" gatewayv1alpha3 "sigs.k8s.io/gateway-api/apis/v1alpha3" + + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" ) func TestMustCastObject(t *testing.T) { @@ -16,10 +18,89 @@ func TestMustCastObject(t *testing.T) { var obj client.Object = &gatewayv1.Gateway{} g.Expect(func() { - _ = MustCastObject[*gatewayv1.Gateway](obj) + _ = helpers.MustCastObject[*gatewayv1.Gateway](obj) }).ToNot(Panic()) g.Expect(func() { - _ = MustCastObject[*gatewayv1alpha3.BackendTLSPolicy](obj) + _ = helpers.MustCastObject[*gatewayv1alpha3.BackendTLSPolicy](obj) }).To(Panic()) } + +func TestEqualPointers(t *testing.T) { + tests := []struct { + p1 *string + p2 *string + name string + expEqual bool + }{ + { + name: "first pointer nil; second has non-empty value", + p1: nil, + p2: helpers.GetPointer("test"), + expEqual: false, + }, + { + name: "second pointer nil; first has non-empty value", + p1: helpers.GetPointer("test"), + p2: nil, + expEqual: false, + }, + { + name: "different values", + p1: helpers.GetPointer("test"), + p2: helpers.GetPointer("different"), + expEqual: false, + }, + { + name: "both pointers nil", + p1: nil, + p2: nil, + expEqual: true, + }, + { + name: "first pointer nil; second empty", + p1: nil, + p2: helpers.GetPointer(""), + expEqual: true, + }, + { + name: "second pointer nil; first empty", + p1: helpers.GetPointer(""), + p2: nil, + expEqual: true, + }, + { + name: "same value", + p1: helpers.GetPointer("test"), + p2: helpers.GetPointer("test"), + expEqual: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + val := helpers.EqualPointers(test.p1, test.p2) + g.Expect(val).To(Equal(test.expEqual)) + }) + } +} + +func TestMustExecuteTemplate(t *testing.T) { + g := NewWithT(t) + + tmpl := template.Must(template.New("test").Parse(`Hello {{.}}`)) + bytes := helpers.MustExecuteTemplate(tmpl, "you") + g.Expect(string(bytes)).To(Equal("Hello you")) +} + +func TestMustExecuteTemplatePanics(t *testing.T) { + g := NewWithT(t) + + execute := func() { + helpers.MustExecuteTemplate(nil, nil) + } + + g.Expect(execute).To(Panic()) +} diff --git a/internal/framework/kinds/kinds.go b/internal/framework/kinds/kinds.go new file mode 100644 index 0000000000..e67376ea3c --- /dev/null +++ b/internal/framework/kinds/kinds.go @@ -0,0 +1,46 @@ +package kinds + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" +) + +// Gateway API Kinds +const ( + // Gateway is the Gateway Kind + Gateway = "Gateway" + // GatewayClass is the GatewayClass Kind. + GatewayClass = "GatewayClass" + // HTTPRoute is the HTTPRoute kind. + HTTPRoute = "HTTPRoute" + // GRPCRoute is the GRPCRoute kind. + GRPCRoute = "GRPCRoute" +) + +// NGINX Gateway Fabric kinds. +const ( + // ClientSettingsPolicy is the ClientSettingsPolicy kind. + ClientSettingsPolicy = "ClientSettingsPolicy" + // NginxProxy is the NginxProxy kind. + NginxProxy = "NginxProxy" +) + +// MustExtractGVK is a function that extracts the GroupVersionKind (GVK) of a client.object. +// It will panic if the GKV cannot be extracted. +type MustExtractGVK func(object client.Object) schema.GroupVersionKind + +// NewMustExtractGKV creates a new MustExtractGVK function using the scheme. +func NewMustExtractGKV(scheme *runtime.Scheme) MustExtractGVK { + return func(obj client.Object) schema.GroupVersionKind { + gvk, err := apiutil.GVKForObject(obj, scheme) + if err != nil { + panic(fmt.Sprintf("could not extract GVK for object: %T", obj)) + } + + return gvk + } +} diff --git a/internal/framework/status/updater_test.go b/internal/framework/status/updater_test.go index 7c047e42cf..1b8c944e78 100644 --- a/internal/framework/status/updater_test.go +++ b/internal/framework/status/updater_test.go @@ -14,6 +14,7 @@ import ( v1 "sigs.k8s.io/gateway-api/apis/v1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" ) func createGC(name string) *v1.GatewayClass { @@ -22,7 +23,7 @@ func createGC(name string) *v1.GatewayClass { Name: name, }, TypeMeta: metav1.TypeMeta{ - Kind: "GatewayClass", + Kind: kinds.GatewayClass, APIVersion: "gateway.networking.k8s.io/v1", }, } diff --git a/internal/mode/static/handler.go b/internal/mode/static/handler.go index e96afb4054..9bfdc1daab 100644 --- a/internal/mode/static/handler.go +++ b/internal/mode/static/handler.go @@ -25,6 +25,7 @@ import ( ngxConfig "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/file" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/runtime" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/graph" @@ -49,14 +50,14 @@ type secretStorer interface { // eventHandlerConfig holds configuration parameters for eventHandlerImpl. type eventHandlerConfig struct { - // gatewayCtlrName is the name of the NGF controller. - gatewayCtlrName string - // k8sClient is a Kubernetes API client - k8sClient client.Client - // gatewayPodConfig contains information about this Pod. - gatewayPodConfig ngfConfig.GatewayPodConfig - // usageReportConfig contains the configuration for NGINX Plus usage reporting. - usageReportConfig *config.UsageReportConfig + // nginxFileMgr is the file Manager for nginx. + nginxFileMgr file.Manager + // metricsCollector collects metrics for this controller. + metricsCollector handlerMetricsCollector + // nginxRuntimeMgr manages nginx runtime. + nginxRuntimeMgr runtime.Manager + // statusUpdater updates statuses on Kubernetes resources. + statusUpdater frameworkStatus.GroupUpdater // usageSecret contains the Secret for the NGINX Plus reporting credentials. usageSecret secretStorer // processor is the state ChangeProcessor. @@ -65,22 +66,24 @@ type eventHandlerConfig struct { serviceResolver resolver.ServiceResolver // generator is the nginx config generator. generator ngxConfig.Generator - // nginxFileMgr is the file Manager for nginx. - nginxFileMgr file.Manager - // nginxRuntimeMgr manages nginx runtime. - nginxRuntimeMgr runtime.Manager - // statusUpdater updates statuses on Kubernetes resources. - statusUpdater frameworkStatus.GroupUpdater - // eventRecorder records events for Kubernetes resources. - eventRecorder record.EventRecorder + // policyConfigGenerator is the config generator for NGF policies. + policyConfigGenerator policies.ConfigGenerator + // k8sClient is a Kubernetes API client + k8sClient client.Client // logLevelSetter is used to update the logging level. logLevelSetter logLevelSetter - // metricsCollector collects metrics for this controller. - metricsCollector handlerMetricsCollector + // eventRecorder records events for Kubernetes resources. + eventRecorder record.EventRecorder + // usageReportConfig contains the configuration for NGINX Plus usage reporting. + usageReportConfig *config.UsageReportConfig // nginxConfiguredOnStartChecker sets the health of the Pod to Ready once we've written out our initial config. nginxConfiguredOnStartChecker *nginxConfiguredOnStartChecker + // gatewayPodConfig contains information about this Pod. + gatewayPodConfig ngfConfig.GatewayPodConfig // controlConfigNSName is the NamespacedName of the NginxGateway config for this controller. controlConfigNSName types.NamespacedName + // gatewayCtlrName is the name of the NGF controller. + gatewayCtlrName string // updateGatewayClassStatus enables updating the status of the GatewayClass resource. updateGatewayClassStatus bool } @@ -194,7 +197,7 @@ func (h *eventHandlerImpl) HandleEventBatch(ctx context.Context, logger logr.Log return case state.EndpointsOnlyChange: h.version++ - cfg := dataplane.BuildConfiguration(ctx, graph, h.cfg.serviceResolver, h.version) + cfg := dataplane.BuildConfiguration(ctx, graph, h.cfg.serviceResolver, h.cfg.policyConfigGenerator, h.version) h.setLatestConfiguration(&cfg) @@ -205,7 +208,7 @@ func (h *eventHandlerImpl) HandleEventBatch(ctx context.Context, logger logr.Log ) case state.ClusterStateChange: h.version++ - cfg := dataplane.BuildConfiguration(ctx, graph, h.cfg.serviceResolver, h.version) + cfg := dataplane.BuildConfiguration(ctx, graph, h.cfg.serviceResolver, h.cfg.policyConfigGenerator, h.version) h.setLatestConfiguration(&cfg) @@ -254,11 +257,13 @@ func (h *eventHandlerImpl) updateStatuses(ctx context.Context, logger logr.Logge ) polReqs := status.PrepareBackendTLSPolicyRequests(graph.BackendTLSPolicies, transitionTime, h.cfg.gatewayCtlrName) + ngfPolReqs := status.PrepareNGFPolicyRequests(graph.NGFPolicies, transitionTime, h.cfg.gatewayCtlrName) - reqs := make([]frameworkStatus.UpdateRequest, 0, len(gcReqs)+len(routeReqs)+len(polReqs)) + reqs := make([]frameworkStatus.UpdateRequest, 0, len(gcReqs)+len(routeReqs)+len(polReqs)+len(ngfPolReqs)) reqs = append(reqs, gcReqs...) reqs = append(reqs, routeReqs...) reqs = append(reqs, polReqs...) + reqs = append(reqs, ngfPolReqs...) h.cfg.statusUpdater.UpdateGroup(ctx, groupAllExceptGateways, reqs...) diff --git a/internal/mode/static/manager.go b/internal/mode/static/manager.go index 30191296eb..a11e670671 100644 --- a/internal/mode/static/manager.go +++ b/internal/mode/static/manager.go @@ -42,6 +42,7 @@ import ( "github.com/nginxinc/nginx-gateway-fabric/internal/framework/events" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/gatewayclass" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/runnables" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/status" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/config" @@ -50,6 +51,8 @@ import ( ngxvalidation "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/validation" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/file" ngxruntime "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/runtime" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies/clientsettings" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/resolver" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation" @@ -110,16 +113,22 @@ func StartManager(cfg config.Config) error { int32(cfg.HealthConfig.Port): "HealthPort", } + mustExtractGVK := kinds.NewMustExtractGKV(scheme) + + genericValidator := ngxvalidation.GenericValidator{} + policyManager := createPolicyManager(mustExtractGVK, genericValidator) + processor := state.NewChangeProcessorImpl(state.ChangeProcessorConfig{ GatewayCtlrName: cfg.GatewayCtlrName, GatewayClassName: cfg.GatewayClassName, Logger: cfg.Logger.WithName("changeProcessor"), Validators: validation.Validators{ HTTPFieldsValidator: ngxvalidation.HTTPValidator{}, - GenericValidator: ngxvalidation.GenericValidator{}, + GenericValidator: genericValidator, + PolicyValidator: policyManager, }, EventRecorder: recorder, - Scheme: scheme, + MustExtractGVK: mustExtractGVK, ProtectedPorts: protectedPorts, }) @@ -221,6 +230,7 @@ func StartManager(cfg config.Config) error { usageSecret: usageSecret, gatewayCtlrName: cfg.GatewayCtlrName, updateGatewayClassStatus: cfg.UpdateGatewayClassStatus, + policyConfigGenerator: policyManager, }) objects, objectLists := prepareFirstEventBatchPreparerArgs( @@ -272,6 +282,21 @@ func StartManager(cfg config.Config) error { return mgr.Start(ctx) } +func createPolicyManager( + mustExtractGVK kinds.MustExtractGVK, + validator validation.GenericValidator, +) *policies.Manager { + cfgs := []policies.ManagerConfig{ + { + GVK: mustExtractGVK(&ngfAPI.ClientSettingsPolicy{}), + Validator: clientsettings.NewValidator(validator), + Generator: clientsettings.Generate, + }, + } + + return policies.NewManager(mustExtractGVK, cfgs...) +} + func createManager(cfg config.Config, nginxChecker *nginxConfiguredOnStartChecker) (manager.Manager, error) { options := manager.Options{ Scheme: scheme, @@ -340,9 +365,12 @@ func registerControllers( { objectType: &gatewayv1.GatewayClass{}, options: []controller.Option{ - controller.WithK8sPredicate(k8spredicate.And( - k8spredicate.GenerationChangedPredicate{}, - predicate.GatewayClassPredicate{ControllerName: cfg.GatewayCtlrName})), + controller.WithK8sPredicate( + k8spredicate.And( + k8spredicate.GenerationChangedPredicate{}, + predicate.GatewayClassPredicate{ControllerName: cfg.GatewayCtlrName}, + ), + ), }, }, { @@ -427,6 +455,12 @@ func registerControllers( controller.WithK8sPredicate(k8spredicate.GenerationChangedPredicate{}), }, }, + { + objectType: &ngfAPI.ClientSettingsPolicy{}, + options: []controller.Option{ + controller.WithK8sPredicate(k8spredicate.GenerationChangedPredicate{}), + }, + }, } if cfg.ExperimentalFeatures { @@ -607,6 +641,7 @@ func prepareFirstEventBatchPreparerArgs( &gatewayv1beta1.ReferenceGrantList{}, &ngfAPI.NginxProxyList{}, &gatewayv1.GRPCRouteList{}, + &ngfAPI.ClientSettingsPolicyList{}, partialObjectMetadataList, } diff --git a/internal/mode/static/manager_test.go b/internal/mode/static/manager_test.go index dbcfc1d254..1d02cee615 100644 --- a/internal/mode/static/manager_test.go +++ b/internal/mode/static/manager_test.go @@ -56,6 +56,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) { &ngfAPI.NginxProxyList{}, &gatewayv1.GRPCRouteList{}, partialObjectMetadataList, + &ngfAPI.ClientSettingsPolicyList{}, }, }, { @@ -78,6 +79,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) { &ngfAPI.NginxProxyList{}, &gatewayv1.GRPCRouteList{}, partialObjectMetadataList, + &ngfAPI.ClientSettingsPolicyList{}, }, }, { @@ -99,9 +101,10 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) { &gatewayv1.HTTPRouteList{}, &gatewayv1beta1.ReferenceGrantList{}, &ngfAPI.NginxProxyList{}, - &gatewayv1.GRPCRouteList{}, partialObjectMetadataList, &gatewayv1alpha3.BackendTLSPolicyList{}, + &gatewayv1.GRPCRouteList{}, + &ngfAPI.ClientSettingsPolicyList{}, }, experimentalEnabled: true, }, diff --git a/internal/mode/static/nginx/config/base_http_config.go b/internal/mode/static/nginx/config/base_http_config.go index bcd8ae948e..abec446492 100644 --- a/internal/mode/static/nginx/config/base_http_config.go +++ b/internal/mode/static/nginx/config/base_http_config.go @@ -3,6 +3,7 @@ package config import ( gotemplate "text/template" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane" ) @@ -11,7 +12,7 @@ var baseHTTPTemplate = gotemplate.Must(gotemplate.New("baseHttp").Parse(baseHTTP func executeBaseHTTPConfig(conf dataplane.Configuration) []executeResult { result := executeResult{ dest: httpConfigFile, - data: execute(baseHTTPTemplate, conf.BaseHTTPConfig), + data: helpers.MustExecuteTemplate(baseHTTPTemplate, conf.BaseHTTPConfig), } return []executeResult{result} diff --git a/internal/mode/static/nginx/config/execute.go b/internal/mode/static/nginx/config/execute.go deleted file mode 100644 index eabc477449..0000000000 --- a/internal/mode/static/nginx/config/execute.go +++ /dev/null @@ -1,17 +0,0 @@ -package config - -import ( - "bytes" - "text/template" -) - -// executes the template with the given data. -func execute(template *template.Template, data interface{}) []byte { - var buf bytes.Buffer - - if err := template.Execute(&buf, data); err != nil { - panic(err) - } - - return buf.Bytes() -} diff --git a/internal/mode/static/nginx/config/execute_test.go b/internal/mode/static/nginx/config/execute_test.go deleted file mode 100644 index 61538973c3..0000000000 --- a/internal/mode/static/nginx/config/execute_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package config - -import ( - "testing" - - . "github.com/onsi/gomega" - - "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/http" -) - -func TestExecute(t *testing.T) { - g := NewWithT(t) - defer func() { - g.Expect(recover()).Should(BeNil()) - }() - bytes := execute(serversTemplate, []http.Server{}) - g.Expect(bytes).ToNot(BeEmpty()) -} - -func TestExecutePanics(t *testing.T) { - defer func() { - g := NewWithT(t) - g.Expect(recover()).ToNot(BeNil()) - }() - - _ = execute(serversTemplate, "not-correct-data") -} diff --git a/internal/mode/static/nginx/config/generator.go b/internal/mode/static/nginx/config/generator.go index 9d5c6e8fb5..a0509194e0 100644 --- a/internal/mode/static/nginx/config/generator.go +++ b/internal/mode/static/nginx/config/generator.go @@ -23,6 +23,9 @@ const ( // secretsFolder is the folder where secrets (like TLS certs/keys) are stored. secretsFolder = configFolder + "/secrets" + // includesFolder is the folder where are all include files are stored. + includesFolder = configFolder + "/includes" + // httpConfigFile is the path to the configuration file with HTTP configuration. httpConfigFile = httpFolder + "/http.conf" @@ -37,7 +40,7 @@ const ( ) // ConfigFolders is a list of folders where NGINX configuration files are stored. -var ConfigFolders = []string{httpFolder, secretsFolder, modulesIncludesFolder} +var ConfigFolders = []string{httpFolder, secretsFolder, includesFolder, modulesIncludesFolder} // Generator generates NGINX configuration files. // This interface is used for testing purposes only. diff --git a/internal/mode/static/nginx/config/http/config.go b/internal/mode/static/nginx/config/http/config.go index cae46e1b8d..19e7db223d 100644 --- a/internal/mode/static/nginx/config/http/config.go +++ b/internal/mode/static/nginx/config/http/config.go @@ -5,10 +5,11 @@ type Server struct { SSL *SSL ServerName string Locations []Location + Includes []string + Port int32 IsDefaultHTTP bool IsDefaultSSL bool GRPC bool - Port int32 } // Location holds all configuration for an HTTP location. @@ -17,11 +18,12 @@ type Location struct { ProxyPass string HTTPMatchKey string HTTPMatchVar string - Rewrites []string ProxySetHeaders []Header ProxySSLVerify *ProxySSLVerify Return *Return ResponseHeaders ResponseHeaders + Rewrites []string + Includes []string GRPC bool } diff --git a/internal/mode/static/nginx/config/maps.go b/internal/mode/static/nginx/config/maps.go index a4d6c419b6..97f5486a98 100644 --- a/internal/mode/static/nginx/config/maps.go +++ b/internal/mode/static/nginx/config/maps.go @@ -4,6 +4,7 @@ import ( "strings" gotemplate "text/template" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/http" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane" ) @@ -14,8 +15,9 @@ func executeMaps(conf dataplane.Configuration) []executeResult { maps := buildAddHeaderMaps(append(conf.HTTPServers, conf.SSLServers...)) result := executeResult{ dest: httpConfigFile, - data: execute(mapsTemplate, maps), + data: helpers.MustExecuteTemplate(mapsTemplate, maps), } + return []executeResult{result} } diff --git a/internal/mode/static/nginx/config/servers.go b/internal/mode/static/nginx/config/servers.go index b67b98af76..cc385449a4 100644 --- a/internal/mode/static/nginx/config/servers.go +++ b/internal/mode/static/nginx/config/servers.go @@ -8,6 +8,7 @@ import ( "strings" gotemplate "text/template" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/http" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane" ) @@ -61,7 +62,7 @@ func executeServers(conf dataplane.Configuration) []executeResult { serverResult := executeResult{ dest: httpConfigFile, - data: execute(serversTemplate, servers), + data: helpers.MustExecuteTemplate(serversTemplate, servers), } // create httpMatchPair conf @@ -76,7 +77,68 @@ func executeServers(conf dataplane.Configuration) []executeResult { data: httpMatchConf, } - return []executeResult{serverResult, httpMatchResult} + additionFileResults := createAdditionFileResults(conf) + + allResults := make([]executeResult, 0, len(additionFileResults)+2) + allResults = append(allResults, additionFileResults...) + allResults = append(allResults, serverResult, httpMatchResult) + + return allResults +} + +func createAdditionFileResults(conf dataplane.Configuration) []executeResult { + uniqueAdditions := make(map[string][]byte) + + findUniqueAdditionsForServer := func(server dataplane.VirtualServer) { + for _, add := range server.Additions { + uniqueAdditions[createAdditionFileName(add)] = add.Bytes + } + + for _, pr := range server.PathRules { + for _, mr := range pr.MatchRules { + for _, add := range mr.Additions { + uniqueAdditions[createAdditionFileName(add)] = add.Bytes + } + } + } + } + + for _, s := range conf.HTTPServers { + findUniqueAdditionsForServer(s) + } + + for _, s := range conf.SSLServers { + findUniqueAdditionsForServer(s) + } + + results := make([]executeResult, 0, len(uniqueAdditions)) + + for filename, contents := range uniqueAdditions { + results = append(results, executeResult{ + dest: filename, + data: contents, + }) + } + + return results +} + +func createAdditionFileName(addition dataplane.Addition) string { + return fmt.Sprintf("%s/%s.conf", includesFolder, addition.Identifier) +} + +func createIncludes(additions []dataplane.Addition) []string { + if len(additions) == 0 { + return nil + } + + includes := make([]string, 0, len(additions)) + + for _, addition := range additions { + includes = append(includes, createAdditionFileName(addition)) + } + + return includes } func createServers(httpServers, sslServers []dataplane.VirtualServer) ([]http.Server, httpMatchPairs) { @@ -117,6 +179,7 @@ func createSSLServer(virtualServer dataplane.VirtualServer, serverID int) (http. Locations: locs, Port: virtualServer.Port, GRPC: grpc, + Includes: createIncludes(virtualServer.Additions), }, matchPairs } @@ -135,6 +198,7 @@ func createServer(virtualServer dataplane.VirtualServer, serverID int) (http.Ser Locations: locs, Port: virtualServer.Port, GRPC: grpc, + Includes: createIncludes(virtualServer.Additions), }, matchPairs } @@ -169,12 +233,20 @@ func createLocations(server *dataplane.VirtualServer, serverID int) ([]http.Loca for matchRuleIdx, r := range rule.MatchRules { buildLocations := extLocations + if len(rule.MatchRules) != 1 || !isPathOnlyMatch(r.Match) { intLocation, match := initializeInternalLocation(pathRuleIdx, matchRuleIdx, r.Match) buildLocations = []http.Location{intLocation} matches = append(matches, match) } + includes := createIncludes(r.Additions) + + // buildLocations will either contain the extLocations OR the intLocation. + // If it contains the intLocation, the extLocations will be added to the final locations after we loop + // through all the MatchRules. + // It is safe to modify the buildLocations here by adding includes and filters. + buildLocations = updateLocationsForIncludes(buildLocations, includes) buildLocations = updateLocationsForFilters(r.Filters, buildLocations, r, server.Port, rule.Path, rule.GRPC) locs = append(locs, buildLocations...) } @@ -203,6 +275,14 @@ func createLocations(server *dataplane.VirtualServer, serverID int) ([]http.Loca return locs, matchPairs, grpc } +func updateLocationsForIncludes(locations []http.Location, includes []string) []http.Location { + for i := range locations { + locations[i].Includes = includes + } + + return locations +} + // pathAndTypeMap contains a map of paths and any path types defined for that path // for example, {/foo: {exact: {}, prefix: {}}} type pathAndTypeMap map[string]map[dataplane.PathType]struct{} diff --git a/internal/mode/static/nginx/config/servers_template.go b/internal/mode/static/nginx/config/servers_template.go index 12ebb60261..c12961be5c 100644 --- a/internal/mode/static/nginx/config/servers_template.go +++ b/internal/mode/static/nginx/config/servers_template.go @@ -32,9 +32,17 @@ server { server_name {{ $s.ServerName }}; + {{- range $i := $s.Includes }} + include {{ $i }}; + {{ end -}} + {{ range $l := $s.Locations }} location {{ $l.Path }} { - {{- range $r := $l.Rewrites }} + {{- range $i := $l.Includes }} + include {{ $i }}; + {{- end -}} + + {{ range $r := $l.Rewrites }} rewrite {{ $r }}; {{- end }} diff --git a/internal/mode/static/nginx/config/servers_test.go b/internal/mode/static/nginx/config/servers_test.go index e3d57bc2bf..ae458e8f03 100644 --- a/internal/mode/static/nginx/config/servers_test.go +++ b/internal/mode/static/nginx/config/servers_test.go @@ -28,6 +28,12 @@ func TestExecuteServers(t *testing.T) { { Hostname: "cafe.example.com", Port: 8080, + Additions: []dataplane.Addition{ + { + Bytes: []byte("addition-1"), + Identifier: "addition-1", + }, + }, }, }, SSLServers: []dataplane.VirtualServer{ @@ -74,6 +80,16 @@ func TestExecuteServers(t *testing.T) { }, }, }, + Additions: []dataplane.Addition{ + { + Bytes: []byte("addition-1"), + Identifier: "addition-1", // duplicate + }, + { + Bytes: []byte("addition-2"), + Identifier: "addition-2", + }, + }, }, }, } @@ -89,14 +105,35 @@ func TestExecuteServers(t *testing.T) { "ssl_certificate_key /etc/nginx/secrets/test-keypair.pem;": 2, "proxy_ssl_server_name on;": 1, } + + type assertion func(g *WithT, data string) + + expectedResults := map[string]assertion{ + httpConfigFile: func(g *WithT, data string) { + for expSubStr, expCount := range expSubStrings { + g.Expect(strings.Count(data, expSubStr)).To(Equal(expCount)) + } + }, + httpMatchVarsFile: func(g *WithT, data string) { + g.Expect(data).To(Equal("{}")) + }, + includesFolder + "/addition-1.conf": func(g *WithT, data string) { + g.Expect(data).To(Equal("addition-1")) + }, + includesFolder + "/addition-2.conf": func(g *WithT, data string) { + g.Expect(data).To(Equal("addition-2")) + }, + } g := NewWithT(t) - serverResults := executeServers(conf) - g.Expect(serverResults).To(HaveLen(2)) - serverConf := string(serverResults[0].data) - httpMatchConf := string(serverResults[1].data) - g.Expect(httpMatchConf).To(Equal("{}")) - for expSubStr, expCount := range expSubStrings { - g.Expect(strings.Count(serverConf, expSubStr)).To(Equal(expCount)) + + results := executeServers(conf) + g.Expect(results).To(HaveLen(len(expectedResults))) + + for _, res := range results { + g.Expect(expectedResults).To(HaveKey(res.dest), "executeServers returned unexpected result destination") + + assertData := expectedResults[res.dest] + assertData(g, string(res.data)) } } @@ -546,6 +583,40 @@ func TestCreateServers(t *testing.T) { }, GRPC: true, }, + { + Path: "/addition-path-only-match", + PathType: dataplane.PathTypeExact, + MatchRules: []dataplane.MatchRule{ + { + Match: dataplane.Match{}, + BackendGroup: fooGroup, + Additions: []dataplane.Addition{ + { + Bytes: []byte("path-only-match-addition"), + Identifier: "path-only-match-addition", + }, + }, + }, + }, + }, + { + Path: "/addition-header-match", + PathType: dataplane.PathTypeExact, + MatchRules: []dataplane.MatchRule{ + { + Match: dataplane.Match{ + Method: helpers.GetPointer("GET"), + }, + BackendGroup: fooGroup, + Additions: []dataplane.Addition{ + { + Bytes: []byte("match-addition"), + Identifier: "match-addition", + }, + }, + }, + }, + }, } httpServers := []dataplane.VirtualServer{ @@ -557,6 +628,16 @@ func TestCreateServers(t *testing.T) { Hostname: "cafe.example.com", PathRules: cafePathRules, Port: 8080, + Additions: []dataplane.Addition{ + { + Bytes: []byte("server-addition-1"), + Identifier: "server-addition-1", + }, + { + Bytes: []byte("server-addition-2"), + Identifier: "server-addition-2", + }, + }, }, } @@ -570,6 +651,16 @@ func TestCreateServers(t *testing.T) { SSL: &dataplane.SSL{KeyPairID: sslKeyPairID}, PathRules: cafePathRules, Port: 8443, + Additions: []dataplane.Addition{ + { + Bytes: []byte("server-addition-1"), + Identifier: "server-addition-1", + }, + { + Bytes: []byte("server-addition-3"), + Identifier: "server-addition-3", + }, + }, }, } @@ -611,6 +702,12 @@ func TestCreateServers(t *testing.T) { Any: false, }, }, + "1_17": { + { + Method: "GET", + RedirectPath: "@rule17-route0", + }, + }, } allExpMatchPair := make(httpMatchPairs) @@ -899,6 +996,26 @@ func TestCreateServers(t *testing.T) { GRPC: true, ProxySetHeaders: grpcBaseHeaders, }, + { + Path: "= /addition-path-only-match", + ProxyPass: "http://test_foo_80$request_uri", + ProxySetHeaders: httpBaseHeaders, + Includes: []string{ + includesFolder + "/path-only-match-addition.conf", + }, + }, + { + Path: "@rule17-route0", + ProxyPass: "http://test_foo_80$request_uri", + ProxySetHeaders: httpBaseHeaders, + Includes: []string{ + includesFolder + "/match-addition.conf", + }, + }, + { + Path: "= /addition-header-match", + HTTPMatchKey: ssl + "1_17", + }, } } @@ -914,6 +1031,10 @@ func TestCreateServers(t *testing.T) { Locations: getExpectedLocations(false), Port: 8080, GRPC: true, + Includes: []string{ + includesFolder + "/server-addition-1.conf", + includesFolder + "/server-addition-2.conf", + }, }, { IsDefaultSSL: true, @@ -928,6 +1049,10 @@ func TestCreateServers(t *testing.T) { Locations: getExpectedLocations(true), Port: 8443, GRPC: true, + Includes: []string{ + includesFolder + "/server-addition-1.conf", + includesFolder + "/server-addition-3.conf", + }, }, } @@ -2220,3 +2345,173 @@ func TestGenerateResponseHeaders(t *testing.T) { }) } } + +func TestCreateIncludes(t *testing.T) { + tests := []struct { + name string + additions []dataplane.Addition + includes []string + }{ + { + name: "no additions", + additions: nil, + includes: nil, + }, + { + name: "additions", + additions: []dataplane.Addition{ + { + Bytes: []byte("one"), + Identifier: "one", + }, + { + Bytes: []byte("two"), + Identifier: "two", + }, + { + Bytes: []byte("three"), + Identifier: "three", + }, + }, + includes: []string{ + includesFolder + "/one.conf", + includesFolder + "/two.conf", + includesFolder + "/three.conf", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + includes := createIncludes(test.additions) + g.Expect(includes).To(Equal(test.includes)) + }) + } +} + +func TestCreateAdditionFileResults(t *testing.T) { + conf := dataplane.Configuration{ + HTTPServers: []dataplane.VirtualServer{ + { + Additions: []dataplane.Addition{ + { + Identifier: "include-1", + Bytes: []byte("include-1"), + }, + { + Identifier: "include-2", + Bytes: []byte("include-2"), + }, + }, + PathRules: []dataplane.PathRule{ + { + MatchRules: []dataplane.MatchRule{ + { + Additions: []dataplane.Addition{ + { + Identifier: "include-3", + Bytes: []byte("include-3"), + }, + { + Identifier: "include-4", + Bytes: []byte("include-4"), + }, + }, + }, + }, + }, + }, + }, + { + Additions: []dataplane.Addition{ + { + Identifier: "include-1", // dupe + Bytes: []byte("include-1"), + }, + { + Identifier: "include-2", // dupe + Bytes: []byte("include-2"), + }, + }, + }, + }, + SSLServers: []dataplane.VirtualServer{ + { + Additions: []dataplane.Addition{ + { + Identifier: "include-1", // dupe + Bytes: []byte("include-1"), + }, + { + Identifier: "include-2", // dupe + Bytes: []byte("include-2"), + }, + }, + PathRules: []dataplane.PathRule{ + { + MatchRules: []dataplane.MatchRule{ + { + Additions: []dataplane.Addition{ + { + Identifier: "include-3", + Bytes: []byte("include-3"), // dupe + }, + { + Identifier: "include-5", + Bytes: []byte("include-5"), // dupe + }, + { + Identifier: "include-6", + Bytes: []byte("include-6"), + }, + }, + }, + }, + }, + }, + }, + }, + } + + results := createAdditionFileResults(conf) + + expResults := []executeResult{ + { + dest: includesFolder + "/" + "include-1.conf", + data: []byte("include-1"), + }, + { + dest: includesFolder + "/" + "include-2.conf", + data: []byte("include-2"), + }, + { + dest: includesFolder + "/" + "include-3.conf", + data: []byte("include-3"), + }, + { + dest: includesFolder + "/" + "include-4.conf", + data: []byte("include-4"), + }, + { + dest: includesFolder + "/" + "include-5.conf", + data: []byte("include-5"), + }, + { + dest: includesFolder + "/" + "include-6.conf", + data: []byte("include-6"), + }, + } + + g := NewWithT(t) + + g.Expect(results).To(ConsistOf(expResults)) +} + +func TestAdditionFilename(t *testing.T) { + g := NewWithT(t) + + name := createAdditionFileName(dataplane.Addition{Identifier: "my-addition"}) + g.Expect(name).To(Equal(includesFolder + "/" + "my-addition.conf")) +} diff --git a/internal/mode/static/nginx/config/split_clients.go b/internal/mode/static/nginx/config/split_clients.go index 7d74ad18be..0cc050e7c0 100644 --- a/internal/mode/static/nginx/config/split_clients.go +++ b/internal/mode/static/nginx/config/split_clients.go @@ -5,6 +5,7 @@ import ( "math" gotemplate "text/template" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/http" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane" ) @@ -16,7 +17,7 @@ func executeSplitClients(conf dataplane.Configuration) []executeResult { result := executeResult{ dest: httpConfigFile, - data: execute(splitClientsTemplate, splitClients), + data: helpers.MustExecuteTemplate(splitClientsTemplate, splitClients), } return []executeResult{result} diff --git a/internal/mode/static/nginx/config/telemetry.go b/internal/mode/static/nginx/config/telemetry.go index fef1e4ec27..719fad9fc2 100644 --- a/internal/mode/static/nginx/config/telemetry.go +++ b/internal/mode/static/nginx/config/telemetry.go @@ -3,6 +3,7 @@ package config import ( gotemplate "text/template" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane" ) @@ -12,7 +13,7 @@ func executeTelemetry(conf dataplane.Configuration) []executeResult { if conf.Telemetry.Endpoint != "" { result := executeResult{ dest: httpConfigFile, - data: execute(otelTemplate, conf.Telemetry), + data: helpers.MustExecuteTemplate(otelTemplate, conf.Telemetry), } return []executeResult{result} diff --git a/internal/mode/static/nginx/config/upstreams.go b/internal/mode/static/nginx/config/upstreams.go index 9c9c2b712c..37d344e938 100644 --- a/internal/mode/static/nginx/config/upstreams.go +++ b/internal/mode/static/nginx/config/upstreams.go @@ -4,6 +4,7 @@ import ( "fmt" gotemplate "text/template" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/http" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/dataplane" ) @@ -28,8 +29,9 @@ func (g GeneratorImpl) executeUpstreams(conf dataplane.Configuration) []executeR result := executeResult{ dest: httpConfigFile, - data: execute(upstreamsTemplate, upstreams), + data: helpers.MustExecuteTemplate(upstreamsTemplate, upstreams), } + return []executeResult{result} } diff --git a/internal/mode/static/nginx/config/validation/generic.go b/internal/mode/static/nginx/config/validation/generic.go index bac2ecf827..73c6f01044 100644 --- a/internal/mode/static/nginx/config/validation/generic.go +++ b/internal/mode/static/nginx/config/validation/generic.go @@ -59,6 +59,29 @@ func (GenericValidator) ValidateNginxDuration(duration string) error { return nil } +const ( + sizeStringFmt = `^\d{1,4}(k|m|g)?$` + sizeStringErrMsg = "must contain a number. May be followed by 'k', 'm', or 'g', otherwise bytes are assumed" +) + +var sizeStringFmtRegexp = regexp.MustCompile("^" + sizeStringFmt + "$") + +// ValidateNginxSize validates a size string that nginx can understand. +func (GenericValidator) ValidateNginxSize(size string) error { + if !sizeStringFmtRegexp.MatchString(size) { + examples := []string{ + "1024", + "8k", + "20m", + "1g", + } + + return errors.New(k8svalidation.RegexError(sizeStringFmt, sizeStringErrMsg, examples...)) + } + + return nil +} + const ( //nolint:lll endpointStringFmt = `(?:http?:\/\/)?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*(?::\d{1,5})?` diff --git a/internal/mode/static/nginx/config/validation/generic_test.go b/internal/mode/static/nginx/config/validation/generic_test.go index b07131ccf0..86903bfa38 100644 --- a/internal/mode/static/nginx/config/validation/generic_test.go +++ b/internal/mode/static/nginx/config/validation/generic_test.go @@ -64,6 +64,27 @@ func TestValidateNginxDuration(t *testing.T) { ) } +func TestValidateNginxSize(t *testing.T) { + validator := GenericValidator{} + + testValidValuesForSimpleValidator( + t, + validator.ValidateNginxSize, + `1024`, + `10k`, + `123m`, + `4096g`, + ) + + testInvalidValuesForSimpleValidator( + t, + validator.ValidateNginxSize, + `test`, + `12345`, + `5b`, + ) +} + func TestValidateEndpoint(t *testing.T) { validator := GenericValidator{} diff --git a/internal/mode/static/nginx/config/version.go b/internal/mode/static/nginx/config/version.go index 494a3f7d31..5baa7f24b8 100644 --- a/internal/mode/static/nginx/config/version.go +++ b/internal/mode/static/nginx/config/version.go @@ -2,10 +2,12 @@ package config import ( gotemplate "text/template" + + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" ) var versionTemplate = gotemplate.Must(gotemplate.New("version").Parse(versionTemplateText)) func executeVersion(version int) []byte { - return execute(versionTemplate, version) + return helpers.MustExecuteTemplate(versionTemplate, version) } diff --git a/internal/mode/static/policies/clientsettings/generator.go b/internal/mode/static/policies/clientsettings/generator.go new file mode 100644 index 0000000000..e7f50ef8bb --- /dev/null +++ b/internal/mode/static/policies/clientsettings/generator.go @@ -0,0 +1,44 @@ +package clientsettings + +import ( + "text/template" + + ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies" +) + +var tmpl = template.Must(template.New("client settings policy").Parse(clientSettingsTemplate)) + +const clientSettingsTemplate = ` +{{- if .Body }} + {{- if .Body.MaxSize }} +client_max_body_size {{ .Body.MaxSize }}; + {{- end }} + {{- if .Body.Timeout }} +client_body_timeout {{ .Body.Timeout }}; + {{- end }} +{{- end }} +{{- if .KeepAlive }} + {{- if .KeepAlive.Requests }} +keepalive_requests {{ .KeepAlive.Requests }}; + {{- end }} + {{- if .KeepAlive.Time }} +keepalive_time {{ .KeepAlive.Time }}; + {{- end }} + {{- if .KeepAlive.Timeout }} + {{- if and .KeepAlive.Timeout.Server .KeepAlive.Timeout.Header }} +keepalive_timeout {{ .KeepAlive.Timeout.Server }} {{ .KeepAlive.Timeout.Header }}; + {{- else if .KeepAlive.Timeout.Server }} +keepalive_timeout {{ .KeepAlive.Timeout.Server }}; + {{- end }} + {{- end }} +{{- end }} +` + +// Generate generates configuration as []byte for a ClientSettingsPolicy. +func Generate(policy policies.Policy) []byte { + csp := helpers.MustCastObject[*ngfAPI.ClientSettingsPolicy](policy) + + return helpers.MustExecuteTemplate(tmpl, csp.Spec) +} diff --git a/internal/mode/static/policies/clientsettings/generator_test.go b/internal/mode/static/policies/clientsettings/generator_test.go new file mode 100644 index 0000000000..a286342d5e --- /dev/null +++ b/internal/mode/static/policies/clientsettings/generator_test.go @@ -0,0 +1,173 @@ +package clientsettings_test + +import ( + "testing" + + . "github.com/onsi/gomega" + + ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies/clientsettings" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies/policiesfakes" +) + +func TestGenerate(t *testing.T) { + maxSize := helpers.GetPointer[ngfAPI.Size]("10m") + bodyTimeout := helpers.GetPointer[ngfAPI.Duration]("600ms") + keepaliveRequests := helpers.GetPointer[int32](900) + keepaliveTime := helpers.GetPointer[ngfAPI.Duration]("50s") + keepaliveServerTimeout := helpers.GetPointer[ngfAPI.Duration]("30s") + keepaliveHeaderTimeout := helpers.GetPointer[ngfAPI.Duration]("60s") + + tests := []struct { + name string + policy policies.Policy + expStrings []string + }{ + { + name: "body max size populated", + policy: &ngfAPI.ClientSettingsPolicy{ + Spec: ngfAPI.ClientSettingsPolicySpec{ + Body: &ngfAPI.ClientBody{ + MaxSize: maxSize, + }, + }, + }, + expStrings: []string{ + "client_max_body_size 10m;", + }, + }, + { + name: "body timeout populated", + policy: &ngfAPI.ClientSettingsPolicy{ + Spec: ngfAPI.ClientSettingsPolicySpec{ + Body: &ngfAPI.ClientBody{ + Timeout: bodyTimeout, + }, + }, + }, + expStrings: []string{ + "client_body_timeout 600ms", + }, + }, + { + name: "keepalive requests populated", + policy: &ngfAPI.ClientSettingsPolicy{ + Spec: ngfAPI.ClientSettingsPolicySpec{ + KeepAlive: &ngfAPI.ClientKeepAlive{ + Requests: keepaliveRequests, + }, + }, + }, + expStrings: []string{ + "keepalive_requests 900;", + }, + }, + { + name: "keepalive time populated", + policy: &ngfAPI.ClientSettingsPolicy{ + Spec: ngfAPI.ClientSettingsPolicySpec{ + KeepAlive: &ngfAPI.ClientKeepAlive{ + Time: keepaliveTime, + }, + }, + }, + expStrings: []string{ + "keepalive_time 50s;", + }, + }, + { + name: "keepalive timeout server populated", + policy: &ngfAPI.ClientSettingsPolicy{ + Spec: ngfAPI.ClientSettingsPolicySpec{ + KeepAlive: &ngfAPI.ClientKeepAlive{ + Timeout: &ngfAPI.ClientKeepAliveTimeout{ + Server: keepaliveServerTimeout, + }, + }, + }, + }, + expStrings: []string{ + "keepalive_timeout 30s;", + }, + }, + { + name: "keepalive timeout server and header populated", + policy: &ngfAPI.ClientSettingsPolicy{ + Spec: ngfAPI.ClientSettingsPolicySpec{ + KeepAlive: &ngfAPI.ClientKeepAlive{ + Timeout: &ngfAPI.ClientKeepAliveTimeout{ + Server: keepaliveServerTimeout, + Header: keepaliveHeaderTimeout, + }, + }, + }, + }, + expStrings: []string{ + "keepalive_timeout 30s 60s;", + }, + }, + { + name: "keepalive timeout header populated", + policy: &ngfAPI.ClientSettingsPolicy{ + Spec: ngfAPI.ClientSettingsPolicySpec{ + KeepAlive: &ngfAPI.ClientKeepAlive{ + Timeout: &ngfAPI.ClientKeepAliveTimeout{ + Header: keepaliveHeaderTimeout, + }, + }, + }, + }, + expStrings: []string{}, // header timeout is ignored if server timeout is not populated + }, + { + name: "all fields populated", + policy: &ngfAPI.ClientSettingsPolicy{ + Spec: ngfAPI.ClientSettingsPolicySpec{ + Body: &ngfAPI.ClientBody{ + MaxSize: maxSize, + Timeout: bodyTimeout, + }, + KeepAlive: &ngfAPI.ClientKeepAlive{ + Requests: keepaliveRequests, + Time: keepaliveTime, + Timeout: &ngfAPI.ClientKeepAliveTimeout{ + Server: keepaliveServerTimeout, + Header: keepaliveHeaderTimeout, + }, + }, + }, + }, + expStrings: []string{ + "client_max_body_size 10m;", + "client_body_timeout 600ms", + "keepalive_requests 900;", + "keepalive_time 50s;", + "keepalive_timeout 30s 60s;", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + cfgString := string(clientsettings.Generate(test.policy)) + + for _, str := range test.expStrings { + g.Expect(cfgString).To(ContainSubstring(str)) + } + }) + } +} + +func TestGeneratePanics(t *testing.T) { + g := NewWithT(t) + + generate := func() { + clientsettings.Generate(&policiesfakes.FakePolicy{}) + } + + g.Expect(generate).To(Panic()) +} diff --git a/internal/mode/static/policies/clientsettings/validator.go b/internal/mode/static/policies/clientsettings/validator.go new file mode 100644 index 0000000000..efe8630436 --- /dev/null +++ b/internal/mode/static/policies/clientsettings/validator.go @@ -0,0 +1,196 @@ +package clientsettings + +import ( + "slices" + + "k8s.io/apimachinery/pkg/util/validation/field" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" + + ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation" +) + +// Validator validates a ClientSettingsPolicy. +// Implements policies.Validator interface. +type Validator struct { + genericValidator validation.GenericValidator +} + +// NewValidator returns a new instance of Validator. +func NewValidator(genericValidator validation.GenericValidator) *Validator { + return &Validator{genericValidator: genericValidator} +} + +// Validate validates the spec of a ClientSettingsPolicy. +func (v *Validator) Validate(policy policies.Policy) error { + csp := helpers.MustCastObject[*ngfAPI.ClientSettingsPolicy](policy) + + if err := validateTargetRef(csp.Spec.TargetRef); err != nil { + return err + } + + return v.validateSettings(csp.Spec) +} + +// Conflicts returns true if the two ClientSettingsPolicies conflict. +func (v *Validator) Conflicts(polA, polB policies.Policy) bool { + cspA := helpers.MustCastObject[*ngfAPI.ClientSettingsPolicy](polA) + cspB := helpers.MustCastObject[*ngfAPI.ClientSettingsPolicy](polB) + + return conflicts(cspA.Spec, cspB.Spec) +} + +func conflicts(a, b ngfAPI.ClientSettingsPolicySpec) bool { + if a.Body != nil && b.Body != nil { + if a.Body.Timeout != nil && b.Body.Timeout != nil { + return true + } + + if a.Body.MaxSize != nil && b.Body.MaxSize != nil { + return true + } + } + + if a.KeepAlive != nil && b.KeepAlive != nil { + if a.KeepAlive.Requests != nil && b.KeepAlive.Requests != nil { + return true + } + + if a.KeepAlive.Time != nil && b.KeepAlive.Time != nil { + return true + } + + if a.KeepAlive.Timeout != nil && b.KeepAlive.Timeout != nil { + return true + } + + } + + return false +} + +func validateTargetRef(ref v1alpha2.LocalPolicyTargetReference) error { + basePath := field.NewPath("spec").Child("targetRef") + + if ref.Group != gatewayv1.GroupName { + path := basePath.Child("group") + + return field.NotSupported( + path, + ref.Group, + []string{gatewayv1.GroupName}, + ) + } + + supportedKinds := []gatewayv1.Kind{kinds.Gateway, kinds.HTTPRoute, kinds.GRPCRoute} + + if !slices.Contains(supportedKinds, ref.Kind) { + path := basePath.Child("kind") + + return field.NotSupported( + path, + ref.Kind, + supportedKinds, + ) + } + + return nil +} + +// validateSettings performs validation on fields in the spec that are vulnerable to code injection. +// For all other fields, we rely on the CRD validation. +func (v *Validator) validateSettings(spec ngfAPI.ClientSettingsPolicySpec) error { + var allErrs field.ErrorList + fieldPath := field.NewPath("spec") + + if spec.Body != nil { + allErrs = append(allErrs, v.validateClientBody(*spec.Body, fieldPath.Child("body"))...) + } + + if spec.KeepAlive != nil { + allErrs = append(allErrs, v.validateClientKeepAlive(*spec.KeepAlive, fieldPath.Child("keepAlive"))...) + } + + return allErrs.ToAggregate() +} + +func (v *Validator) validateClientBody(body ngfAPI.ClientBody, fieldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + if body.Timeout != nil { + if err := v.genericValidator.ValidateNginxDuration(string(*body.Timeout)); err != nil { + path := fieldPath.Child("timeout") + + allErrs = append(allErrs, field.Invalid(path, body.Timeout, err.Error())) + } + } + + if body.MaxSize != nil { + if err := v.genericValidator.ValidateNginxSize(string(*body.MaxSize)); err != nil { + path := fieldPath.Child("maxSize") + + allErrs = append(allErrs, field.Invalid(path, body.MaxSize, err.Error())) + } + } + + return allErrs +} + +func (v *Validator) validateClientKeepAlive(keepAlive ngfAPI.ClientKeepAlive, fieldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + + if keepAlive.Time != nil { + if err := v.genericValidator.ValidateNginxDuration(string(*keepAlive.Time)); err != nil { + path := fieldPath.Child("time") + + allErrs = append(allErrs, field.Invalid(path, *keepAlive.Time, err.Error())) + } + } + + if keepAlive.Timeout != nil { + timeout := keepAlive.Timeout + + if timeout.Server != nil { + if err := v.genericValidator.ValidateNginxDuration(string(*timeout.Server)); err != nil { + path := fieldPath.Child("timeout").Child("server") + + allErrs = append( + allErrs, + field.Invalid(path, *keepAlive.Timeout.Server, err.Error()), + ) + } + } + + if timeout.Header != nil { + if err := v.genericValidator.ValidateNginxDuration(string(*timeout.Header)); err != nil { + path := fieldPath.Child("timeout").Child("header") + + allErrs = append( + allErrs, + field.Invalid(path, *keepAlive.Timeout.Header, err.Error()), + ) + } + } + + // This is a special case. The keepalive_timeout directive takes two parameters: + // keepalive_timeout server [header], where header is optional. If header is provided and server is not, + // we can't properly configure the directive. + if keepAlive.Timeout.Header != nil && keepAlive.Timeout.Server == nil { + path := fieldPath.Child("timeout") + + allErrs = append( + allErrs, + field.Invalid( + path, + nil, + "server timeout must be set if header timeout is set", + ), + ) + } + } + + return allErrs +} diff --git a/internal/mode/static/policies/clientsettings/validator_test.go b/internal/mode/static/policies/clientsettings/validator_test.go new file mode 100644 index 0000000000..04d83ae97d --- /dev/null +++ b/internal/mode/static/policies/clientsettings/validator_test.go @@ -0,0 +1,267 @@ +package clientsettings_test + +import ( + "testing" + + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" + + ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/config/validation" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies/clientsettings" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies/policiesfakes" +) + +type policyModFunc func(policy *ngfAPI.ClientSettingsPolicy) *ngfAPI.ClientSettingsPolicy + +func createValidPolicy() *ngfAPI.ClientSettingsPolicy { + return &ngfAPI.ClientSettingsPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + }, + Spec: ngfAPI.ClientSettingsPolicySpec{ + TargetRef: v1alpha2.LocalPolicyTargetReference{ + Group: v1.GroupName, + Kind: kinds.Gateway, + Name: "gateway", + }, + Body: &ngfAPI.ClientBody{ + MaxSize: helpers.GetPointer[ngfAPI.Size]("10m"), + Timeout: helpers.GetPointer[ngfAPI.Duration]("600ms"), + }, + KeepAlive: &ngfAPI.ClientKeepAlive{ + Requests: helpers.GetPointer[int32](900), + Time: helpers.GetPointer[ngfAPI.Duration]("50s"), + Timeout: &ngfAPI.ClientKeepAliveTimeout{ + Server: helpers.GetPointer[ngfAPI.Duration]("30s"), + Header: helpers.GetPointer[ngfAPI.Duration]("60s"), + }, + }, + }, + Status: v1alpha2.PolicyStatus{}, + } +} + +func createModifiedPolicy(mod policyModFunc) *ngfAPI.ClientSettingsPolicy { + return mod(createValidPolicy()) +} + +func TestValidator_Validate(t *testing.T) { + tests := []struct { + name string + policy *ngfAPI.ClientSettingsPolicy + expErrSubstrings []string + }{ + { + name: "invalid target ref; unsupported group", + policy: createModifiedPolicy(func(p *ngfAPI.ClientSettingsPolicy) *ngfAPI.ClientSettingsPolicy { + p.Spec.TargetRef.Group = "Unsupported" + return p + }), + expErrSubstrings: []string{"spec.targetRef.group"}, + }, + { + name: "invalid target ref; unsupported kind", + policy: createModifiedPolicy(func(p *ngfAPI.ClientSettingsPolicy) *ngfAPI.ClientSettingsPolicy { + p.Spec.TargetRef.Kind = "Unsupported" + return p + }), + expErrSubstrings: []string{"spec.targetRef.kind"}, + }, + { + name: "invalid client max body size", + policy: createModifiedPolicy(func(p *ngfAPI.ClientSettingsPolicy) *ngfAPI.ClientSettingsPolicy { + p.Spec.Body.MaxSize = helpers.GetPointer[ngfAPI.Size]("invalid") + return p + }), + expErrSubstrings: []string{"spec.body.maxSize"}, + }, + { + name: "invalid durations", + policy: createModifiedPolicy(func(p *ngfAPI.ClientSettingsPolicy) *ngfAPI.ClientSettingsPolicy { + p.Spec.Body.Timeout = helpers.GetPointer[ngfAPI.Duration]("invalid") + p.Spec.KeepAlive.Time = helpers.GetPointer[ngfAPI.Duration]("invalid") + p.Spec.KeepAlive.Timeout.Server = helpers.GetPointer[ngfAPI.Duration]("invalid") + p.Spec.KeepAlive.Timeout.Header = helpers.GetPointer[ngfAPI.Duration]("invalid") + return p + }), + expErrSubstrings: []string{ + "spec.body.timeout", + "spec.keepAlive.time", + "spec.keepAlive.timeout.server", + "spec.keepAlive.timeout.header", + }, + }, + { + name: "invalid keepalive timeout; header provided without server", + policy: createModifiedPolicy(func(p *ngfAPI.ClientSettingsPolicy) *ngfAPI.ClientSettingsPolicy { + p.Spec.KeepAlive.Timeout.Server = nil + return p + }), + expErrSubstrings: []string{"spec.keepAlive.timeout"}, + }, + { + name: "valid", + policy: createValidPolicy(), + expErrSubstrings: nil, + }, + } + + v := clientsettings.NewValidator(validation.GenericValidator{}) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + err := v.Validate(test.policy) + + if len(test.expErrSubstrings) == 0 { + g.Expect(err).ToNot(HaveOccurred()) + } else { + g.Expect(err).To(HaveOccurred()) + } + + for _, str := range test.expErrSubstrings { + g.Expect(err.Error()).To(ContainSubstring(str)) + } + }) + } +} + +func TestValidator_ValidatePanics(t *testing.T) { + v := clientsettings.NewValidator(nil) + + validate := func() { + _ = v.Validate(&policiesfakes.FakePolicy{}) + } + + g := NewWithT(t) + + g.Expect(validate).To(Panic()) +} + +func TestValidator_Conflicts(t *testing.T) { + tests := []struct { + polA *ngfAPI.ClientSettingsPolicy + polB *ngfAPI.ClientSettingsPolicy + name string + conflicts bool + }{ + { + name: "no conflicts", + polA: &ngfAPI.ClientSettingsPolicy{ + Spec: ngfAPI.ClientSettingsPolicySpec{ + Body: &ngfAPI.ClientBody{ + MaxSize: helpers.GetPointer[ngfAPI.Size]("10m"), + }, + KeepAlive: &ngfAPI.ClientKeepAlive{ + Requests: helpers.GetPointer[int32](900), + Time: helpers.GetPointer[ngfAPI.Duration]("50s"), + }, + }, + }, + polB: &ngfAPI.ClientSettingsPolicy{ + Spec: ngfAPI.ClientSettingsPolicySpec{ + Body: &ngfAPI.ClientBody{ + Timeout: helpers.GetPointer[ngfAPI.Duration]("600ms"), + }, + KeepAlive: &ngfAPI.ClientKeepAlive{ + Timeout: &ngfAPI.ClientKeepAliveTimeout{ + Server: helpers.GetPointer[ngfAPI.Duration]("30s"), + Header: helpers.GetPointer[ngfAPI.Duration]("60s"), + }, + }, + }, + }, + conflicts: false, + }, + { + name: "body max size conflicts", + polA: createValidPolicy(), + polB: &ngfAPI.ClientSettingsPolicy{ + Spec: ngfAPI.ClientSettingsPolicySpec{ + Body: &ngfAPI.ClientBody{ + MaxSize: helpers.GetPointer[ngfAPI.Size]("10m"), + }, + }, + }, + conflicts: true, + }, + { + name: "body timeout conflicts", + polA: createValidPolicy(), + polB: &ngfAPI.ClientSettingsPolicy{ + Spec: ngfAPI.ClientSettingsPolicySpec{ + Body: &ngfAPI.ClientBody{ + Timeout: helpers.GetPointer[ngfAPI.Duration]("600ms"), + }, + }, + }, + conflicts: true, + }, + { + name: "keepalive requests conflicts", + polA: createValidPolicy(), + polB: &ngfAPI.ClientSettingsPolicy{ + Spec: ngfAPI.ClientSettingsPolicySpec{ + KeepAlive: &ngfAPI.ClientKeepAlive{ + Requests: helpers.GetPointer[int32](900), + }, + }, + }, + conflicts: true, + }, + { + name: "keepalive time conflicts", + polA: createValidPolicy(), + polB: &ngfAPI.ClientSettingsPolicy{ + Spec: ngfAPI.ClientSettingsPolicySpec{ + KeepAlive: &ngfAPI.ClientKeepAlive{ + Time: helpers.GetPointer[ngfAPI.Duration]("50s"), + }, + }, + }, + conflicts: true, + }, + { + name: "keepalive timeout conflicts", + polA: createValidPolicy(), + polB: &ngfAPI.ClientSettingsPolicy{ + Spec: ngfAPI.ClientSettingsPolicySpec{ + KeepAlive: &ngfAPI.ClientKeepAlive{ + Timeout: &ngfAPI.ClientKeepAliveTimeout{ + Server: helpers.GetPointer[ngfAPI.Duration]("30s"), + }, + }, + }, + }, + conflicts: true, + }, + } + + v := clientsettings.NewValidator(nil) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + g.Expect(v.Conflicts(test.polA, test.polB)).To(Equal(test.conflicts)) + }) + } +} + +func TestValidator_ConflictsPanics(t *testing.T) { + v := clientsettings.NewValidator(nil) + + conflicts := func() { + _ = v.Conflicts(&policiesfakes.FakePolicy{}, &policiesfakes.FakePolicy{}) + } + + g := NewWithT(t) + + g.Expect(conflicts).To(Panic()) +} diff --git a/internal/mode/static/policies/manager.go b/internal/mode/static/policies/manager.go new file mode 100644 index 0000000000..f1f7a15fb3 --- /dev/null +++ b/internal/mode/static/policies/manager.go @@ -0,0 +1,97 @@ +package policies + +//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" +) + +// GenerateFunc generates config as []byte for an NGF Policy. +type GenerateFunc func(policy Policy) []byte + +// Validator validates an NGF Policy. +// +//counterfeiter:generate . Validator +type Validator interface { + // Validate validates an NGF Policy. + Validate(policy Policy) error + // Conflicts returns true if the two Policies conflict. + Conflicts(a, b Policy) bool +} + +// Manager manages the validators and generators for NGF Policies. +type Manager struct { + validators map[schema.GroupVersionKind]Validator + generators map[schema.GroupVersionKind]GenerateFunc + mustExtractGVK kinds.MustExtractGVK +} + +// ManagerConfig contains the config to register a Policy with the Manager. +type ManagerConfig struct { + // Validator is the Validator for the Policy. + Validator Validator + // Generate is the GenerateFunc for the Policy. + Generator GenerateFunc + // GVK is the GroupVersionKind of the Policy. + GVK schema.GroupVersionKind +} + +// NewManager returns a new Manager. +// Implements dataplane.ConfigGenerator and validation.PolicyValidator. +func NewManager( + mustExtractGVK kinds.MustExtractGVK, + configs ...ManagerConfig, +) *Manager { + v := &Manager{ + validators: make(map[schema.GroupVersionKind]Validator), + generators: make(map[schema.GroupVersionKind]GenerateFunc), + mustExtractGVK: mustExtractGVK, + } + + for _, cfg := range configs { + v.validators[cfg.GVK] = cfg.Validator + v.generators[cfg.GVK] = cfg.Generator + } + + return v +} + +// Generate generates config for the policy as a byte array. +func (m *Manager) Generate(policy Policy) []byte { + gvk := m.mustExtractGVK(policy) + + generate, ok := m.generators[gvk] + if !ok { + panic(fmt.Sprintf("no generate function registered for policy %T", policy)) + } + + return generate(policy) +} + +// Validate validates the policy. +func (m *Manager) Validate(policy Policy) error { + gvk := m.mustExtractGVK(policy) + + validator, ok := m.validators[gvk] + if !ok { + panic(fmt.Sprintf("no validator registered for policy %T", policy)) + } + + return validator.Validate(policy) +} + +// Conflicts returns true if the policies conflict. +func (m *Manager) Conflicts(polA, polB Policy) bool { + gvk := m.mustExtractGVK(polA) + + validator, ok := m.validators[gvk] + if !ok { + panic(fmt.Sprintf("no validator registered for policy %T", polA)) + } + + return validator.Conflicts(polA, polB) +} diff --git a/internal/mode/static/policies/manager_test.go b/internal/mode/static/policies/manager_test.go new file mode 100644 index 0000000000..db6d81fda7 --- /dev/null +++ b/internal/mode/static/policies/manager_test.go @@ -0,0 +1,115 @@ +package policies_test + +import ( + "errors" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies/policiesfakes" +) + +var _ = Describe("Policy Manager", func() { + orangeGVK := schema.GroupVersionKind{Group: "fruit", Version: "1", Kind: "orange"} + orangePolicy := &policiesfakes.FakePolicy{ + GetNameStub: func() string { + return "orange" + }, + } + + appleGVK := schema.GroupVersionKind{Group: "fruit", Version: "1", Kind: "apple"} + applePolicy := &policiesfakes.FakePolicy{ + GetNameStub: func() string { + return "apple" + }, + } + + mustExtractGVK := func(object client.Object) schema.GroupVersionKind { + switch object.GetName() { + case "apple": + return appleGVK + case "orange": + return orangeGVK + default: + return schema.GroupVersionKind{} + } + } + + mgr := policies.NewManager( + mustExtractGVK, + policies.ManagerConfig{ + Validator: &policiesfakes.FakeValidator{ + ValidateStub: func(_ policies.Policy) error { return errors.New("apple error") }, + ConflictsStub: func(_ policies.Policy, _ policies.Policy) bool { return true }, + }, + Generator: func(_ policies.Policy) []byte { + return []byte("apple") + }, + GVK: appleGVK, + }, + policies.ManagerConfig{ + Validator: &policiesfakes.FakeValidator{ + ValidateStub: func(_ policies.Policy) error { return errors.New("orange error") }, + ConflictsStub: func(_ policies.Policy, _ policies.Policy) bool { return false }, + }, + Generator: func(_ policies.Policy) []byte { + return []byte("orange") + }, + GVK: orangeGVK, + }, + ) + + Context("Validation", func() { + When("Policy is registered with manager", func() { + It("Validates the policy", func() { + err := mgr.Validate(applePolicy) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("apple error")) + + err = mgr.Validate(orangePolicy) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("orange error")) + }) + It("Returns whether the policies conflict", func() { + Expect(mgr.Conflicts(applePolicy, applePolicy)).To(BeTrue()) + Expect(mgr.Conflicts(orangePolicy, orangePolicy)).To(BeFalse()) + }) + }) + When("Policy is not registered with manager", func() { + It("Panics on call to validate", func() { + validate := func() { + _ = mgr.Validate(&policiesfakes.FakePolicy{}) + } + + Expect(validate).To(Panic()) + }) + It("panics on call to conflicts", func() { + conflict := func() { + _ = mgr.Conflicts(&policiesfakes.FakePolicy{}, &policiesfakes.FakePolicy{}) + } + + Expect(conflict).To(Panic()) + }) + }) + }) + Context("Generation", func() { + When("Policy is registered with manager", func() { + It("Generates the configuration for the policy", func() { + Expect(mgr.Generate(applePolicy)).To(Equal([]byte("apple"))) + Expect(mgr.Generate(orangePolicy)).To(Equal([]byte("orange"))) + }) + }) + When("Policy is not registered with manager", func() { + It("Panics on generate", func() { + generate := func() { + _ = mgr.Generate(&policiesfakes.FakePolicy{}) + } + + Expect(generate).To(Panic()) + }) + }) + }) +}) diff --git a/internal/mode/static/policies/policies_suite_test.go b/internal/mode/static/policies/policies_suite_test.go new file mode 100644 index 0000000000..6703df23f1 --- /dev/null +++ b/internal/mode/static/policies/policies_suite_test.go @@ -0,0 +1,13 @@ +package policies_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestPolicies(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Policies Suite") +} diff --git a/internal/mode/static/policies/policiesfakes/fake_config_generator.go b/internal/mode/static/policies/policiesfakes/fake_config_generator.go new file mode 100644 index 0000000000..92680ac251 --- /dev/null +++ b/internal/mode/static/policies/policiesfakes/fake_config_generator.go @@ -0,0 +1,111 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package policiesfakes + +import ( + "sync" + + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies" +) + +type FakeConfigGenerator struct { + GenerateStub func(policies.Policy) []byte + generateMutex sync.RWMutex + generateArgsForCall []struct { + arg1 policies.Policy + } + generateReturns struct { + result1 []byte + } + generateReturnsOnCall map[int]struct { + result1 []byte + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeConfigGenerator) Generate(arg1 policies.Policy) []byte { + fake.generateMutex.Lock() + ret, specificReturn := fake.generateReturnsOnCall[len(fake.generateArgsForCall)] + fake.generateArgsForCall = append(fake.generateArgsForCall, struct { + arg1 policies.Policy + }{arg1}) + stub := fake.GenerateStub + fakeReturns := fake.generateReturns + fake.recordInvocation("Generate", []interface{}{arg1}) + fake.generateMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeConfigGenerator) GenerateCallCount() int { + fake.generateMutex.RLock() + defer fake.generateMutex.RUnlock() + return len(fake.generateArgsForCall) +} + +func (fake *FakeConfigGenerator) GenerateCalls(stub func(policies.Policy) []byte) { + fake.generateMutex.Lock() + defer fake.generateMutex.Unlock() + fake.GenerateStub = stub +} + +func (fake *FakeConfigGenerator) GenerateArgsForCall(i int) policies.Policy { + fake.generateMutex.RLock() + defer fake.generateMutex.RUnlock() + argsForCall := fake.generateArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeConfigGenerator) GenerateReturns(result1 []byte) { + fake.generateMutex.Lock() + defer fake.generateMutex.Unlock() + fake.GenerateStub = nil + fake.generateReturns = struct { + result1 []byte + }{result1} +} + +func (fake *FakeConfigGenerator) GenerateReturnsOnCall(i int, result1 []byte) { + fake.generateMutex.Lock() + defer fake.generateMutex.Unlock() + fake.GenerateStub = nil + if fake.generateReturnsOnCall == nil { + fake.generateReturnsOnCall = make(map[int]struct { + result1 []byte + }) + } + fake.generateReturnsOnCall[i] = struct { + result1 []byte + }{result1} +} + +func (fake *FakeConfigGenerator) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.generateMutex.RLock() + defer fake.generateMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeConfigGenerator) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ policies.ConfigGenerator = new(FakeConfigGenerator) diff --git a/internal/mode/static/policies/policiesfakes/fake_object_kind.go b/internal/mode/static/policies/policiesfakes/fake_object_kind.go new file mode 100644 index 0000000000..02f55e091b --- /dev/null +++ b/internal/mode/static/policies/policiesfakes/fake_object_kind.go @@ -0,0 +1,141 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package policiesfakes + +import ( + "sync" + + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type FakeObjectKind struct { + GroupVersionKindStub func() schema.GroupVersionKind + groupVersionKindMutex sync.RWMutex + groupVersionKindArgsForCall []struct { + } + groupVersionKindReturns struct { + result1 schema.GroupVersionKind + } + groupVersionKindReturnsOnCall map[int]struct { + result1 schema.GroupVersionKind + } + SetGroupVersionKindStub func(schema.GroupVersionKind) + setGroupVersionKindMutex sync.RWMutex + setGroupVersionKindArgsForCall []struct { + arg1 schema.GroupVersionKind + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeObjectKind) GroupVersionKind() schema.GroupVersionKind { + fake.groupVersionKindMutex.Lock() + ret, specificReturn := fake.groupVersionKindReturnsOnCall[len(fake.groupVersionKindArgsForCall)] + fake.groupVersionKindArgsForCall = append(fake.groupVersionKindArgsForCall, struct { + }{}) + stub := fake.GroupVersionKindStub + fakeReturns := fake.groupVersionKindReturns + fake.recordInvocation("GroupVersionKind", []interface{}{}) + fake.groupVersionKindMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeObjectKind) GroupVersionKindCallCount() int { + fake.groupVersionKindMutex.RLock() + defer fake.groupVersionKindMutex.RUnlock() + return len(fake.groupVersionKindArgsForCall) +} + +func (fake *FakeObjectKind) GroupVersionKindCalls(stub func() schema.GroupVersionKind) { + fake.groupVersionKindMutex.Lock() + defer fake.groupVersionKindMutex.Unlock() + fake.GroupVersionKindStub = stub +} + +func (fake *FakeObjectKind) GroupVersionKindReturns(result1 schema.GroupVersionKind) { + fake.groupVersionKindMutex.Lock() + defer fake.groupVersionKindMutex.Unlock() + fake.GroupVersionKindStub = nil + fake.groupVersionKindReturns = struct { + result1 schema.GroupVersionKind + }{result1} +} + +func (fake *FakeObjectKind) GroupVersionKindReturnsOnCall(i int, result1 schema.GroupVersionKind) { + fake.groupVersionKindMutex.Lock() + defer fake.groupVersionKindMutex.Unlock() + fake.GroupVersionKindStub = nil + if fake.groupVersionKindReturnsOnCall == nil { + fake.groupVersionKindReturnsOnCall = make(map[int]struct { + result1 schema.GroupVersionKind + }) + } + fake.groupVersionKindReturnsOnCall[i] = struct { + result1 schema.GroupVersionKind + }{result1} +} + +func (fake *FakeObjectKind) SetGroupVersionKind(arg1 schema.GroupVersionKind) { + fake.setGroupVersionKindMutex.Lock() + fake.setGroupVersionKindArgsForCall = append(fake.setGroupVersionKindArgsForCall, struct { + arg1 schema.GroupVersionKind + }{arg1}) + stub := fake.SetGroupVersionKindStub + fake.recordInvocation("SetGroupVersionKind", []interface{}{arg1}) + fake.setGroupVersionKindMutex.Unlock() + if stub != nil { + fake.SetGroupVersionKindStub(arg1) + } +} + +func (fake *FakeObjectKind) SetGroupVersionKindCallCount() int { + fake.setGroupVersionKindMutex.RLock() + defer fake.setGroupVersionKindMutex.RUnlock() + return len(fake.setGroupVersionKindArgsForCall) +} + +func (fake *FakeObjectKind) SetGroupVersionKindCalls(stub func(schema.GroupVersionKind)) { + fake.setGroupVersionKindMutex.Lock() + defer fake.setGroupVersionKindMutex.Unlock() + fake.SetGroupVersionKindStub = stub +} + +func (fake *FakeObjectKind) SetGroupVersionKindArgsForCall(i int) schema.GroupVersionKind { + fake.setGroupVersionKindMutex.RLock() + defer fake.setGroupVersionKindMutex.RUnlock() + argsForCall := fake.setGroupVersionKindArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeObjectKind) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.groupVersionKindMutex.RLock() + defer fake.groupVersionKindMutex.RUnlock() + fake.setGroupVersionKindMutex.RLock() + defer fake.setGroupVersionKindMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeObjectKind) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ schema.ObjectKind = new(FakeObjectKind) diff --git a/internal/mode/static/policies/policiesfakes/fake_policy.go b/internal/mode/static/policies/policiesfakes/fake_policy.go new file mode 100644 index 0000000000..649d78963f --- /dev/null +++ b/internal/mode/static/policies/policiesfakes/fake_policy.go @@ -0,0 +1,1916 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package policiesfakes + +import ( + "sync" + + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +type FakePolicy struct { + DeepCopyObjectStub func() runtime.Object + deepCopyObjectMutex sync.RWMutex + deepCopyObjectArgsForCall []struct { + } + deepCopyObjectReturns struct { + result1 runtime.Object + } + deepCopyObjectReturnsOnCall map[int]struct { + result1 runtime.Object + } + GetAnnotationsStub func() map[string]string + getAnnotationsMutex sync.RWMutex + getAnnotationsArgsForCall []struct { + } + getAnnotationsReturns struct { + result1 map[string]string + } + getAnnotationsReturnsOnCall map[int]struct { + result1 map[string]string + } + GetCreationTimestampStub func() v1.Time + getCreationTimestampMutex sync.RWMutex + getCreationTimestampArgsForCall []struct { + } + getCreationTimestampReturns struct { + result1 v1.Time + } + getCreationTimestampReturnsOnCall map[int]struct { + result1 v1.Time + } + GetDeletionGracePeriodSecondsStub func() *int64 + getDeletionGracePeriodSecondsMutex sync.RWMutex + getDeletionGracePeriodSecondsArgsForCall []struct { + } + getDeletionGracePeriodSecondsReturns struct { + result1 *int64 + } + getDeletionGracePeriodSecondsReturnsOnCall map[int]struct { + result1 *int64 + } + GetDeletionTimestampStub func() *v1.Time + getDeletionTimestampMutex sync.RWMutex + getDeletionTimestampArgsForCall []struct { + } + getDeletionTimestampReturns struct { + result1 *v1.Time + } + getDeletionTimestampReturnsOnCall map[int]struct { + result1 *v1.Time + } + GetFinalizersStub func() []string + getFinalizersMutex sync.RWMutex + getFinalizersArgsForCall []struct { + } + getFinalizersReturns struct { + result1 []string + } + getFinalizersReturnsOnCall map[int]struct { + result1 []string + } + GetGenerateNameStub func() string + getGenerateNameMutex sync.RWMutex + getGenerateNameArgsForCall []struct { + } + getGenerateNameReturns struct { + result1 string + } + getGenerateNameReturnsOnCall map[int]struct { + result1 string + } + GetGenerationStub func() int64 + getGenerationMutex sync.RWMutex + getGenerationArgsForCall []struct { + } + getGenerationReturns struct { + result1 int64 + } + getGenerationReturnsOnCall map[int]struct { + result1 int64 + } + GetLabelsStub func() map[string]string + getLabelsMutex sync.RWMutex + getLabelsArgsForCall []struct { + } + getLabelsReturns struct { + result1 map[string]string + } + getLabelsReturnsOnCall map[int]struct { + result1 map[string]string + } + GetManagedFieldsStub func() []v1.ManagedFieldsEntry + getManagedFieldsMutex sync.RWMutex + getManagedFieldsArgsForCall []struct { + } + getManagedFieldsReturns struct { + result1 []v1.ManagedFieldsEntry + } + getManagedFieldsReturnsOnCall map[int]struct { + result1 []v1.ManagedFieldsEntry + } + GetNameStub func() string + getNameMutex sync.RWMutex + getNameArgsForCall []struct { + } + getNameReturns struct { + result1 string + } + getNameReturnsOnCall map[int]struct { + result1 string + } + GetNamespaceStub func() string + getNamespaceMutex sync.RWMutex + getNamespaceArgsForCall []struct { + } + getNamespaceReturns struct { + result1 string + } + getNamespaceReturnsOnCall map[int]struct { + result1 string + } + GetObjectKindStub func() schema.ObjectKind + getObjectKindMutex sync.RWMutex + getObjectKindArgsForCall []struct { + } + getObjectKindReturns struct { + result1 schema.ObjectKind + } + getObjectKindReturnsOnCall map[int]struct { + result1 schema.ObjectKind + } + GetOwnerReferencesStub func() []v1.OwnerReference + getOwnerReferencesMutex sync.RWMutex + getOwnerReferencesArgsForCall []struct { + } + getOwnerReferencesReturns struct { + result1 []v1.OwnerReference + } + getOwnerReferencesReturnsOnCall map[int]struct { + result1 []v1.OwnerReference + } + GetPolicyStatusStub func() v1alpha2.PolicyStatus + getPolicyStatusMutex sync.RWMutex + getPolicyStatusArgsForCall []struct { + } + getPolicyStatusReturns struct { + result1 v1alpha2.PolicyStatus + } + getPolicyStatusReturnsOnCall map[int]struct { + result1 v1alpha2.PolicyStatus + } + GetResourceVersionStub func() string + getResourceVersionMutex sync.RWMutex + getResourceVersionArgsForCall []struct { + } + getResourceVersionReturns struct { + result1 string + } + getResourceVersionReturnsOnCall map[int]struct { + result1 string + } + GetSelfLinkStub func() string + getSelfLinkMutex sync.RWMutex + getSelfLinkArgsForCall []struct { + } + getSelfLinkReturns struct { + result1 string + } + getSelfLinkReturnsOnCall map[int]struct { + result1 string + } + GetTargetRefStub func() v1alpha2.LocalPolicyTargetReference + getTargetRefMutex sync.RWMutex + getTargetRefArgsForCall []struct { + } + getTargetRefReturns struct { + result1 v1alpha2.LocalPolicyTargetReference + } + getTargetRefReturnsOnCall map[int]struct { + result1 v1alpha2.LocalPolicyTargetReference + } + GetUIDStub func() types.UID + getUIDMutex sync.RWMutex + getUIDArgsForCall []struct { + } + getUIDReturns struct { + result1 types.UID + } + getUIDReturnsOnCall map[int]struct { + result1 types.UID + } + SetAnnotationsStub func(map[string]string) + setAnnotationsMutex sync.RWMutex + setAnnotationsArgsForCall []struct { + arg1 map[string]string + } + SetCreationTimestampStub func(v1.Time) + setCreationTimestampMutex sync.RWMutex + setCreationTimestampArgsForCall []struct { + arg1 v1.Time + } + SetDeletionGracePeriodSecondsStub func(*int64) + setDeletionGracePeriodSecondsMutex sync.RWMutex + setDeletionGracePeriodSecondsArgsForCall []struct { + arg1 *int64 + } + SetDeletionTimestampStub func(*v1.Time) + setDeletionTimestampMutex sync.RWMutex + setDeletionTimestampArgsForCall []struct { + arg1 *v1.Time + } + SetFinalizersStub func([]string) + setFinalizersMutex sync.RWMutex + setFinalizersArgsForCall []struct { + arg1 []string + } + SetGenerateNameStub func(string) + setGenerateNameMutex sync.RWMutex + setGenerateNameArgsForCall []struct { + arg1 string + } + SetGenerationStub func(int64) + setGenerationMutex sync.RWMutex + setGenerationArgsForCall []struct { + arg1 int64 + } + SetLabelsStub func(map[string]string) + setLabelsMutex sync.RWMutex + setLabelsArgsForCall []struct { + arg1 map[string]string + } + SetManagedFieldsStub func([]v1.ManagedFieldsEntry) + setManagedFieldsMutex sync.RWMutex + setManagedFieldsArgsForCall []struct { + arg1 []v1.ManagedFieldsEntry + } + SetNameStub func(string) + setNameMutex sync.RWMutex + setNameArgsForCall []struct { + arg1 string + } + SetNamespaceStub func(string) + setNamespaceMutex sync.RWMutex + setNamespaceArgsForCall []struct { + arg1 string + } + SetOwnerReferencesStub func([]v1.OwnerReference) + setOwnerReferencesMutex sync.RWMutex + setOwnerReferencesArgsForCall []struct { + arg1 []v1.OwnerReference + } + SetPolicyStatusStub func(v1alpha2.PolicyStatus) + setPolicyStatusMutex sync.RWMutex + setPolicyStatusArgsForCall []struct { + arg1 v1alpha2.PolicyStatus + } + SetResourceVersionStub func(string) + setResourceVersionMutex sync.RWMutex + setResourceVersionArgsForCall []struct { + arg1 string + } + SetSelfLinkStub func(string) + setSelfLinkMutex sync.RWMutex + setSelfLinkArgsForCall []struct { + arg1 string + } + SetUIDStub func(types.UID) + setUIDMutex sync.RWMutex + setUIDArgsForCall []struct { + arg1 types.UID + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakePolicy) DeepCopyObject() runtime.Object { + fake.deepCopyObjectMutex.Lock() + ret, specificReturn := fake.deepCopyObjectReturnsOnCall[len(fake.deepCopyObjectArgsForCall)] + fake.deepCopyObjectArgsForCall = append(fake.deepCopyObjectArgsForCall, struct { + }{}) + stub := fake.DeepCopyObjectStub + fakeReturns := fake.deepCopyObjectReturns + fake.recordInvocation("DeepCopyObject", []interface{}{}) + fake.deepCopyObjectMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakePolicy) DeepCopyObjectCallCount() int { + fake.deepCopyObjectMutex.RLock() + defer fake.deepCopyObjectMutex.RUnlock() + return len(fake.deepCopyObjectArgsForCall) +} + +func (fake *FakePolicy) DeepCopyObjectCalls(stub func() runtime.Object) { + fake.deepCopyObjectMutex.Lock() + defer fake.deepCopyObjectMutex.Unlock() + fake.DeepCopyObjectStub = stub +} + +func (fake *FakePolicy) DeepCopyObjectReturns(result1 runtime.Object) { + fake.deepCopyObjectMutex.Lock() + defer fake.deepCopyObjectMutex.Unlock() + fake.DeepCopyObjectStub = nil + fake.deepCopyObjectReturns = struct { + result1 runtime.Object + }{result1} +} + +func (fake *FakePolicy) DeepCopyObjectReturnsOnCall(i int, result1 runtime.Object) { + fake.deepCopyObjectMutex.Lock() + defer fake.deepCopyObjectMutex.Unlock() + fake.DeepCopyObjectStub = nil + if fake.deepCopyObjectReturnsOnCall == nil { + fake.deepCopyObjectReturnsOnCall = make(map[int]struct { + result1 runtime.Object + }) + } + fake.deepCopyObjectReturnsOnCall[i] = struct { + result1 runtime.Object + }{result1} +} + +func (fake *FakePolicy) GetAnnotations() map[string]string { + fake.getAnnotationsMutex.Lock() + ret, specificReturn := fake.getAnnotationsReturnsOnCall[len(fake.getAnnotationsArgsForCall)] + fake.getAnnotationsArgsForCall = append(fake.getAnnotationsArgsForCall, struct { + }{}) + stub := fake.GetAnnotationsStub + fakeReturns := fake.getAnnotationsReturns + fake.recordInvocation("GetAnnotations", []interface{}{}) + fake.getAnnotationsMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakePolicy) GetAnnotationsCallCount() int { + fake.getAnnotationsMutex.RLock() + defer fake.getAnnotationsMutex.RUnlock() + return len(fake.getAnnotationsArgsForCall) +} + +func (fake *FakePolicy) GetAnnotationsCalls(stub func() map[string]string) { + fake.getAnnotationsMutex.Lock() + defer fake.getAnnotationsMutex.Unlock() + fake.GetAnnotationsStub = stub +} + +func (fake *FakePolicy) GetAnnotationsReturns(result1 map[string]string) { + fake.getAnnotationsMutex.Lock() + defer fake.getAnnotationsMutex.Unlock() + fake.GetAnnotationsStub = nil + fake.getAnnotationsReturns = struct { + result1 map[string]string + }{result1} +} + +func (fake *FakePolicy) GetAnnotationsReturnsOnCall(i int, result1 map[string]string) { + fake.getAnnotationsMutex.Lock() + defer fake.getAnnotationsMutex.Unlock() + fake.GetAnnotationsStub = nil + if fake.getAnnotationsReturnsOnCall == nil { + fake.getAnnotationsReturnsOnCall = make(map[int]struct { + result1 map[string]string + }) + } + fake.getAnnotationsReturnsOnCall[i] = struct { + result1 map[string]string + }{result1} +} + +func (fake *FakePolicy) GetCreationTimestamp() v1.Time { + fake.getCreationTimestampMutex.Lock() + ret, specificReturn := fake.getCreationTimestampReturnsOnCall[len(fake.getCreationTimestampArgsForCall)] + fake.getCreationTimestampArgsForCall = append(fake.getCreationTimestampArgsForCall, struct { + }{}) + stub := fake.GetCreationTimestampStub + fakeReturns := fake.getCreationTimestampReturns + fake.recordInvocation("GetCreationTimestamp", []interface{}{}) + fake.getCreationTimestampMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakePolicy) GetCreationTimestampCallCount() int { + fake.getCreationTimestampMutex.RLock() + defer fake.getCreationTimestampMutex.RUnlock() + return len(fake.getCreationTimestampArgsForCall) +} + +func (fake *FakePolicy) GetCreationTimestampCalls(stub func() v1.Time) { + fake.getCreationTimestampMutex.Lock() + defer fake.getCreationTimestampMutex.Unlock() + fake.GetCreationTimestampStub = stub +} + +func (fake *FakePolicy) GetCreationTimestampReturns(result1 v1.Time) { + fake.getCreationTimestampMutex.Lock() + defer fake.getCreationTimestampMutex.Unlock() + fake.GetCreationTimestampStub = nil + fake.getCreationTimestampReturns = struct { + result1 v1.Time + }{result1} +} + +func (fake *FakePolicy) GetCreationTimestampReturnsOnCall(i int, result1 v1.Time) { + fake.getCreationTimestampMutex.Lock() + defer fake.getCreationTimestampMutex.Unlock() + fake.GetCreationTimestampStub = nil + if fake.getCreationTimestampReturnsOnCall == nil { + fake.getCreationTimestampReturnsOnCall = make(map[int]struct { + result1 v1.Time + }) + } + fake.getCreationTimestampReturnsOnCall[i] = struct { + result1 v1.Time + }{result1} +} + +func (fake *FakePolicy) GetDeletionGracePeriodSeconds() *int64 { + fake.getDeletionGracePeriodSecondsMutex.Lock() + ret, specificReturn := fake.getDeletionGracePeriodSecondsReturnsOnCall[len(fake.getDeletionGracePeriodSecondsArgsForCall)] + fake.getDeletionGracePeriodSecondsArgsForCall = append(fake.getDeletionGracePeriodSecondsArgsForCall, struct { + }{}) + stub := fake.GetDeletionGracePeriodSecondsStub + fakeReturns := fake.getDeletionGracePeriodSecondsReturns + fake.recordInvocation("GetDeletionGracePeriodSeconds", []interface{}{}) + fake.getDeletionGracePeriodSecondsMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakePolicy) GetDeletionGracePeriodSecondsCallCount() int { + fake.getDeletionGracePeriodSecondsMutex.RLock() + defer fake.getDeletionGracePeriodSecondsMutex.RUnlock() + return len(fake.getDeletionGracePeriodSecondsArgsForCall) +} + +func (fake *FakePolicy) GetDeletionGracePeriodSecondsCalls(stub func() *int64) { + fake.getDeletionGracePeriodSecondsMutex.Lock() + defer fake.getDeletionGracePeriodSecondsMutex.Unlock() + fake.GetDeletionGracePeriodSecondsStub = stub +} + +func (fake *FakePolicy) GetDeletionGracePeriodSecondsReturns(result1 *int64) { + fake.getDeletionGracePeriodSecondsMutex.Lock() + defer fake.getDeletionGracePeriodSecondsMutex.Unlock() + fake.GetDeletionGracePeriodSecondsStub = nil + fake.getDeletionGracePeriodSecondsReturns = struct { + result1 *int64 + }{result1} +} + +func (fake *FakePolicy) GetDeletionGracePeriodSecondsReturnsOnCall(i int, result1 *int64) { + fake.getDeletionGracePeriodSecondsMutex.Lock() + defer fake.getDeletionGracePeriodSecondsMutex.Unlock() + fake.GetDeletionGracePeriodSecondsStub = nil + if fake.getDeletionGracePeriodSecondsReturnsOnCall == nil { + fake.getDeletionGracePeriodSecondsReturnsOnCall = make(map[int]struct { + result1 *int64 + }) + } + fake.getDeletionGracePeriodSecondsReturnsOnCall[i] = struct { + result1 *int64 + }{result1} +} + +func (fake *FakePolicy) GetDeletionTimestamp() *v1.Time { + fake.getDeletionTimestampMutex.Lock() + ret, specificReturn := fake.getDeletionTimestampReturnsOnCall[len(fake.getDeletionTimestampArgsForCall)] + fake.getDeletionTimestampArgsForCall = append(fake.getDeletionTimestampArgsForCall, struct { + }{}) + stub := fake.GetDeletionTimestampStub + fakeReturns := fake.getDeletionTimestampReturns + fake.recordInvocation("GetDeletionTimestamp", []interface{}{}) + fake.getDeletionTimestampMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakePolicy) GetDeletionTimestampCallCount() int { + fake.getDeletionTimestampMutex.RLock() + defer fake.getDeletionTimestampMutex.RUnlock() + return len(fake.getDeletionTimestampArgsForCall) +} + +func (fake *FakePolicy) GetDeletionTimestampCalls(stub func() *v1.Time) { + fake.getDeletionTimestampMutex.Lock() + defer fake.getDeletionTimestampMutex.Unlock() + fake.GetDeletionTimestampStub = stub +} + +func (fake *FakePolicy) GetDeletionTimestampReturns(result1 *v1.Time) { + fake.getDeletionTimestampMutex.Lock() + defer fake.getDeletionTimestampMutex.Unlock() + fake.GetDeletionTimestampStub = nil + fake.getDeletionTimestampReturns = struct { + result1 *v1.Time + }{result1} +} + +func (fake *FakePolicy) GetDeletionTimestampReturnsOnCall(i int, result1 *v1.Time) { + fake.getDeletionTimestampMutex.Lock() + defer fake.getDeletionTimestampMutex.Unlock() + fake.GetDeletionTimestampStub = nil + if fake.getDeletionTimestampReturnsOnCall == nil { + fake.getDeletionTimestampReturnsOnCall = make(map[int]struct { + result1 *v1.Time + }) + } + fake.getDeletionTimestampReturnsOnCall[i] = struct { + result1 *v1.Time + }{result1} +} + +func (fake *FakePolicy) GetFinalizers() []string { + fake.getFinalizersMutex.Lock() + ret, specificReturn := fake.getFinalizersReturnsOnCall[len(fake.getFinalizersArgsForCall)] + fake.getFinalizersArgsForCall = append(fake.getFinalizersArgsForCall, struct { + }{}) + stub := fake.GetFinalizersStub + fakeReturns := fake.getFinalizersReturns + fake.recordInvocation("GetFinalizers", []interface{}{}) + fake.getFinalizersMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakePolicy) GetFinalizersCallCount() int { + fake.getFinalizersMutex.RLock() + defer fake.getFinalizersMutex.RUnlock() + return len(fake.getFinalizersArgsForCall) +} + +func (fake *FakePolicy) GetFinalizersCalls(stub func() []string) { + fake.getFinalizersMutex.Lock() + defer fake.getFinalizersMutex.Unlock() + fake.GetFinalizersStub = stub +} + +func (fake *FakePolicy) GetFinalizersReturns(result1 []string) { + fake.getFinalizersMutex.Lock() + defer fake.getFinalizersMutex.Unlock() + fake.GetFinalizersStub = nil + fake.getFinalizersReturns = struct { + result1 []string + }{result1} +} + +func (fake *FakePolicy) GetFinalizersReturnsOnCall(i int, result1 []string) { + fake.getFinalizersMutex.Lock() + defer fake.getFinalizersMutex.Unlock() + fake.GetFinalizersStub = nil + if fake.getFinalizersReturnsOnCall == nil { + fake.getFinalizersReturnsOnCall = make(map[int]struct { + result1 []string + }) + } + fake.getFinalizersReturnsOnCall[i] = struct { + result1 []string + }{result1} +} + +func (fake *FakePolicy) GetGenerateName() string { + fake.getGenerateNameMutex.Lock() + ret, specificReturn := fake.getGenerateNameReturnsOnCall[len(fake.getGenerateNameArgsForCall)] + fake.getGenerateNameArgsForCall = append(fake.getGenerateNameArgsForCall, struct { + }{}) + stub := fake.GetGenerateNameStub + fakeReturns := fake.getGenerateNameReturns + fake.recordInvocation("GetGenerateName", []interface{}{}) + fake.getGenerateNameMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakePolicy) GetGenerateNameCallCount() int { + fake.getGenerateNameMutex.RLock() + defer fake.getGenerateNameMutex.RUnlock() + return len(fake.getGenerateNameArgsForCall) +} + +func (fake *FakePolicy) GetGenerateNameCalls(stub func() string) { + fake.getGenerateNameMutex.Lock() + defer fake.getGenerateNameMutex.Unlock() + fake.GetGenerateNameStub = stub +} + +func (fake *FakePolicy) GetGenerateNameReturns(result1 string) { + fake.getGenerateNameMutex.Lock() + defer fake.getGenerateNameMutex.Unlock() + fake.GetGenerateNameStub = nil + fake.getGenerateNameReturns = struct { + result1 string + }{result1} +} + +func (fake *FakePolicy) GetGenerateNameReturnsOnCall(i int, result1 string) { + fake.getGenerateNameMutex.Lock() + defer fake.getGenerateNameMutex.Unlock() + fake.GetGenerateNameStub = nil + if fake.getGenerateNameReturnsOnCall == nil { + fake.getGenerateNameReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.getGenerateNameReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *FakePolicy) GetGeneration() int64 { + fake.getGenerationMutex.Lock() + ret, specificReturn := fake.getGenerationReturnsOnCall[len(fake.getGenerationArgsForCall)] + fake.getGenerationArgsForCall = append(fake.getGenerationArgsForCall, struct { + }{}) + stub := fake.GetGenerationStub + fakeReturns := fake.getGenerationReturns + fake.recordInvocation("GetGeneration", []interface{}{}) + fake.getGenerationMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakePolicy) GetGenerationCallCount() int { + fake.getGenerationMutex.RLock() + defer fake.getGenerationMutex.RUnlock() + return len(fake.getGenerationArgsForCall) +} + +func (fake *FakePolicy) GetGenerationCalls(stub func() int64) { + fake.getGenerationMutex.Lock() + defer fake.getGenerationMutex.Unlock() + fake.GetGenerationStub = stub +} + +func (fake *FakePolicy) GetGenerationReturns(result1 int64) { + fake.getGenerationMutex.Lock() + defer fake.getGenerationMutex.Unlock() + fake.GetGenerationStub = nil + fake.getGenerationReturns = struct { + result1 int64 + }{result1} +} + +func (fake *FakePolicy) GetGenerationReturnsOnCall(i int, result1 int64) { + fake.getGenerationMutex.Lock() + defer fake.getGenerationMutex.Unlock() + fake.GetGenerationStub = nil + if fake.getGenerationReturnsOnCall == nil { + fake.getGenerationReturnsOnCall = make(map[int]struct { + result1 int64 + }) + } + fake.getGenerationReturnsOnCall[i] = struct { + result1 int64 + }{result1} +} + +func (fake *FakePolicy) GetLabels() map[string]string { + fake.getLabelsMutex.Lock() + ret, specificReturn := fake.getLabelsReturnsOnCall[len(fake.getLabelsArgsForCall)] + fake.getLabelsArgsForCall = append(fake.getLabelsArgsForCall, struct { + }{}) + stub := fake.GetLabelsStub + fakeReturns := fake.getLabelsReturns + fake.recordInvocation("GetLabels", []interface{}{}) + fake.getLabelsMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakePolicy) GetLabelsCallCount() int { + fake.getLabelsMutex.RLock() + defer fake.getLabelsMutex.RUnlock() + return len(fake.getLabelsArgsForCall) +} + +func (fake *FakePolicy) GetLabelsCalls(stub func() map[string]string) { + fake.getLabelsMutex.Lock() + defer fake.getLabelsMutex.Unlock() + fake.GetLabelsStub = stub +} + +func (fake *FakePolicy) GetLabelsReturns(result1 map[string]string) { + fake.getLabelsMutex.Lock() + defer fake.getLabelsMutex.Unlock() + fake.GetLabelsStub = nil + fake.getLabelsReturns = struct { + result1 map[string]string + }{result1} +} + +func (fake *FakePolicy) GetLabelsReturnsOnCall(i int, result1 map[string]string) { + fake.getLabelsMutex.Lock() + defer fake.getLabelsMutex.Unlock() + fake.GetLabelsStub = nil + if fake.getLabelsReturnsOnCall == nil { + fake.getLabelsReturnsOnCall = make(map[int]struct { + result1 map[string]string + }) + } + fake.getLabelsReturnsOnCall[i] = struct { + result1 map[string]string + }{result1} +} + +func (fake *FakePolicy) GetManagedFields() []v1.ManagedFieldsEntry { + fake.getManagedFieldsMutex.Lock() + ret, specificReturn := fake.getManagedFieldsReturnsOnCall[len(fake.getManagedFieldsArgsForCall)] + fake.getManagedFieldsArgsForCall = append(fake.getManagedFieldsArgsForCall, struct { + }{}) + stub := fake.GetManagedFieldsStub + fakeReturns := fake.getManagedFieldsReturns + fake.recordInvocation("GetManagedFields", []interface{}{}) + fake.getManagedFieldsMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakePolicy) GetManagedFieldsCallCount() int { + fake.getManagedFieldsMutex.RLock() + defer fake.getManagedFieldsMutex.RUnlock() + return len(fake.getManagedFieldsArgsForCall) +} + +func (fake *FakePolicy) GetManagedFieldsCalls(stub func() []v1.ManagedFieldsEntry) { + fake.getManagedFieldsMutex.Lock() + defer fake.getManagedFieldsMutex.Unlock() + fake.GetManagedFieldsStub = stub +} + +func (fake *FakePolicy) GetManagedFieldsReturns(result1 []v1.ManagedFieldsEntry) { + fake.getManagedFieldsMutex.Lock() + defer fake.getManagedFieldsMutex.Unlock() + fake.GetManagedFieldsStub = nil + fake.getManagedFieldsReturns = struct { + result1 []v1.ManagedFieldsEntry + }{result1} +} + +func (fake *FakePolicy) GetManagedFieldsReturnsOnCall(i int, result1 []v1.ManagedFieldsEntry) { + fake.getManagedFieldsMutex.Lock() + defer fake.getManagedFieldsMutex.Unlock() + fake.GetManagedFieldsStub = nil + if fake.getManagedFieldsReturnsOnCall == nil { + fake.getManagedFieldsReturnsOnCall = make(map[int]struct { + result1 []v1.ManagedFieldsEntry + }) + } + fake.getManagedFieldsReturnsOnCall[i] = struct { + result1 []v1.ManagedFieldsEntry + }{result1} +} + +func (fake *FakePolicy) GetName() string { + fake.getNameMutex.Lock() + ret, specificReturn := fake.getNameReturnsOnCall[len(fake.getNameArgsForCall)] + fake.getNameArgsForCall = append(fake.getNameArgsForCall, struct { + }{}) + stub := fake.GetNameStub + fakeReturns := fake.getNameReturns + fake.recordInvocation("GetName", []interface{}{}) + fake.getNameMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakePolicy) GetNameCallCount() int { + fake.getNameMutex.RLock() + defer fake.getNameMutex.RUnlock() + return len(fake.getNameArgsForCall) +} + +func (fake *FakePolicy) GetNameCalls(stub func() string) { + fake.getNameMutex.Lock() + defer fake.getNameMutex.Unlock() + fake.GetNameStub = stub +} + +func (fake *FakePolicy) GetNameReturns(result1 string) { + fake.getNameMutex.Lock() + defer fake.getNameMutex.Unlock() + fake.GetNameStub = nil + fake.getNameReturns = struct { + result1 string + }{result1} +} + +func (fake *FakePolicy) GetNameReturnsOnCall(i int, result1 string) { + fake.getNameMutex.Lock() + defer fake.getNameMutex.Unlock() + fake.GetNameStub = nil + if fake.getNameReturnsOnCall == nil { + fake.getNameReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.getNameReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *FakePolicy) GetNamespace() string { + fake.getNamespaceMutex.Lock() + ret, specificReturn := fake.getNamespaceReturnsOnCall[len(fake.getNamespaceArgsForCall)] + fake.getNamespaceArgsForCall = append(fake.getNamespaceArgsForCall, struct { + }{}) + stub := fake.GetNamespaceStub + fakeReturns := fake.getNamespaceReturns + fake.recordInvocation("GetNamespace", []interface{}{}) + fake.getNamespaceMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakePolicy) GetNamespaceCallCount() int { + fake.getNamespaceMutex.RLock() + defer fake.getNamespaceMutex.RUnlock() + return len(fake.getNamespaceArgsForCall) +} + +func (fake *FakePolicy) GetNamespaceCalls(stub func() string) { + fake.getNamespaceMutex.Lock() + defer fake.getNamespaceMutex.Unlock() + fake.GetNamespaceStub = stub +} + +func (fake *FakePolicy) GetNamespaceReturns(result1 string) { + fake.getNamespaceMutex.Lock() + defer fake.getNamespaceMutex.Unlock() + fake.GetNamespaceStub = nil + fake.getNamespaceReturns = struct { + result1 string + }{result1} +} + +func (fake *FakePolicy) GetNamespaceReturnsOnCall(i int, result1 string) { + fake.getNamespaceMutex.Lock() + defer fake.getNamespaceMutex.Unlock() + fake.GetNamespaceStub = nil + if fake.getNamespaceReturnsOnCall == nil { + fake.getNamespaceReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.getNamespaceReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *FakePolicy) GetObjectKind() schema.ObjectKind { + fake.getObjectKindMutex.Lock() + ret, specificReturn := fake.getObjectKindReturnsOnCall[len(fake.getObjectKindArgsForCall)] + fake.getObjectKindArgsForCall = append(fake.getObjectKindArgsForCall, struct { + }{}) + stub := fake.GetObjectKindStub + fakeReturns := fake.getObjectKindReturns + fake.recordInvocation("GetObjectKind", []interface{}{}) + fake.getObjectKindMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakePolicy) GetObjectKindCallCount() int { + fake.getObjectKindMutex.RLock() + defer fake.getObjectKindMutex.RUnlock() + return len(fake.getObjectKindArgsForCall) +} + +func (fake *FakePolicy) GetObjectKindCalls(stub func() schema.ObjectKind) { + fake.getObjectKindMutex.Lock() + defer fake.getObjectKindMutex.Unlock() + fake.GetObjectKindStub = stub +} + +func (fake *FakePolicy) GetObjectKindReturns(result1 schema.ObjectKind) { + fake.getObjectKindMutex.Lock() + defer fake.getObjectKindMutex.Unlock() + fake.GetObjectKindStub = nil + fake.getObjectKindReturns = struct { + result1 schema.ObjectKind + }{result1} +} + +func (fake *FakePolicy) GetObjectKindReturnsOnCall(i int, result1 schema.ObjectKind) { + fake.getObjectKindMutex.Lock() + defer fake.getObjectKindMutex.Unlock() + fake.GetObjectKindStub = nil + if fake.getObjectKindReturnsOnCall == nil { + fake.getObjectKindReturnsOnCall = make(map[int]struct { + result1 schema.ObjectKind + }) + } + fake.getObjectKindReturnsOnCall[i] = struct { + result1 schema.ObjectKind + }{result1} +} + +func (fake *FakePolicy) GetOwnerReferences() []v1.OwnerReference { + fake.getOwnerReferencesMutex.Lock() + ret, specificReturn := fake.getOwnerReferencesReturnsOnCall[len(fake.getOwnerReferencesArgsForCall)] + fake.getOwnerReferencesArgsForCall = append(fake.getOwnerReferencesArgsForCall, struct { + }{}) + stub := fake.GetOwnerReferencesStub + fakeReturns := fake.getOwnerReferencesReturns + fake.recordInvocation("GetOwnerReferences", []interface{}{}) + fake.getOwnerReferencesMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakePolicy) GetOwnerReferencesCallCount() int { + fake.getOwnerReferencesMutex.RLock() + defer fake.getOwnerReferencesMutex.RUnlock() + return len(fake.getOwnerReferencesArgsForCall) +} + +func (fake *FakePolicy) GetOwnerReferencesCalls(stub func() []v1.OwnerReference) { + fake.getOwnerReferencesMutex.Lock() + defer fake.getOwnerReferencesMutex.Unlock() + fake.GetOwnerReferencesStub = stub +} + +func (fake *FakePolicy) GetOwnerReferencesReturns(result1 []v1.OwnerReference) { + fake.getOwnerReferencesMutex.Lock() + defer fake.getOwnerReferencesMutex.Unlock() + fake.GetOwnerReferencesStub = nil + fake.getOwnerReferencesReturns = struct { + result1 []v1.OwnerReference + }{result1} +} + +func (fake *FakePolicy) GetOwnerReferencesReturnsOnCall(i int, result1 []v1.OwnerReference) { + fake.getOwnerReferencesMutex.Lock() + defer fake.getOwnerReferencesMutex.Unlock() + fake.GetOwnerReferencesStub = nil + if fake.getOwnerReferencesReturnsOnCall == nil { + fake.getOwnerReferencesReturnsOnCall = make(map[int]struct { + result1 []v1.OwnerReference + }) + } + fake.getOwnerReferencesReturnsOnCall[i] = struct { + result1 []v1.OwnerReference + }{result1} +} + +func (fake *FakePolicy) GetPolicyStatus() v1alpha2.PolicyStatus { + fake.getPolicyStatusMutex.Lock() + ret, specificReturn := fake.getPolicyStatusReturnsOnCall[len(fake.getPolicyStatusArgsForCall)] + fake.getPolicyStatusArgsForCall = append(fake.getPolicyStatusArgsForCall, struct { + }{}) + stub := fake.GetPolicyStatusStub + fakeReturns := fake.getPolicyStatusReturns + fake.recordInvocation("GetPolicyStatus", []interface{}{}) + fake.getPolicyStatusMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakePolicy) GetPolicyStatusCallCount() int { + fake.getPolicyStatusMutex.RLock() + defer fake.getPolicyStatusMutex.RUnlock() + return len(fake.getPolicyStatusArgsForCall) +} + +func (fake *FakePolicy) GetPolicyStatusCalls(stub func() v1alpha2.PolicyStatus) { + fake.getPolicyStatusMutex.Lock() + defer fake.getPolicyStatusMutex.Unlock() + fake.GetPolicyStatusStub = stub +} + +func (fake *FakePolicy) GetPolicyStatusReturns(result1 v1alpha2.PolicyStatus) { + fake.getPolicyStatusMutex.Lock() + defer fake.getPolicyStatusMutex.Unlock() + fake.GetPolicyStatusStub = nil + fake.getPolicyStatusReturns = struct { + result1 v1alpha2.PolicyStatus + }{result1} +} + +func (fake *FakePolicy) GetPolicyStatusReturnsOnCall(i int, result1 v1alpha2.PolicyStatus) { + fake.getPolicyStatusMutex.Lock() + defer fake.getPolicyStatusMutex.Unlock() + fake.GetPolicyStatusStub = nil + if fake.getPolicyStatusReturnsOnCall == nil { + fake.getPolicyStatusReturnsOnCall = make(map[int]struct { + result1 v1alpha2.PolicyStatus + }) + } + fake.getPolicyStatusReturnsOnCall[i] = struct { + result1 v1alpha2.PolicyStatus + }{result1} +} + +func (fake *FakePolicy) GetResourceVersion() string { + fake.getResourceVersionMutex.Lock() + ret, specificReturn := fake.getResourceVersionReturnsOnCall[len(fake.getResourceVersionArgsForCall)] + fake.getResourceVersionArgsForCall = append(fake.getResourceVersionArgsForCall, struct { + }{}) + stub := fake.GetResourceVersionStub + fakeReturns := fake.getResourceVersionReturns + fake.recordInvocation("GetResourceVersion", []interface{}{}) + fake.getResourceVersionMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakePolicy) GetResourceVersionCallCount() int { + fake.getResourceVersionMutex.RLock() + defer fake.getResourceVersionMutex.RUnlock() + return len(fake.getResourceVersionArgsForCall) +} + +func (fake *FakePolicy) GetResourceVersionCalls(stub func() string) { + fake.getResourceVersionMutex.Lock() + defer fake.getResourceVersionMutex.Unlock() + fake.GetResourceVersionStub = stub +} + +func (fake *FakePolicy) GetResourceVersionReturns(result1 string) { + fake.getResourceVersionMutex.Lock() + defer fake.getResourceVersionMutex.Unlock() + fake.GetResourceVersionStub = nil + fake.getResourceVersionReturns = struct { + result1 string + }{result1} +} + +func (fake *FakePolicy) GetResourceVersionReturnsOnCall(i int, result1 string) { + fake.getResourceVersionMutex.Lock() + defer fake.getResourceVersionMutex.Unlock() + fake.GetResourceVersionStub = nil + if fake.getResourceVersionReturnsOnCall == nil { + fake.getResourceVersionReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.getResourceVersionReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *FakePolicy) GetSelfLink() string { + fake.getSelfLinkMutex.Lock() + ret, specificReturn := fake.getSelfLinkReturnsOnCall[len(fake.getSelfLinkArgsForCall)] + fake.getSelfLinkArgsForCall = append(fake.getSelfLinkArgsForCall, struct { + }{}) + stub := fake.GetSelfLinkStub + fakeReturns := fake.getSelfLinkReturns + fake.recordInvocation("GetSelfLink", []interface{}{}) + fake.getSelfLinkMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakePolicy) GetSelfLinkCallCount() int { + fake.getSelfLinkMutex.RLock() + defer fake.getSelfLinkMutex.RUnlock() + return len(fake.getSelfLinkArgsForCall) +} + +func (fake *FakePolicy) GetSelfLinkCalls(stub func() string) { + fake.getSelfLinkMutex.Lock() + defer fake.getSelfLinkMutex.Unlock() + fake.GetSelfLinkStub = stub +} + +func (fake *FakePolicy) GetSelfLinkReturns(result1 string) { + fake.getSelfLinkMutex.Lock() + defer fake.getSelfLinkMutex.Unlock() + fake.GetSelfLinkStub = nil + fake.getSelfLinkReturns = struct { + result1 string + }{result1} +} + +func (fake *FakePolicy) GetSelfLinkReturnsOnCall(i int, result1 string) { + fake.getSelfLinkMutex.Lock() + defer fake.getSelfLinkMutex.Unlock() + fake.GetSelfLinkStub = nil + if fake.getSelfLinkReturnsOnCall == nil { + fake.getSelfLinkReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.getSelfLinkReturnsOnCall[i] = struct { + result1 string + }{result1} +} + +func (fake *FakePolicy) GetTargetRef() v1alpha2.LocalPolicyTargetReference { + fake.getTargetRefMutex.Lock() + ret, specificReturn := fake.getTargetRefReturnsOnCall[len(fake.getTargetRefArgsForCall)] + fake.getTargetRefArgsForCall = append(fake.getTargetRefArgsForCall, struct { + }{}) + stub := fake.GetTargetRefStub + fakeReturns := fake.getTargetRefReturns + fake.recordInvocation("GetTargetRef", []interface{}{}) + fake.getTargetRefMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakePolicy) GetTargetRefCallCount() int { + fake.getTargetRefMutex.RLock() + defer fake.getTargetRefMutex.RUnlock() + return len(fake.getTargetRefArgsForCall) +} + +func (fake *FakePolicy) GetTargetRefCalls(stub func() v1alpha2.LocalPolicyTargetReference) { + fake.getTargetRefMutex.Lock() + defer fake.getTargetRefMutex.Unlock() + fake.GetTargetRefStub = stub +} + +func (fake *FakePolicy) GetTargetRefReturns(result1 v1alpha2.LocalPolicyTargetReference) { + fake.getTargetRefMutex.Lock() + defer fake.getTargetRefMutex.Unlock() + fake.GetTargetRefStub = nil + fake.getTargetRefReturns = struct { + result1 v1alpha2.LocalPolicyTargetReference + }{result1} +} + +func (fake *FakePolicy) GetTargetRefReturnsOnCall(i int, result1 v1alpha2.LocalPolicyTargetReference) { + fake.getTargetRefMutex.Lock() + defer fake.getTargetRefMutex.Unlock() + fake.GetTargetRefStub = nil + if fake.getTargetRefReturnsOnCall == nil { + fake.getTargetRefReturnsOnCall = make(map[int]struct { + result1 v1alpha2.LocalPolicyTargetReference + }) + } + fake.getTargetRefReturnsOnCall[i] = struct { + result1 v1alpha2.LocalPolicyTargetReference + }{result1} +} + +func (fake *FakePolicy) GetUID() types.UID { + fake.getUIDMutex.Lock() + ret, specificReturn := fake.getUIDReturnsOnCall[len(fake.getUIDArgsForCall)] + fake.getUIDArgsForCall = append(fake.getUIDArgsForCall, struct { + }{}) + stub := fake.GetUIDStub + fakeReturns := fake.getUIDReturns + fake.recordInvocation("GetUID", []interface{}{}) + fake.getUIDMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakePolicy) GetUIDCallCount() int { + fake.getUIDMutex.RLock() + defer fake.getUIDMutex.RUnlock() + return len(fake.getUIDArgsForCall) +} + +func (fake *FakePolicy) GetUIDCalls(stub func() types.UID) { + fake.getUIDMutex.Lock() + defer fake.getUIDMutex.Unlock() + fake.GetUIDStub = stub +} + +func (fake *FakePolicy) GetUIDReturns(result1 types.UID) { + fake.getUIDMutex.Lock() + defer fake.getUIDMutex.Unlock() + fake.GetUIDStub = nil + fake.getUIDReturns = struct { + result1 types.UID + }{result1} +} + +func (fake *FakePolicy) GetUIDReturnsOnCall(i int, result1 types.UID) { + fake.getUIDMutex.Lock() + defer fake.getUIDMutex.Unlock() + fake.GetUIDStub = nil + if fake.getUIDReturnsOnCall == nil { + fake.getUIDReturnsOnCall = make(map[int]struct { + result1 types.UID + }) + } + fake.getUIDReturnsOnCall[i] = struct { + result1 types.UID + }{result1} +} + +func (fake *FakePolicy) SetAnnotations(arg1 map[string]string) { + fake.setAnnotationsMutex.Lock() + fake.setAnnotationsArgsForCall = append(fake.setAnnotationsArgsForCall, struct { + arg1 map[string]string + }{arg1}) + stub := fake.SetAnnotationsStub + fake.recordInvocation("SetAnnotations", []interface{}{arg1}) + fake.setAnnotationsMutex.Unlock() + if stub != nil { + fake.SetAnnotationsStub(arg1) + } +} + +func (fake *FakePolicy) SetAnnotationsCallCount() int { + fake.setAnnotationsMutex.RLock() + defer fake.setAnnotationsMutex.RUnlock() + return len(fake.setAnnotationsArgsForCall) +} + +func (fake *FakePolicy) SetAnnotationsCalls(stub func(map[string]string)) { + fake.setAnnotationsMutex.Lock() + defer fake.setAnnotationsMutex.Unlock() + fake.SetAnnotationsStub = stub +} + +func (fake *FakePolicy) SetAnnotationsArgsForCall(i int) map[string]string { + fake.setAnnotationsMutex.RLock() + defer fake.setAnnotationsMutex.RUnlock() + argsForCall := fake.setAnnotationsArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakePolicy) SetCreationTimestamp(arg1 v1.Time) { + fake.setCreationTimestampMutex.Lock() + fake.setCreationTimestampArgsForCall = append(fake.setCreationTimestampArgsForCall, struct { + arg1 v1.Time + }{arg1}) + stub := fake.SetCreationTimestampStub + fake.recordInvocation("SetCreationTimestamp", []interface{}{arg1}) + fake.setCreationTimestampMutex.Unlock() + if stub != nil { + fake.SetCreationTimestampStub(arg1) + } +} + +func (fake *FakePolicy) SetCreationTimestampCallCount() int { + fake.setCreationTimestampMutex.RLock() + defer fake.setCreationTimestampMutex.RUnlock() + return len(fake.setCreationTimestampArgsForCall) +} + +func (fake *FakePolicy) SetCreationTimestampCalls(stub func(v1.Time)) { + fake.setCreationTimestampMutex.Lock() + defer fake.setCreationTimestampMutex.Unlock() + fake.SetCreationTimestampStub = stub +} + +func (fake *FakePolicy) SetCreationTimestampArgsForCall(i int) v1.Time { + fake.setCreationTimestampMutex.RLock() + defer fake.setCreationTimestampMutex.RUnlock() + argsForCall := fake.setCreationTimestampArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakePolicy) SetDeletionGracePeriodSeconds(arg1 *int64) { + fake.setDeletionGracePeriodSecondsMutex.Lock() + fake.setDeletionGracePeriodSecondsArgsForCall = append(fake.setDeletionGracePeriodSecondsArgsForCall, struct { + arg1 *int64 + }{arg1}) + stub := fake.SetDeletionGracePeriodSecondsStub + fake.recordInvocation("SetDeletionGracePeriodSeconds", []interface{}{arg1}) + fake.setDeletionGracePeriodSecondsMutex.Unlock() + if stub != nil { + fake.SetDeletionGracePeriodSecondsStub(arg1) + } +} + +func (fake *FakePolicy) SetDeletionGracePeriodSecondsCallCount() int { + fake.setDeletionGracePeriodSecondsMutex.RLock() + defer fake.setDeletionGracePeriodSecondsMutex.RUnlock() + return len(fake.setDeletionGracePeriodSecondsArgsForCall) +} + +func (fake *FakePolicy) SetDeletionGracePeriodSecondsCalls(stub func(*int64)) { + fake.setDeletionGracePeriodSecondsMutex.Lock() + defer fake.setDeletionGracePeriodSecondsMutex.Unlock() + fake.SetDeletionGracePeriodSecondsStub = stub +} + +func (fake *FakePolicy) SetDeletionGracePeriodSecondsArgsForCall(i int) *int64 { + fake.setDeletionGracePeriodSecondsMutex.RLock() + defer fake.setDeletionGracePeriodSecondsMutex.RUnlock() + argsForCall := fake.setDeletionGracePeriodSecondsArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakePolicy) SetDeletionTimestamp(arg1 *v1.Time) { + fake.setDeletionTimestampMutex.Lock() + fake.setDeletionTimestampArgsForCall = append(fake.setDeletionTimestampArgsForCall, struct { + arg1 *v1.Time + }{arg1}) + stub := fake.SetDeletionTimestampStub + fake.recordInvocation("SetDeletionTimestamp", []interface{}{arg1}) + fake.setDeletionTimestampMutex.Unlock() + if stub != nil { + fake.SetDeletionTimestampStub(arg1) + } +} + +func (fake *FakePolicy) SetDeletionTimestampCallCount() int { + fake.setDeletionTimestampMutex.RLock() + defer fake.setDeletionTimestampMutex.RUnlock() + return len(fake.setDeletionTimestampArgsForCall) +} + +func (fake *FakePolicy) SetDeletionTimestampCalls(stub func(*v1.Time)) { + fake.setDeletionTimestampMutex.Lock() + defer fake.setDeletionTimestampMutex.Unlock() + fake.SetDeletionTimestampStub = stub +} + +func (fake *FakePolicy) SetDeletionTimestampArgsForCall(i int) *v1.Time { + fake.setDeletionTimestampMutex.RLock() + defer fake.setDeletionTimestampMutex.RUnlock() + argsForCall := fake.setDeletionTimestampArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakePolicy) SetFinalizers(arg1 []string) { + var arg1Copy []string + if arg1 != nil { + arg1Copy = make([]string, len(arg1)) + copy(arg1Copy, arg1) + } + fake.setFinalizersMutex.Lock() + fake.setFinalizersArgsForCall = append(fake.setFinalizersArgsForCall, struct { + arg1 []string + }{arg1Copy}) + stub := fake.SetFinalizersStub + fake.recordInvocation("SetFinalizers", []interface{}{arg1Copy}) + fake.setFinalizersMutex.Unlock() + if stub != nil { + fake.SetFinalizersStub(arg1) + } +} + +func (fake *FakePolicy) SetFinalizersCallCount() int { + fake.setFinalizersMutex.RLock() + defer fake.setFinalizersMutex.RUnlock() + return len(fake.setFinalizersArgsForCall) +} + +func (fake *FakePolicy) SetFinalizersCalls(stub func([]string)) { + fake.setFinalizersMutex.Lock() + defer fake.setFinalizersMutex.Unlock() + fake.SetFinalizersStub = stub +} + +func (fake *FakePolicy) SetFinalizersArgsForCall(i int) []string { + fake.setFinalizersMutex.RLock() + defer fake.setFinalizersMutex.RUnlock() + argsForCall := fake.setFinalizersArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakePolicy) SetGenerateName(arg1 string) { + fake.setGenerateNameMutex.Lock() + fake.setGenerateNameArgsForCall = append(fake.setGenerateNameArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.SetGenerateNameStub + fake.recordInvocation("SetGenerateName", []interface{}{arg1}) + fake.setGenerateNameMutex.Unlock() + if stub != nil { + fake.SetGenerateNameStub(arg1) + } +} + +func (fake *FakePolicy) SetGenerateNameCallCount() int { + fake.setGenerateNameMutex.RLock() + defer fake.setGenerateNameMutex.RUnlock() + return len(fake.setGenerateNameArgsForCall) +} + +func (fake *FakePolicy) SetGenerateNameCalls(stub func(string)) { + fake.setGenerateNameMutex.Lock() + defer fake.setGenerateNameMutex.Unlock() + fake.SetGenerateNameStub = stub +} + +func (fake *FakePolicy) SetGenerateNameArgsForCall(i int) string { + fake.setGenerateNameMutex.RLock() + defer fake.setGenerateNameMutex.RUnlock() + argsForCall := fake.setGenerateNameArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakePolicy) SetGeneration(arg1 int64) { + fake.setGenerationMutex.Lock() + fake.setGenerationArgsForCall = append(fake.setGenerationArgsForCall, struct { + arg1 int64 + }{arg1}) + stub := fake.SetGenerationStub + fake.recordInvocation("SetGeneration", []interface{}{arg1}) + fake.setGenerationMutex.Unlock() + if stub != nil { + fake.SetGenerationStub(arg1) + } +} + +func (fake *FakePolicy) SetGenerationCallCount() int { + fake.setGenerationMutex.RLock() + defer fake.setGenerationMutex.RUnlock() + return len(fake.setGenerationArgsForCall) +} + +func (fake *FakePolicy) SetGenerationCalls(stub func(int64)) { + fake.setGenerationMutex.Lock() + defer fake.setGenerationMutex.Unlock() + fake.SetGenerationStub = stub +} + +func (fake *FakePolicy) SetGenerationArgsForCall(i int) int64 { + fake.setGenerationMutex.RLock() + defer fake.setGenerationMutex.RUnlock() + argsForCall := fake.setGenerationArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakePolicy) SetLabels(arg1 map[string]string) { + fake.setLabelsMutex.Lock() + fake.setLabelsArgsForCall = append(fake.setLabelsArgsForCall, struct { + arg1 map[string]string + }{arg1}) + stub := fake.SetLabelsStub + fake.recordInvocation("SetLabels", []interface{}{arg1}) + fake.setLabelsMutex.Unlock() + if stub != nil { + fake.SetLabelsStub(arg1) + } +} + +func (fake *FakePolicy) SetLabelsCallCount() int { + fake.setLabelsMutex.RLock() + defer fake.setLabelsMutex.RUnlock() + return len(fake.setLabelsArgsForCall) +} + +func (fake *FakePolicy) SetLabelsCalls(stub func(map[string]string)) { + fake.setLabelsMutex.Lock() + defer fake.setLabelsMutex.Unlock() + fake.SetLabelsStub = stub +} + +func (fake *FakePolicy) SetLabelsArgsForCall(i int) map[string]string { + fake.setLabelsMutex.RLock() + defer fake.setLabelsMutex.RUnlock() + argsForCall := fake.setLabelsArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakePolicy) SetManagedFields(arg1 []v1.ManagedFieldsEntry) { + var arg1Copy []v1.ManagedFieldsEntry + if arg1 != nil { + arg1Copy = make([]v1.ManagedFieldsEntry, len(arg1)) + copy(arg1Copy, arg1) + } + fake.setManagedFieldsMutex.Lock() + fake.setManagedFieldsArgsForCall = append(fake.setManagedFieldsArgsForCall, struct { + arg1 []v1.ManagedFieldsEntry + }{arg1Copy}) + stub := fake.SetManagedFieldsStub + fake.recordInvocation("SetManagedFields", []interface{}{arg1Copy}) + fake.setManagedFieldsMutex.Unlock() + if stub != nil { + fake.SetManagedFieldsStub(arg1) + } +} + +func (fake *FakePolicy) SetManagedFieldsCallCount() int { + fake.setManagedFieldsMutex.RLock() + defer fake.setManagedFieldsMutex.RUnlock() + return len(fake.setManagedFieldsArgsForCall) +} + +func (fake *FakePolicy) SetManagedFieldsCalls(stub func([]v1.ManagedFieldsEntry)) { + fake.setManagedFieldsMutex.Lock() + defer fake.setManagedFieldsMutex.Unlock() + fake.SetManagedFieldsStub = stub +} + +func (fake *FakePolicy) SetManagedFieldsArgsForCall(i int) []v1.ManagedFieldsEntry { + fake.setManagedFieldsMutex.RLock() + defer fake.setManagedFieldsMutex.RUnlock() + argsForCall := fake.setManagedFieldsArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakePolicy) SetName(arg1 string) { + fake.setNameMutex.Lock() + fake.setNameArgsForCall = append(fake.setNameArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.SetNameStub + fake.recordInvocation("SetName", []interface{}{arg1}) + fake.setNameMutex.Unlock() + if stub != nil { + fake.SetNameStub(arg1) + } +} + +func (fake *FakePolicy) SetNameCallCount() int { + fake.setNameMutex.RLock() + defer fake.setNameMutex.RUnlock() + return len(fake.setNameArgsForCall) +} + +func (fake *FakePolicy) SetNameCalls(stub func(string)) { + fake.setNameMutex.Lock() + defer fake.setNameMutex.Unlock() + fake.SetNameStub = stub +} + +func (fake *FakePolicy) SetNameArgsForCall(i int) string { + fake.setNameMutex.RLock() + defer fake.setNameMutex.RUnlock() + argsForCall := fake.setNameArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakePolicy) SetNamespace(arg1 string) { + fake.setNamespaceMutex.Lock() + fake.setNamespaceArgsForCall = append(fake.setNamespaceArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.SetNamespaceStub + fake.recordInvocation("SetNamespace", []interface{}{arg1}) + fake.setNamespaceMutex.Unlock() + if stub != nil { + fake.SetNamespaceStub(arg1) + } +} + +func (fake *FakePolicy) SetNamespaceCallCount() int { + fake.setNamespaceMutex.RLock() + defer fake.setNamespaceMutex.RUnlock() + return len(fake.setNamespaceArgsForCall) +} + +func (fake *FakePolicy) SetNamespaceCalls(stub func(string)) { + fake.setNamespaceMutex.Lock() + defer fake.setNamespaceMutex.Unlock() + fake.SetNamespaceStub = stub +} + +func (fake *FakePolicy) SetNamespaceArgsForCall(i int) string { + fake.setNamespaceMutex.RLock() + defer fake.setNamespaceMutex.RUnlock() + argsForCall := fake.setNamespaceArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakePolicy) SetOwnerReferences(arg1 []v1.OwnerReference) { + var arg1Copy []v1.OwnerReference + if arg1 != nil { + arg1Copy = make([]v1.OwnerReference, len(arg1)) + copy(arg1Copy, arg1) + } + fake.setOwnerReferencesMutex.Lock() + fake.setOwnerReferencesArgsForCall = append(fake.setOwnerReferencesArgsForCall, struct { + arg1 []v1.OwnerReference + }{arg1Copy}) + stub := fake.SetOwnerReferencesStub + fake.recordInvocation("SetOwnerReferences", []interface{}{arg1Copy}) + fake.setOwnerReferencesMutex.Unlock() + if stub != nil { + fake.SetOwnerReferencesStub(arg1) + } +} + +func (fake *FakePolicy) SetOwnerReferencesCallCount() int { + fake.setOwnerReferencesMutex.RLock() + defer fake.setOwnerReferencesMutex.RUnlock() + return len(fake.setOwnerReferencesArgsForCall) +} + +func (fake *FakePolicy) SetOwnerReferencesCalls(stub func([]v1.OwnerReference)) { + fake.setOwnerReferencesMutex.Lock() + defer fake.setOwnerReferencesMutex.Unlock() + fake.SetOwnerReferencesStub = stub +} + +func (fake *FakePolicy) SetOwnerReferencesArgsForCall(i int) []v1.OwnerReference { + fake.setOwnerReferencesMutex.RLock() + defer fake.setOwnerReferencesMutex.RUnlock() + argsForCall := fake.setOwnerReferencesArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakePolicy) SetPolicyStatus(arg1 v1alpha2.PolicyStatus) { + fake.setPolicyStatusMutex.Lock() + fake.setPolicyStatusArgsForCall = append(fake.setPolicyStatusArgsForCall, struct { + arg1 v1alpha2.PolicyStatus + }{arg1}) + stub := fake.SetPolicyStatusStub + fake.recordInvocation("SetPolicyStatus", []interface{}{arg1}) + fake.setPolicyStatusMutex.Unlock() + if stub != nil { + fake.SetPolicyStatusStub(arg1) + } +} + +func (fake *FakePolicy) SetPolicyStatusCallCount() int { + fake.setPolicyStatusMutex.RLock() + defer fake.setPolicyStatusMutex.RUnlock() + return len(fake.setPolicyStatusArgsForCall) +} + +func (fake *FakePolicy) SetPolicyStatusCalls(stub func(v1alpha2.PolicyStatus)) { + fake.setPolicyStatusMutex.Lock() + defer fake.setPolicyStatusMutex.Unlock() + fake.SetPolicyStatusStub = stub +} + +func (fake *FakePolicy) SetPolicyStatusArgsForCall(i int) v1alpha2.PolicyStatus { + fake.setPolicyStatusMutex.RLock() + defer fake.setPolicyStatusMutex.RUnlock() + argsForCall := fake.setPolicyStatusArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakePolicy) SetResourceVersion(arg1 string) { + fake.setResourceVersionMutex.Lock() + fake.setResourceVersionArgsForCall = append(fake.setResourceVersionArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.SetResourceVersionStub + fake.recordInvocation("SetResourceVersion", []interface{}{arg1}) + fake.setResourceVersionMutex.Unlock() + if stub != nil { + fake.SetResourceVersionStub(arg1) + } +} + +func (fake *FakePolicy) SetResourceVersionCallCount() int { + fake.setResourceVersionMutex.RLock() + defer fake.setResourceVersionMutex.RUnlock() + return len(fake.setResourceVersionArgsForCall) +} + +func (fake *FakePolicy) SetResourceVersionCalls(stub func(string)) { + fake.setResourceVersionMutex.Lock() + defer fake.setResourceVersionMutex.Unlock() + fake.SetResourceVersionStub = stub +} + +func (fake *FakePolicy) SetResourceVersionArgsForCall(i int) string { + fake.setResourceVersionMutex.RLock() + defer fake.setResourceVersionMutex.RUnlock() + argsForCall := fake.setResourceVersionArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakePolicy) SetSelfLink(arg1 string) { + fake.setSelfLinkMutex.Lock() + fake.setSelfLinkArgsForCall = append(fake.setSelfLinkArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.SetSelfLinkStub + fake.recordInvocation("SetSelfLink", []interface{}{arg1}) + fake.setSelfLinkMutex.Unlock() + if stub != nil { + fake.SetSelfLinkStub(arg1) + } +} + +func (fake *FakePolicy) SetSelfLinkCallCount() int { + fake.setSelfLinkMutex.RLock() + defer fake.setSelfLinkMutex.RUnlock() + return len(fake.setSelfLinkArgsForCall) +} + +func (fake *FakePolicy) SetSelfLinkCalls(stub func(string)) { + fake.setSelfLinkMutex.Lock() + defer fake.setSelfLinkMutex.Unlock() + fake.SetSelfLinkStub = stub +} + +func (fake *FakePolicy) SetSelfLinkArgsForCall(i int) string { + fake.setSelfLinkMutex.RLock() + defer fake.setSelfLinkMutex.RUnlock() + argsForCall := fake.setSelfLinkArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakePolicy) SetUID(arg1 types.UID) { + fake.setUIDMutex.Lock() + fake.setUIDArgsForCall = append(fake.setUIDArgsForCall, struct { + arg1 types.UID + }{arg1}) + stub := fake.SetUIDStub + fake.recordInvocation("SetUID", []interface{}{arg1}) + fake.setUIDMutex.Unlock() + if stub != nil { + fake.SetUIDStub(arg1) + } +} + +func (fake *FakePolicy) SetUIDCallCount() int { + fake.setUIDMutex.RLock() + defer fake.setUIDMutex.RUnlock() + return len(fake.setUIDArgsForCall) +} + +func (fake *FakePolicy) SetUIDCalls(stub func(types.UID)) { + fake.setUIDMutex.Lock() + defer fake.setUIDMutex.Unlock() + fake.SetUIDStub = stub +} + +func (fake *FakePolicy) SetUIDArgsForCall(i int) types.UID { + fake.setUIDMutex.RLock() + defer fake.setUIDMutex.RUnlock() + argsForCall := fake.setUIDArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakePolicy) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.deepCopyObjectMutex.RLock() + defer fake.deepCopyObjectMutex.RUnlock() + fake.getAnnotationsMutex.RLock() + defer fake.getAnnotationsMutex.RUnlock() + fake.getCreationTimestampMutex.RLock() + defer fake.getCreationTimestampMutex.RUnlock() + fake.getDeletionGracePeriodSecondsMutex.RLock() + defer fake.getDeletionGracePeriodSecondsMutex.RUnlock() + fake.getDeletionTimestampMutex.RLock() + defer fake.getDeletionTimestampMutex.RUnlock() + fake.getFinalizersMutex.RLock() + defer fake.getFinalizersMutex.RUnlock() + fake.getGenerateNameMutex.RLock() + defer fake.getGenerateNameMutex.RUnlock() + fake.getGenerationMutex.RLock() + defer fake.getGenerationMutex.RUnlock() + fake.getLabelsMutex.RLock() + defer fake.getLabelsMutex.RUnlock() + fake.getManagedFieldsMutex.RLock() + defer fake.getManagedFieldsMutex.RUnlock() + fake.getNameMutex.RLock() + defer fake.getNameMutex.RUnlock() + fake.getNamespaceMutex.RLock() + defer fake.getNamespaceMutex.RUnlock() + fake.getObjectKindMutex.RLock() + defer fake.getObjectKindMutex.RUnlock() + fake.getOwnerReferencesMutex.RLock() + defer fake.getOwnerReferencesMutex.RUnlock() + fake.getPolicyStatusMutex.RLock() + defer fake.getPolicyStatusMutex.RUnlock() + fake.getResourceVersionMutex.RLock() + defer fake.getResourceVersionMutex.RUnlock() + fake.getSelfLinkMutex.RLock() + defer fake.getSelfLinkMutex.RUnlock() + fake.getTargetRefMutex.RLock() + defer fake.getTargetRefMutex.RUnlock() + fake.getUIDMutex.RLock() + defer fake.getUIDMutex.RUnlock() + fake.setAnnotationsMutex.RLock() + defer fake.setAnnotationsMutex.RUnlock() + fake.setCreationTimestampMutex.RLock() + defer fake.setCreationTimestampMutex.RUnlock() + fake.setDeletionGracePeriodSecondsMutex.RLock() + defer fake.setDeletionGracePeriodSecondsMutex.RUnlock() + fake.setDeletionTimestampMutex.RLock() + defer fake.setDeletionTimestampMutex.RUnlock() + fake.setFinalizersMutex.RLock() + defer fake.setFinalizersMutex.RUnlock() + fake.setGenerateNameMutex.RLock() + defer fake.setGenerateNameMutex.RUnlock() + fake.setGenerationMutex.RLock() + defer fake.setGenerationMutex.RUnlock() + fake.setLabelsMutex.RLock() + defer fake.setLabelsMutex.RUnlock() + fake.setManagedFieldsMutex.RLock() + defer fake.setManagedFieldsMutex.RUnlock() + fake.setNameMutex.RLock() + defer fake.setNameMutex.RUnlock() + fake.setNamespaceMutex.RLock() + defer fake.setNamespaceMutex.RUnlock() + fake.setOwnerReferencesMutex.RLock() + defer fake.setOwnerReferencesMutex.RUnlock() + fake.setPolicyStatusMutex.RLock() + defer fake.setPolicyStatusMutex.RUnlock() + fake.setResourceVersionMutex.RLock() + defer fake.setResourceVersionMutex.RUnlock() + fake.setSelfLinkMutex.RLock() + defer fake.setSelfLinkMutex.RUnlock() + fake.setUIDMutex.RLock() + defer fake.setUIDMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakePolicy) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ policies.Policy = new(FakePolicy) diff --git a/internal/mode/static/policies/policiesfakes/fake_validator.go b/internal/mode/static/policies/policiesfakes/fake_validator.go new file mode 100644 index 0000000000..79a19826a6 --- /dev/null +++ b/internal/mode/static/policies/policiesfakes/fake_validator.go @@ -0,0 +1,187 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package policiesfakes + +import ( + "sync" + + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies" +) + +type FakeValidator struct { + ConflictsStub func(policies.Policy, policies.Policy) bool + conflictsMutex sync.RWMutex + conflictsArgsForCall []struct { + arg1 policies.Policy + arg2 policies.Policy + } + conflictsReturns struct { + result1 bool + } + conflictsReturnsOnCall map[int]struct { + result1 bool + } + ValidateStub func(policies.Policy) error + validateMutex sync.RWMutex + validateArgsForCall []struct { + arg1 policies.Policy + } + validateReturns struct { + result1 error + } + validateReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeValidator) Conflicts(arg1 policies.Policy, arg2 policies.Policy) bool { + fake.conflictsMutex.Lock() + ret, specificReturn := fake.conflictsReturnsOnCall[len(fake.conflictsArgsForCall)] + fake.conflictsArgsForCall = append(fake.conflictsArgsForCall, struct { + arg1 policies.Policy + arg2 policies.Policy + }{arg1, arg2}) + stub := fake.ConflictsStub + fakeReturns := fake.conflictsReturns + fake.recordInvocation("Conflicts", []interface{}{arg1, arg2}) + fake.conflictsMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeValidator) ConflictsCallCount() int { + fake.conflictsMutex.RLock() + defer fake.conflictsMutex.RUnlock() + return len(fake.conflictsArgsForCall) +} + +func (fake *FakeValidator) ConflictsCalls(stub func(policies.Policy, policies.Policy) bool) { + fake.conflictsMutex.Lock() + defer fake.conflictsMutex.Unlock() + fake.ConflictsStub = stub +} + +func (fake *FakeValidator) ConflictsArgsForCall(i int) (policies.Policy, policies.Policy) { + fake.conflictsMutex.RLock() + defer fake.conflictsMutex.RUnlock() + argsForCall := fake.conflictsArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeValidator) ConflictsReturns(result1 bool) { + fake.conflictsMutex.Lock() + defer fake.conflictsMutex.Unlock() + fake.ConflictsStub = nil + fake.conflictsReturns = struct { + result1 bool + }{result1} +} + +func (fake *FakeValidator) ConflictsReturnsOnCall(i int, result1 bool) { + fake.conflictsMutex.Lock() + defer fake.conflictsMutex.Unlock() + fake.ConflictsStub = nil + if fake.conflictsReturnsOnCall == nil { + fake.conflictsReturnsOnCall = make(map[int]struct { + result1 bool + }) + } + fake.conflictsReturnsOnCall[i] = struct { + result1 bool + }{result1} +} + +func (fake *FakeValidator) Validate(arg1 policies.Policy) error { + fake.validateMutex.Lock() + ret, specificReturn := fake.validateReturnsOnCall[len(fake.validateArgsForCall)] + fake.validateArgsForCall = append(fake.validateArgsForCall, struct { + arg1 policies.Policy + }{arg1}) + stub := fake.ValidateStub + fakeReturns := fake.validateReturns + fake.recordInvocation("Validate", []interface{}{arg1}) + fake.validateMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeValidator) ValidateCallCount() int { + fake.validateMutex.RLock() + defer fake.validateMutex.RUnlock() + return len(fake.validateArgsForCall) +} + +func (fake *FakeValidator) ValidateCalls(stub func(policies.Policy) error) { + fake.validateMutex.Lock() + defer fake.validateMutex.Unlock() + fake.ValidateStub = stub +} + +func (fake *FakeValidator) ValidateArgsForCall(i int) policies.Policy { + fake.validateMutex.RLock() + defer fake.validateMutex.RUnlock() + argsForCall := fake.validateArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeValidator) ValidateReturns(result1 error) { + fake.validateMutex.Lock() + defer fake.validateMutex.Unlock() + fake.ValidateStub = nil + fake.validateReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeValidator) ValidateReturnsOnCall(i int, result1 error) { + fake.validateMutex.Lock() + defer fake.validateMutex.Unlock() + fake.ValidateStub = nil + if fake.validateReturnsOnCall == nil { + fake.validateReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.validateReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeValidator) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.conflictsMutex.RLock() + defer fake.conflictsMutex.RUnlock() + fake.validateMutex.RLock() + defer fake.validateMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeValidator) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ policies.Validator = new(FakeValidator) diff --git a/internal/mode/static/policies/policy.go b/internal/mode/static/policies/policy.go new file mode 100644 index 0000000000..12135d8f8e --- /dev/null +++ b/internal/mode/static/policies/policy.go @@ -0,0 +1,28 @@ +package policies + +import ( + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate + +// Policy is an extension of client.Object. It adds methods that are common among all NGF Policies. +// +//counterfeiter:generate . Policy +type Policy interface { + GetTargetRef() v1alpha2.LocalPolicyTargetReference + GetPolicyStatus() v1alpha2.PolicyStatus + SetPolicyStatus(status v1alpha2.PolicyStatus) + client.Object +} + +// ConfigGenerator generates a slice of bytes containing the configuration from a Policy. +// +//counterfeiter:generate . ConfigGenerator +type ConfigGenerator interface { + Generate(policy Policy) []byte +} + +// We generate a mock of ObjectKind so that we can create fake policies and set their GVKs. +//counterfeiter:generate k8s.io/apimachinery/pkg/runtime/schema.ObjectKind diff --git a/internal/mode/static/sort/sort.go b/internal/mode/static/sort/sort.go index ee9db1b24b..6f25f13870 100644 --- a/internal/mode/static/sort/sort.go +++ b/internal/mode/static/sort/sort.go @@ -1,6 +1,9 @@ package sort -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) // LessObjectMeta compares two ObjectMetas according to the Gateway API conflict resolution guidelines. // See https://gateway-api.sigs.k8s.io/concepts/guidelines/?h=conflict#conflicts @@ -14,3 +17,21 @@ func LessObjectMeta(meta1 *metav1.ObjectMeta, meta2 *metav1.ObjectMeta) bool { return meta1.CreationTimestamp.Before(&meta2.CreationTimestamp) } + +// LessClientObject compares two client.Objects and returns true if: +// - the first object was created first, +// - the objects were created at the same time, or +// - the first object's name appears first in alphabetical order. +func LessClientObject(obj1 client.Object, obj2 client.Object) bool { + create1 := obj1.GetCreationTimestamp() + create2 := obj2.GetCreationTimestamp() + + if create1.Time.Equal(create2.Time) { + if obj1.GetNamespace() == obj2.GetNamespace() { + return obj1.GetName() < obj2.GetName() + } + return obj1.GetNamespace() < obj2.GetNamespace() + } + + return create1.Time.Before(create2.Time) +} diff --git a/internal/mode/static/sort/sort_test.go b/internal/mode/static/sort/sort_test.go index e7cac06551..d1b63e830d 100644 --- a/internal/mode/static/sort/sort_test.go +++ b/internal/mode/static/sort/sort_test.go @@ -6,6 +6,8 @@ import ( . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + v1 "sigs.k8s.io/gateway-api/apis/v1" ) func TestLessObjectMeta(t *testing.T) { @@ -27,7 +29,6 @@ func TestLessObjectMeta(t *testing.T) { meta2: &metav1.ObjectMeta{ Namespace: "ns1", Name: "meta2", - UID: "b", CreationTimestamp: later, }, name: "first is less by timestamp", @@ -89,3 +90,100 @@ func TestLessObjectMeta(t *testing.T) { }) } } + +func TestLessClientObject(t *testing.T) { + before := metav1.Now() + later := metav1.NewTime(before.Add(1 * time.Second)) + + tests := []struct { + obj1 client.Object + obj2 client.Object + name string + expected bool + }{ + { + name: "first is less by timestamp", + obj1: &v1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns1", + Name: "obj1", + CreationTimestamp: before, + }, + }, + obj2: &v1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns1", + Name: "obj2", + CreationTimestamp: later, + }, + }, + expected: true, + }, + { + name: "first is less by namespace", + obj1: &v1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns1", + Name: "obj1", + CreationTimestamp: before, + }, + }, + obj2: &v1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns2", + Name: "obj1", + CreationTimestamp: before, + }, + }, + expected: true, + }, + { + name: "first is less by name", + obj1: &v1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns1", + Name: "obj1", + CreationTimestamp: before, + }, + }, + obj2: &v1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns1", + Name: "obj2", + CreationTimestamp: before, + }, + }, + expected: true, + }, + { + name: "equal", + obj1: &v1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns1", + Name: "obj1", + CreationTimestamp: before, + }, + }, + obj2: &v1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns1", + Name: "obj1", + CreationTimestamp: before, + }, + }, + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + result := LessClientObject(test.obj1, test.obj2) + invertedResult := LessClientObject(test.obj2, test.obj1) + + g.Expect(result).To(Equal(test.expected)) + g.Expect(invertedResult).To(BeFalse()) + }) + } +} diff --git a/internal/mode/static/state/change_processor.go b/internal/mode/static/state/change_processor.go index 0a57d8cd92..e8342aa6c1 100644 --- a/internal/mode/static/state/change_processor.go +++ b/internal/mode/static/state/change_processor.go @@ -1,7 +1,6 @@ package state import ( - "fmt" "sync" "github.com/go-logr/logr" @@ -9,18 +8,17 @@ import ( discoveryV1 "k8s.io/api/discovery/v1" apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/apiutil" v1 "sigs.k8s.io/gateway-api/apis/v1" "sigs.k8s.io/gateway-api/apis/v1alpha3" "sigs.k8s.io/gateway-api/apis/v1beta1" ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/gatewayclass" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/graph" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation" ) @@ -42,8 +40,6 @@ const ( //counterfeiter:generate . ChangeProcessor -type extractGVKFunc func(obj client.Object) schema.GroupVersionKind - // ChangeProcessor processes the changes to resources and produces a graph-like representation // of the Gateway configuration. It only supports one GatewayClass resource. type ChangeProcessor interface { @@ -68,8 +64,8 @@ type ChangeProcessorConfig struct { Validators validation.Validators // EventRecorder records events for Kubernetes resources. EventRecorder record.EventRecorder - // Scheme is the Kubernetes scheme. - Scheme *runtime.Scheme + // MustExtractGVK is a function that extracts schema.GroupVersionKind from a client.Object. + MustExtractGVK kinds.MustExtractGVK // ProtectedPorts are the ports that may not be configured by a listener with a descriptive name of the ports. ProtectedPorts graph.ProtectedPorts // Logger is the logger for this Change Processor. @@ -110,14 +106,7 @@ func NewChangeProcessorImpl(cfg ChangeProcessorConfig) *ChangeProcessorImpl { ConfigMaps: make(map[types.NamespacedName]*apiv1.ConfigMap), NginxProxies: make(map[types.NamespacedName]*ngfAPI.NginxProxy), GRPCRoutes: make(map[types.NamespacedName]*v1.GRPCRoute), - } - - extractGVK := func(obj client.Object) schema.GroupVersionKind { - gvk, err := apiutil.GVKForObject(obj, cfg.Scheme) - if err != nil { - panic(fmt.Errorf("failed to get GVK for object %T: %w", obj, err)) - } - return gvk + NGFPolicies: make(map[graph.PolicyKey]policies.Policy), } processor := &ChangeProcessorImpl{ @@ -129,74 +118,93 @@ func NewChangeProcessorImpl(cfg ChangeProcessorConfig) *ChangeProcessorImpl { return processor.latestGraph != nil && processor.latestGraph.IsReferenced(obj, nsname) } + isNGFPolicyRelevant := func(obj client.Object, nsname types.NamespacedName) bool { + pol, ok := obj.(policies.Policy) + if !ok { + return false + } + + gvk := cfg.MustExtractGVK(obj) + + return processor.latestGraph != nil && processor.latestGraph.IsNGFPolicyRelevant(pol, gvk, nsname) + } + + // Use this object store for all NGF policies + commonPolicyObjectStore := newNGFPolicyObjectStore(clusterStore.NGFPolicies, cfg.MustExtractGVK) + trackingUpdater := newChangeTrackingUpdater( - extractGVK, + cfg.MustExtractGVK, []changeTrackingUpdaterObjectTypeCfg{ { - gvk: extractGVK(&v1.GatewayClass{}), + gvk: cfg.MustExtractGVK(&v1.GatewayClass{}), store: newObjectStoreMapAdapter(clusterStore.GatewayClasses), predicate: nil, }, { - gvk: extractGVK(&v1.Gateway{}), + gvk: cfg.MustExtractGVK(&v1.Gateway{}), store: newObjectStoreMapAdapter(clusterStore.Gateways), predicate: nil, }, { - gvk: extractGVK(&v1.HTTPRoute{}), + gvk: cfg.MustExtractGVK(&v1.HTTPRoute{}), store: newObjectStoreMapAdapter(clusterStore.HTTPRoutes), predicate: nil, }, { - gvk: extractGVK(&v1beta1.ReferenceGrant{}), + gvk: cfg.MustExtractGVK(&v1beta1.ReferenceGrant{}), store: newObjectStoreMapAdapter(clusterStore.ReferenceGrants), predicate: nil, }, { - gvk: extractGVK(&v1alpha3.BackendTLSPolicy{}), + gvk: cfg.MustExtractGVK(&v1alpha3.BackendTLSPolicy{}), store: newObjectStoreMapAdapter(clusterStore.BackendTLSPolicies), predicate: nil, }, { - gvk: extractGVK(&v1.GRPCRoute{}), + gvk: cfg.MustExtractGVK(&v1.GRPCRoute{}), store: newObjectStoreMapAdapter(clusterStore.GRPCRoutes), predicate: nil, }, { - gvk: extractGVK(&apiv1.Namespace{}), + gvk: cfg.MustExtractGVK(&apiv1.Namespace{}), store: newObjectStoreMapAdapter(clusterStore.Namespaces), predicate: funcPredicate{stateChanged: isReferenced}, }, { - gvk: extractGVK(&apiv1.Service{}), + gvk: cfg.MustExtractGVK(&apiv1.Service{}), store: newObjectStoreMapAdapter(clusterStore.Services), predicate: funcPredicate{stateChanged: isReferenced}, }, { - gvk: extractGVK(&discoveryV1.EndpointSlice{}), + gvk: cfg.MustExtractGVK(&discoveryV1.EndpointSlice{}), store: nil, predicate: funcPredicate{stateChanged: isReferenced}, }, { - gvk: extractGVK(&apiv1.Secret{}), + gvk: cfg.MustExtractGVK(&apiv1.Secret{}), store: newObjectStoreMapAdapter(clusterStore.Secrets), predicate: funcPredicate{stateChanged: isReferenced}, }, { - gvk: extractGVK(&apiv1.ConfigMap{}), + gvk: cfg.MustExtractGVK(&apiv1.ConfigMap{}), store: newObjectStoreMapAdapter(clusterStore.ConfigMaps), predicate: funcPredicate{stateChanged: isReferenced}, }, { - gvk: extractGVK(&apiext.CustomResourceDefinition{}), + gvk: cfg.MustExtractGVK(&apiext.CustomResourceDefinition{}), store: newObjectStoreMapAdapter(clusterStore.CRDMetadata), predicate: annotationChangedPredicate{annotation: gatewayclass.BundleVersionAnnotation}, }, { - gvk: extractGVK(&ngfAPI.NginxProxy{}), + gvk: cfg.MustExtractGVK(&ngfAPI.NginxProxy{}), store: newObjectStoreMapAdapter(clusterStore.NginxProxies), predicate: funcPredicate{stateChanged: isReferenced}, }, + { + gvk: cfg.MustExtractGVK(&ngfAPI.ClientSettingsPolicy{}), + store: commonPolicyObjectStore, + predicate: funcPredicate{stateChanged: isNGFPolicyRelevant}, + }, }, ) diff --git a/internal/mode/static/state/change_processor_test.go b/internal/mode/static/state/change_processor_test.go index 6c65ff3719..55073b00f2 100644 --- a/internal/mode/static/state/change_processor_test.go +++ b/internal/mode/static/state/change_processor_test.go @@ -9,6 +9,7 @@ import ( apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -23,6 +24,7 @@ import ( "github.com/nginxinc/nginx-gateway-fabric/internal/framework/controller/index" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/gatewayclass" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state" staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/graph" @@ -187,6 +189,7 @@ func createAlwaysValidValidators() validation.Validators { return validation.Validators{ HTTPFieldsValidator: &validationfakes.FakeHTTPFieldsValidator{}, GenericValidator: &validationfakes.FakeGenericValidator{}, + PolicyValidator: &validationfakes.FakePolicyValidator{}, } } @@ -287,7 +290,7 @@ var _ = Describe("ChangeProcessor", func() { GatewayClassName: gcName, Logger: zap.New(), Validators: createAlwaysValidValidators(), - Scheme: createScheme(), + MustExtractGVK: kinds.NewMustExtractGKV(createScheme()), }) }) @@ -338,7 +341,7 @@ var _ = Describe("ChangeProcessor", func() { From: []v1beta1.ReferenceGrantFrom{ { Group: v1.GroupName, - Kind: "Gateway", + Kind: kinds.Gateway, Namespace: "test", }, }, @@ -359,7 +362,7 @@ var _ = Describe("ChangeProcessor", func() { From: []v1beta1.ReferenceGrantFrom{ { Group: v1.GroupName, - Kind: "HTTPRoute", + Kind: kinds.HTTPRoute, Namespace: "test", }, }, @@ -520,7 +523,7 @@ var _ = Describe("ChangeProcessor", func() { Valid: true, Attachable: true, Routes: map[graph.RouteKey]*graph.L7Route{routeKey1: expRouteHR1}, - SupportedKinds: []v1.RouteGroupKind{{Kind: "HTTPRoute"}}, + SupportedKinds: []v1.RouteGroupKind{{Kind: kinds.HTTPRoute}}, }, { Name: "listener-443-1", @@ -529,7 +532,7 @@ var _ = Describe("ChangeProcessor", func() { Attachable: true, Routes: map[graph.RouteKey]*graph.L7Route{routeKey1: expRouteHR1}, ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(diffNsTLSSecret)), - SupportedKinds: []v1.RouteGroupKind{{Kind: "HTTPRoute"}}, + SupportedKinds: []v1.RouteGroupKind{{Kind: kinds.HTTPRoute}}, }, }, Valid: true, @@ -1451,7 +1454,7 @@ var _ = Describe("ChangeProcessor", func() { GatewayClassName: gcName, Logger: zap.New(), Validators: createAlwaysValidValidators(), - Scheme: createScheme(), + MustExtractGVK: kinds.NewMustExtractGKV(createScheme()), }) processor.CaptureUpsertChange(gc) processor.CaptureUpsertChange(gw) @@ -1500,16 +1503,19 @@ var _ = Describe("ChangeProcessor", func() { Expect(changed).To(Equal(state.ClusterStateChange)) }) }) - When("a namespace that is linked to a listener has its labels changed to no longer match a listener", func() { - It("triggers an update", func() { - nsDifferentLabels.Labels = map[string]string{ - "oranges": "bananas", - } - processor.CaptureUpsertChange(nsDifferentLabels) - changed, _ := processor.Process() - Expect(changed).To(Equal(state.ClusterStateChange)) - }) - }) + When( + "a namespace that is linked to a listener has its labels changed to no longer match a listener", + func() { + It("triggers an update", func() { + nsDifferentLabels.Labels = map[string]string{ + "oranges": "bananas", + } + processor.CaptureUpsertChange(nsDifferentLabels) + changed, _ := processor.Process() + Expect(changed).To(Equal(state.ClusterStateChange)) + }) + }, + ) When("a gateway changes its listener's labels", func() { It("triggers an update when a namespace that matches the new labels is created", func() { gwChangedLabel := gw.DeepCopy() @@ -1559,7 +1565,7 @@ var _ = Describe("ChangeProcessor", func() { paramGC := gc.DeepCopy() paramGC.Spec.ParametersRef = &v1beta1.ParametersReference{ Group: ngfAPI.GroupName, - Kind: v1beta1.Kind("NginxProxy"), + Kind: v1beta1.Kind(kinds.NginxProxy), Name: "np", } @@ -1608,8 +1614,98 @@ var _ = Describe("ChangeProcessor", func() { Expect(graph.NginxProxy).To(BeNil()) }) }) - }) + Describe("NGF Policy resource changes", Ordered, func() { + var ( + gw *v1.Gateway + csp, cspUpdated *ngfAPI.ClientSettingsPolicy + cspKey graph.PolicyKey + ) + + BeforeAll(func() { + processor.CaptureUpsertChange(gc) + changed, newGraph := processor.Process() + Expect(changed).To(Equal(state.ClusterStateChange)) + Expect(newGraph.GatewayClass.Source).To(Equal(gc)) + Expect(newGraph.NGFPolicies).To(BeEmpty()) + + gw = createGateway("gw") + + csp = &ngfAPI.ClientSettingsPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "csp", + Namespace: "test", + }, + Spec: ngfAPI.ClientSettingsPolicySpec{ + TargetRef: v1alpha2.LocalPolicyTargetReference{ + Group: v1.GroupName, + Kind: kinds.Gateway, + Name: "gw", + }, + Body: &ngfAPI.ClientBody{ + MaxSize: helpers.GetPointer[ngfAPI.Size]("10m"), + }, + }, + } + + cspUpdated = csp.DeepCopy() + cspUpdated.Spec.Body.MaxSize = helpers.GetPointer[ngfAPI.Size]("20m") + + cspKey = graph.PolicyKey{ + NsName: types.NamespacedName{Name: "csp", Namespace: "test"}, + GVK: schema.GroupVersionKind{ + Group: ngfAPI.GroupName, + Kind: kinds.ClientSettingsPolicy, + Version: "v1alpha1", + }, + } + }) + + /* + NOTE: When adding a new NGF policy to the change processor, + update the following tests to make sure that the change processor can track changes for multiple NGF + policies. + */ + + When("a policy is created that references a resource that is not in the last graph", func() { + It("reports no changes", func() { + processor.CaptureUpsertChange(csp) + + changed, _ := processor.Process() + Expect(changed).To(Equal(state.NoChange)) + }) + }) + When("the resource the policy references is created", func() { + It("populates the graph with the policy", func() { + processor.CaptureUpsertChange(gw) + + changed, graph := processor.Process() + Expect(changed).To(Equal(state.ClusterStateChange)) + Expect(graph.NGFPolicies).To(HaveKey(cspKey)) + Expect(graph.NGFPolicies[cspKey].Source).To(Equal(csp)) + }) + }) + When("the policy is updated", func() { + It("captures changes for a policy", func() { + processor.CaptureUpsertChange(cspUpdated) + + changed, graph := processor.Process() + Expect(changed).To(Equal(state.ClusterStateChange)) + Expect(graph.NGFPolicies).To(HaveKey(cspKey)) + Expect(graph.NGFPolicies[cspKey].Source).To(Equal(cspUpdated)) + }) + }) + When("the policy is deleted", func() { + It("removes the policy from the graph", func() { + processor.CaptureDeleteChange(&ngfAPI.ClientSettingsPolicy{}, client.ObjectKeyFromObject(csp)) + + changed, graph := processor.Process() + Expect(changed).To(Equal(state.ClusterStateChange)) + Expect(graph.NGFPolicies).To(BeEmpty()) + }) + }) + }) + }) Describe("Ensuring non-changing changes don't override previously changing changes", func() { // Note: in these tests, we deliberately don't fully inspect the returned configuration and statuses // -- this is done in 'Normal cases of processing changes' @@ -1636,7 +1732,7 @@ var _ = Describe("ChangeProcessor", func() { GatewayCtlrName: "test.controller", GatewayClassName: "test-class", Validators: createAlwaysValidValidators(), - Scheme: createScheme(), + MustExtractGVK: kinds.NewMustExtractGKV(createScheme()), }) secretNsName = types.NamespacedName{Namespace: "test", Name: "tls-secret"} @@ -2163,7 +2259,7 @@ var _ = Describe("ChangeProcessor", func() { GatewayCtlrName: "test.controller", GatewayClassName: "my-class", Validators: createAlwaysValidValidators(), - Scheme: createScheme(), + MustExtractGVK: kinds.NewMustExtractGKV(createScheme()), }) }) diff --git a/internal/mode/static/state/conditions/conditions.go b/internal/mode/static/state/conditions/conditions.go index 84d1cbafe4..aa7c73ea3e 100644 --- a/internal/mode/static/state/conditions/conditions.go +++ b/internal/mode/static/state/conditions/conditions.go @@ -11,14 +11,6 @@ import ( "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" ) -// BackendTLSPolicyConditionType is a type of condition associated with a BackendTLSPolicy. -// This type should be used with the BackendTLSPolicyStatus.Conditions field. -type BackendTLSPolicyConditionType string - -// BackendTLSPolicyConditionReason defines the set of reasons that explain why a particular BackendTLSPolicy condition -// type has been raised. -type BackendTLSPolicyConditionReason string - const ( // ListenerReasonUnsupportedValue is used with the "Accepted" condition when a value of a field in a Listener // is invalid or not supported. @@ -605,19 +597,19 @@ func NewNginxGatewayInvalid(msg string) conditions.Condition { } } -// NewBackendTLSPolicyAccepted returns a Condition that indicates that the BackendTLSPolicy config is valid and accepted -// by the Gateway. -func NewBackendTLSPolicyAccepted() conditions.Condition { +// NewPolicyAccepted returns a Condition that indicates that the Policy is accepted. +func NewPolicyAccepted() conditions.Condition { return conditions.Condition{ Type: string(v1alpha2.PolicyConditionAccepted), Status: metav1.ConditionTrue, Reason: string(v1alpha2.PolicyReasonAccepted), - Message: "BackendTLSPolicy is accepted by the Gateway", + Message: "Policy is accepted", } } -// NewBackendTLSPolicyInvalid returns a Condition that indicates that the BackendTLSPolicy config is invalid. -func NewBackendTLSPolicyInvalid(msg string) conditions.Condition { +// NewPolicyInvalid returns a Condition that indicates that the Policy is not accepted because it is semantically or +// syntactically invalid. +func NewPolicyInvalid(msg string) conditions.Condition { return conditions.Condition{ Type: string(v1alpha2.PolicyConditionAccepted), Status: metav1.ConditionFalse, @@ -625,3 +617,25 @@ func NewBackendTLSPolicyInvalid(msg string) conditions.Condition { Message: msg, } } + +// NewPolicyConflicted returns a Condition that indicates that the Policy is not accepted because it conflicts with +// another Policy and a merge is not possible. +func NewPolicyConflicted(msg string) conditions.Condition { + return conditions.Condition{ + Type: string(v1alpha2.PolicyConditionAccepted), + Status: metav1.ConditionFalse, + Reason: string(v1alpha2.PolicyReasonConflicted), + Message: msg, + } +} + +// NewPolicyTargetNotFound returns a Condition that indicates that the Policy is not accepted because the target +// resource does not exist or can not be attached to. +func NewPolicyTargetNotFound(msg string) conditions.Condition { + return conditions.Condition{ + Type: string(v1alpha2.PolicyConditionAccepted), + Status: metav1.ConditionFalse, + Reason: string(v1alpha2.PolicyReasonTargetNotFound), + Message: msg, + } +} diff --git a/internal/mode/static/state/dataplane/configuration.go b/internal/mode/static/state/dataplane/configuration.go index 648d0a5cd9..a8ed937cac 100644 --- a/internal/mode/static/state/dataplane/configuration.go +++ b/internal/mode/static/state/dataplane/configuration.go @@ -13,6 +13,7 @@ import ( v1 "sigs.k8s.io/gateway-api/apis/v1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/graph" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/resolver" ) @@ -27,6 +28,7 @@ func BuildConfiguration( ctx context.Context, g *graph.Graph, resolver resolver.ServiceResolver, + generator policies.ConfigGenerator, configVersion int, ) Configuration { if g.GatewayClass == nil || !g.GatewayClass.Valid { @@ -38,7 +40,7 @@ func BuildConfiguration( } upstreams := buildUpstreams(ctx, g.Gateway.Listeners, resolver) - httpServers, sslServers := buildServers(g.Gateway.Listeners) + httpServers, sslServers := buildServers(g.Gateway, generator) backendGroups := buildBackendGroups(append(httpServers, sslServers...)) keyPairs := buildSSLKeyPairs(g.ReferencedSecrets, g.Gateway.Listeners) certBundles := buildCertBundles(g.ReferencedCaCertConfigMaps, backendGroups) @@ -200,17 +202,17 @@ func convertBackendTLS(btp *graph.BackendTLSPolicy) *VerifyTLS { return verify } -func buildServers(listeners []*graph.Listener) (http, ssl []VirtualServer) { +func buildServers(gw *graph.Gateway, generator policies.ConfigGenerator) (http, ssl []VirtualServer) { rulesForProtocol := map[v1.ProtocolType]portPathRules{ v1.HTTPProtocolType: make(portPathRules), v1.HTTPSProtocolType: make(portPathRules), } - for _, l := range listeners { + for _, l := range gw.Listeners { if l.Valid { rules := rulesForProtocol[l.Source.Protocol][l.Source.Port] if rules == nil { - rules = newHostPathRules() + rules = newHostPathRules(generator) rulesForProtocol[l.Source.Protocol][l.Source.Port] = rules } @@ -221,7 +223,19 @@ func buildServers(listeners []*graph.Listener) (http, ssl []VirtualServer) { httpRules := rulesForProtocol[v1.HTTPProtocolType] sslRules := rulesForProtocol[v1.HTTPSProtocolType] - return httpRules.buildServers(), sslRules.buildServers() + httpServers, sslServers := httpRules.buildServers(), sslRules.buildServers() + + additions := buildAdditions(gw.Policies, generator) + + for i := range httpServers { + httpServers[i].Additions = additions + } + + for i := range sslServers { + sslServers[i].Additions = additions + } + + return httpServers, sslServers } // portPathRules keeps track of hostPathRules per port @@ -248,18 +262,20 @@ type pathAndType struct { } type hostPathRules struct { + generator policies.ConfigGenerator rulesPerHost map[string]map[pathAndType]PathRule listenersForHost map[string]*graph.Listener httpsListeners []*graph.Listener - listenersExist bool port int32 + listenersExist bool } -func newHostPathRules() *hostPathRules { +func newHostPathRules(generator policies.ConfigGenerator) *hostPathRules { return &hostPathRules{ rulesPerHost: make(map[string]map[pathAndType]PathRule), listenersForHost: make(map[string]*graph.Listener), httpsListeners: make([]*graph.Listener, 0), + generator: generator, } } @@ -327,6 +343,8 @@ func (hpr *hostPathRules) upsertRoute(route *graph.L7Route, listener *graph.List } } + additions := buildAdditions(route.Policies, hpr.generator) + for _, h := range hostnames { for _, m := range rule.Matches { path := getPath(m.Path) @@ -351,6 +369,7 @@ func (hpr *hostPathRules) upsertRoute(route *graph.L7Route, listener *graph.List BackendGroup: newBackendGroup(rule.BackendRefs, routeNsName, i), Filters: filters, Match: convertMatch(m), + Additions: additions, }) hpr.rulesPerHost[h][key] = hostRule @@ -637,3 +656,29 @@ func buildBaseHTTPConfig(g *graph.Graph) BaseHTTPConfig { return baseConfig } + +func buildAdditions(policies []*graph.Policy, generator policies.ConfigGenerator) []Addition { + if len(policies) == 0 { + return nil + } + + additions := make([]Addition, 0, len(policies)) + + for _, policy := range policies { + if !policy.Valid { + continue + } + + additions = append(additions, Addition{ + Bytes: generator.Generate(policy.Source), + Identifier: fmt.Sprintf( + "%s_%s_%s", + policy.Source.GetObjectKind().GroupVersionKind().Kind, + policy.Source.GetNamespace(), + policy.Source.GetName(), + ), + }) + } + + return additions +} diff --git a/internal/mode/static/state/dataplane/configuration_test.go b/internal/mode/static/state/dataplane/configuration_test.go index 2fa7b75c35..8ea630a20b 100644 --- a/internal/mode/static/state/dataplane/configuration_test.go +++ b/internal/mode/static/state/dataplane/configuration_test.go @@ -9,6 +9,7 @@ import ( . "github.com/onsi/gomega" apiv1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" v1 "sigs.k8s.io/gateway-api/apis/v1" @@ -17,17 +18,64 @@ import ( ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies/policiesfakes" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/graph" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/resolver" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/resolver/resolverfakes" ) +func createFakePolicy(name string, kind string) policies.Policy { + fakeKind := &policiesfakes.FakeObjectKind{ + GroupVersionKindStub: func() schema.GroupVersionKind { + return schema.GroupVersionKind{Kind: kind} + }, + } + + return &policiesfakes.FakePolicy{ + GetNameStub: func() string { + return name + }, + GetNamespaceStub: func() string { + return "default" + }, + GetObjectKindStub: func() schema.ObjectKind { + return fakeKind + }, + } +} + func TestBuildConfiguration(t *testing.T) { const ( invalidMatchesPath = "/not-valid-matches" invalidFiltersPath = "/not-valid-filters" ) + gwPolicy1 := &graph.Policy{ + Source: createFakePolicy("attach-gw", "ApplePolicy"), + Valid: true, + } + + gwPolicy2 := &graph.Policy{ + Source: createFakePolicy("attach-gw", "OrangePolicy"), + Valid: true, + } + + hrPolicy1 := &graph.Policy{ + Source: createFakePolicy("attach-hr", "LemonPolicy"), + Valid: true, + } + + hrPolicy2 := &graph.Policy{ + Source: createFakePolicy("attach-hr", "LimePolicy"), + Valid: true, + } + + invalidPolicy := &graph.Policy{ + Source: createFakePolicy("invalid", "LimePolicy"), + Valid: false, + } + createRoute := func(name string) *v1.HTTPRoute { return &v1.HTTPRoute{ ObjectMeta: metav1.ObjectMeta{ @@ -408,6 +456,30 @@ func TestBuildConfiguration(t *testing.T) { Hostname: "foo.example.com", } + hrWithPolicy, expHRWithPolicyGroups, l7RouteWithPolicy := createTestResources( + "hr-with-policy", + "policy.com", + "listener-80-1", + pathAndType{ + path: "/", + pathType: prefix, + }, + ) + + l7RouteWithPolicy.Policies = []*graph.Policy{hrPolicy1, invalidPolicy} + + httpsHRWithPolicy, expHTTPSHRWithPolicyGroups, l7HTTPSRouteWithPolicy := createTestResources( + "https-hr-with-policy", + "policy.com", + "listener-443-1", + pathAndType{ + path: "/", + pathType: prefix, + }, + ) + + l7HTTPSRouteWithPolicy.Policies = []*graph.Policy{hrPolicy2, invalidPolicy} + secret1NsName := types.NamespacedName{Namespace: "test", Name: "secret-1"} secret1 := &graph.Secret{ Source: &apiv1.Secret{ @@ -1999,13 +2071,204 @@ func TestBuildConfiguration(t *testing.T) { }, msg: "NginxProxy with tracing config and http2 disabled", }, + { + graph: &graph.Graph{ + GatewayClass: &graph.GatewayClass{ + Source: &v1.GatewayClass{}, + Valid: true, + }, + Gateway: &graph.Gateway{ + Source: &v1.Gateway{}, + Listeners: []*graph.Listener{ + { + Name: "listener-80-1", + Source: listener80, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hrWithPolicy): l7RouteWithPolicy, + }, + }, + { + Name: "listener-443", + Source: listener443, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(httpsHRWithPolicy): l7HTTPSRouteWithPolicy, + }, + ResolvedSecret: &secret1NsName, + }, + }, + Policies: []*graph.Policy{gwPolicy1, gwPolicy2}, + }, + Routes: map[graph.RouteKey]*graph.L7Route{ + graph.CreateRouteKey(hrWithPolicy): l7RouteWithPolicy, + graph.CreateRouteKey(httpsHRWithPolicy): l7HTTPSRouteWithPolicy, + }, + ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ + secret1NsName: secret1, + }, + }, + expConf: Configuration{ + SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{ + "ssl_keypair_test_secret-1": { + Cert: []byte("cert-1"), + Key: []byte("privateKey-1"), + }, + }, + CertBundles: map[CertBundleID]CertBundle{}, + HTTPServers: []VirtualServer{ + { + IsDefault: true, + Port: 80, + Additions: []Addition{ + { + Bytes: []byte("apple"), + Identifier: "ApplePolicy_default_attach-gw", + }, + { + Bytes: []byte("orange"), + Identifier: "OrangePolicy_default_attach-gw", + }, + }, + }, + { + Hostname: "policy.com", + PathRules: []PathRule{ + { + Path: "/", + PathType: PathTypePrefix, + MatchRules: []MatchRule{ + { + Source: &hrWithPolicy.ObjectMeta, + BackendGroup: expHRWithPolicyGroups[0], + Additions: []Addition{ + { + Bytes: []byte("lemon"), + Identifier: "LemonPolicy_default_attach-hr", + }, + }, + }, + }, + }, + }, + Port: 80, + Additions: []Addition{ + { + Bytes: []byte("apple"), + Identifier: "ApplePolicy_default_attach-gw", + }, + { + Bytes: []byte("orange"), + Identifier: "OrangePolicy_default_attach-gw", + }, + }, + }, + }, + SSLServers: []VirtualServer{ + { + IsDefault: true, + Port: 443, + Additions: []Addition{ + { + Bytes: []byte("apple"), + Identifier: "ApplePolicy_default_attach-gw", + }, + { + Bytes: []byte("orange"), + Identifier: "OrangePolicy_default_attach-gw", + }, + }, + }, + { + Hostname: "policy.com", + PathRules: []PathRule{ + { + Path: "/", + PathType: PathTypePrefix, + MatchRules: []MatchRule{ + { + BackendGroup: expHTTPSHRWithPolicyGroups[0], + Source: &httpsHRWithPolicy.ObjectMeta, + Additions: []Addition{ + { + Bytes: []byte("lime"), + Identifier: "LimePolicy_default_attach-hr", + }, + }, + }, + }, + }, + }, + SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, + Port: 443, + Additions: []Addition{ + { + Bytes: []byte("apple"), + Identifier: "ApplePolicy_default_attach-gw", + }, + { + Bytes: []byte("orange"), + Identifier: "OrangePolicy_default_attach-gw", + }, + }, + }, + { + Hostname: wildcardHostname, + SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, + Port: 443, + Additions: []Addition{ + { + Bytes: []byte("apple"), + Identifier: "ApplePolicy_default_attach-gw", + }, + { + Bytes: []byte("orange"), + Identifier: "OrangePolicy_default_attach-gw", + }, + }, + }, + }, + Upstreams: []Upstream{fooUpstream}, + BackendGroups: []BackendGroup{ + expHRWithPolicyGroups[0], + expHTTPSHRWithPolicyGroups[0], + }, + BaseHTTPConfig: BaseHTTPConfig{ + HTTP2: true, + }, + }, + msg: "Simple Gateway and HTTPRoute with policies attached", + }, } for _, test := range tests { t.Run(test.msg, func(t *testing.T) { g := NewWithT(t) - result := BuildConfiguration(context.TODO(), test.graph, fakeResolver, 1) + fakeGenerator := &policiesfakes.FakeConfigGenerator{ + GenerateStub: func(p policies.Policy) []byte { + switch kind := p.GetObjectKind().GroupVersionKind().Kind; kind { + case "ApplePolicy": + return []byte("apple") + case "OrangePolicy": + return []byte("orange") + case "LemonPolicy": + return []byte("lemon") + case "LimePolicy": + return []byte("lime") + default: + panic(fmt.Sprintf("unknown policy kind: %s", kind)) + } + }, + } + + result := BuildConfiguration( + context.TODO(), + test.graph, + fakeResolver, + fakeGenerator, + 1, + ) g.Expect(result.BackendGroups).To(ConsistOf(test.expConf.BackendGroups)) g.Expect(result.Upstreams).To(ConsistOf(test.expConf.Upstreams)) @@ -2768,3 +3031,91 @@ func TestBuildTelemetry(t *testing.T) { }) } } + +func TestBuildAdditions(t *testing.T) { + getPolicy := func(kind, name string) policies.Policy { + return &policiesfakes.FakePolicy{ + GetNameStub: func() string { + return name + }, + GetNamespaceStub: func() string { + return "test" + }, + GetObjectKindStub: func() schema.ObjectKind { + objKind := &policiesfakes.FakeObjectKind{ + GroupVersionKindStub: func() schema.GroupVersionKind { + return schema.GroupVersionKind{Kind: kind} + }, + } + + return objKind + }, + } + } + + tests := []struct { + name string + policies []*graph.Policy + expAdditions []Addition + }{ + { + name: "nil policies", + policies: nil, + expAdditions: nil, + }, + { + name: "mix of valid and invalid policies", + policies: []*graph.Policy{ + { + Source: getPolicy("Kind1", "valid1"), + Valid: true, + }, + { + Source: getPolicy("Kind2", "valid2"), + Valid: true, + }, + { + Source: getPolicy("Kind1", "invalid1"), + Valid: false, + }, + { + Source: getPolicy("Kind2", "invalid2"), + Valid: false, + }, + { + Source: getPolicy("Kind3", "valid3"), + Valid: true, + }, + }, + expAdditions: []Addition{ + { + Identifier: "Kind1_test_valid1", + Bytes: []byte("valid1"), + }, + { + Identifier: "Kind2_test_valid2", + Bytes: []byte("valid2"), + }, + { + Identifier: "Kind3_test_valid3", + Bytes: []byte("valid3"), + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + generator := &policiesfakes.FakeConfigGenerator{ + GenerateStub: func(policy policies.Policy) []byte { + return []byte(policy.GetName()) + }, + } + + additions := buildAdditions(test.policies, generator) + g.Expect(additions).To(BeEquivalentTo(test.expAdditions)) + }) + } +} diff --git a/internal/mode/static/state/dataplane/types.go b/internal/mode/static/state/dataplane/types.go index 23ab0a46ea..7e7feb8f5c 100644 --- a/internal/mode/static/state/dataplane/types.go +++ b/internal/mode/static/state/dataplane/types.go @@ -68,10 +68,20 @@ type VirtualServer struct { Hostname string // PathRules is a collection of routing rules. PathRules []PathRule - // IsDefault indicates whether the server is the default server. - IsDefault bool + // Additions is a list of config additions for the server. + Additions []Addition // Port is the port of the server. Port int32 + // IsDefault indicates whether the server is the default server. + IsDefault bool +} + +// Addition holds additional configuration. +type Addition struct { + // Identifier is a unique identifier for the addition. + Identifier string + // Bytes contains the additional configuration. + Bytes []byte } // Upstream is a pool of endpoints to be load balanced. @@ -204,6 +214,8 @@ type MatchRule struct { Source *metav1.ObjectMeta // Match holds the match for the rule. Match Match + // Additions holds the config additions for the rule. + Additions []Addition // BackendGroup is the group of Backends that the rule routes to. BackendGroup BackendGroup } diff --git a/internal/mode/static/state/graph/backend_refs.go b/internal/mode/static/state/graph/backend_refs.go index 172f5d5abb..5e36338972 100644 --- a/internal/mode/static/state/graph/backend_refs.go +++ b/internal/mode/static/state/graph/backend_refs.go @@ -247,7 +247,7 @@ func findBackendTLSPolicyForService( for _, targetRef := range btp.Source.Spec.TargetRefs { if string(targetRef.Name) == refName && btpNs == refNs { if beTLSPolicy != nil { - if sort.LessObjectMeta(&btp.Source.ObjectMeta, &beTLSPolicy.Source.ObjectMeta) { + if sort.LessClientObject(btp.Source, beTLSPolicy.Source) { beTLSPolicy = btp } } else { @@ -263,7 +263,7 @@ func findBackendTLSPolicyForService( if !beTLSPolicy.Valid { err = fmt.Errorf("the backend TLS policy is invalid: %s", beTLSPolicy.Conditions[0].Message) } else { - beTLSPolicy.Conditions = append(beTLSPolicy.Conditions, staticConds.NewBackendTLSPolicyAccepted()) + beTLSPolicy.Conditions = append(beTLSPolicy.Conditions, staticConds.NewPolicyAccepted()) } } diff --git a/internal/mode/static/state/graph/backend_refs_test.go b/internal/mode/static/state/graph/backend_refs_test.go index 2bee129940..140d718864 100644 --- a/internal/mode/static/state/graph/backend_refs_test.go +++ b/internal/mode/static/state/graph/backend_refs_test.go @@ -16,6 +16,7 @@ import ( "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" ) @@ -107,7 +108,7 @@ func TestValidateBackendRef(t *testing.T) { From: []v1beta1.ReferenceGrantFrom{ { Group: gatewayv1.GroupName, - Kind: "HTTPRoute", + Kind: kinds.HTTPRoute, Namespace: "test", }, }, @@ -523,7 +524,7 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { Type: "Accepted", Status: "True", Reason: "Accepted", - Message: "BackendTLSPolicy is accepted by the Gateway", + Message: "Policy is accepted", }, }, Valid: true, @@ -538,7 +539,7 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { Type: "Accepted", Status: "True", Reason: "Accepted", - Message: "BackendTLSPolicy is accepted by the Gateway", + Message: "Policy is accepted", }, ) @@ -764,7 +765,7 @@ func TestCreateBackend(t *testing.T) { }, Valid: false, Conditions: []conditions.Condition{ - staticConds.NewBackendTLSPolicyInvalid("unsupported value"), + staticConds.NewPolicyInvalid("unsupported value"), }, } diff --git a/internal/mode/static/state/graph/backend_tls_policy.go b/internal/mode/static/state/graph/backend_tls_policy.go index 7749970c38..1545abf686 100644 --- a/internal/mode/static/state/graph/backend_tls_policy.go +++ b/internal/mode/static/state/graph/backend_tls_policy.go @@ -1,16 +1,13 @@ package graph import ( - "errors" "fmt" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/validation/field" - v1 "sigs.k8s.io/gateway-api/apis/v1" "sigs.k8s.io/gateway-api/apis/v1alpha3" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" - "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" ) @@ -44,12 +41,8 @@ func processBackendTLSPolicies( processedBackendTLSPolicies := make(map[types.NamespacedName]*BackendTLSPolicy, len(backendTLSPolicies)) for nsname, backendTLSPolicy := range backendTLSPolicies { var caCertRef types.NamespacedName - valid, ignored, conds := validateBackendTLSPolicy( - backendTLSPolicy, - configMapResolver, - ctlrName, - gateway, - ) + + valid, ignored, conds := validateBackendTLSPolicy(backendTLSPolicy, configMapResolver, ctlrName) if valid && !ignored && backendTLSPolicy.Spec.Validation.CACertificateRefs != nil { caCertRef = types.NamespacedName{ @@ -76,17 +69,19 @@ func validateBackendTLSPolicy( backendTLSPolicy *v1alpha3.BackendTLSPolicy, configMapResolver *configMapResolver, ctlrName string, - gateway *Gateway, ) (valid, ignored bool, conds []conditions.Condition) { valid = true ignored = false - if err := validateAncestorMaxCount(backendTLSPolicy, ctlrName, gateway); err != nil { + + // FIXME (kate-osborn): https://github.com/nginxinc/nginx-gateway-fabric/issues/1987 + if ancestorsFull(backendTLSPolicy.Status.Ancestors, ctlrName) { valid = false ignored = true } + if err := validateBackendTLSHostname(backendTLSPolicy); err != nil { valid = false - conds = append(conds, staticConds.NewBackendTLSPolicyInvalid(fmt.Sprintf("invalid hostname: %s", err.Error()))) + conds = append(conds, staticConds.NewPolicyInvalid(fmt.Sprintf("invalid hostname: %s", err.Error()))) } caCertRefs := backendTLSPolicy.Spec.Validation.CACertificateRefs @@ -94,49 +89,26 @@ func validateBackendTLSPolicy( if len(caCertRefs) > 0 && wellKnownCerts != nil { valid = false msg := "CACertificateRefs and WellKnownCACertificates are mutually exclusive" - conds = append(conds, staticConds.NewBackendTLSPolicyInvalid(msg)) + conds = append(conds, staticConds.NewPolicyInvalid(msg)) } else if len(caCertRefs) > 0 { if err := validateBackendTLSCACertRef(backendTLSPolicy, configMapResolver); err != nil { valid = false - conds = append(conds, staticConds.NewBackendTLSPolicyInvalid( + conds = append(conds, staticConds.NewPolicyInvalid( fmt.Sprintf("invalid CACertificateRef: %s", err.Error()))) } } else if wellKnownCerts != nil { if err := validateBackendTLSWellKnownCACerts(backendTLSPolicy); err != nil { valid = false - conds = append(conds, staticConds.NewBackendTLSPolicyInvalid( + conds = append(conds, staticConds.NewPolicyInvalid( fmt.Sprintf("invalid WellKnownCACertificates: %s", err.Error()))) } } else { valid = false - conds = append(conds, staticConds.NewBackendTLSPolicyInvalid("CACertRefs and WellKnownCACerts are both nil")) + conds = append(conds, staticConds.NewPolicyInvalid("CACertRefs and WellKnownCACerts are both nil")) } return valid, ignored, conds } -func validateAncestorMaxCount(backendTLSPolicy *v1alpha3.BackendTLSPolicy, ctlrName string, gateway *Gateway) error { - var err error - if len(backendTLSPolicy.Status.Ancestors) >= 16 { - // check if we already are an ancestor on this policy. If we are, we are safe to continue. - ancestorRef := v1.ParentReference{ - Namespace: helpers.GetPointer((v1.Namespace)(gateway.Source.Namespace)), - Name: v1.ObjectName(gateway.Source.Name), - } - var alreadyAncestor bool - for _, ancestor := range backendTLSPolicy.Status.Ancestors { - if string(ancestor.ControllerName) == ctlrName && ancestor.AncestorRef.Name == ancestorRef.Name && - ancestor.AncestorRef.Namespace != nil && *ancestor.AncestorRef.Namespace == *ancestorRef.Namespace { - alreadyAncestor = true - break - } - } - if !alreadyAncestor { - err = errors.New("too many ancestors, cannot attach a new Gateway") - } - } - return err -} - func validateBackendTLSHostname(btp *v1alpha3.BackendTLSPolicy) error { h := string(btp.Spec.Validation.Hostname) @@ -159,12 +131,16 @@ func validateBackendTLSCACertRef(btp *v1alpha3.BackendTLSPolicy, configMapResolv valErr := field.NotSupported(path, btp.Spec.Validation.CACertificateRefs[0].Kind, []string{"ConfigMap"}) return valErr } - if btp.Spec.Validation.CACertificateRefs[0].Group != "" && btp.Spec.Validation.CACertificateRefs[0].Group != "core" { + if btp.Spec.Validation.CACertificateRefs[0].Group != "" && + btp.Spec.Validation.CACertificateRefs[0].Group != "core" { path := field.NewPath("tls.cacertrefs[0].group") valErr := field.NotSupported(path, btp.Spec.Validation.CACertificateRefs[0].Group, []string{"", "core"}) return valErr } - nsName := types.NamespacedName{Namespace: btp.Namespace, Name: string(btp.Spec.Validation.CACertificateRefs[0].Name)} + nsName := types.NamespacedName{ + Namespace: btp.Namespace, + Name: string(btp.Spec.Validation.CACertificateRefs[0].Name), + } if err := configMapResolver.resolve(nsName); err != nil { path := field.NewPath("tls.cacertrefs[0]") return field.Invalid(path, btp.Spec.Validation.CACertificateRefs[0], err.Error()) diff --git a/internal/mode/static/state/graph/backend_tls_policy_test.go b/internal/mode/static/state/graph/backend_tls_policy_test.go index bcf03c98ff..b4ded8f21d 100644 --- a/internal/mode/static/state/graph/backend_tls_policy_test.go +++ b/internal/mode/static/state/graph/backend_tls_policy_test.go @@ -12,6 +12,7 @@ import ( "sigs.k8s.io/gateway-api/apis/v1alpha3" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" ) func TestProcessBackendTLSPoliciesEmpty(t *testing.T) { @@ -129,6 +130,8 @@ func TestValidateBackendTLSPolicy(t *testing.T) { AncestorRef: gatewayv1.ParentReference{ Name: gatewayv1.ObjectName(parentName), Namespace: helpers.GetPointer(gatewayv1.Namespace("test")), + Group: helpers.GetPointer[gatewayv1.Group](gatewayv1.GroupName), + Kind: helpers.GetPointer[gatewayv1.Kind](kinds.Gateway), }, } } @@ -152,7 +155,9 @@ func TestValidateBackendTLSPolicy(t *testing.T) { getAncestorRef("not-us", "not-us"), } - ancestorsWithUs := append(ancestors, getAncestorRef("test", "gateway")) + ancestorsWithUs := make([]v1alpha2.PolicyAncestorStatus, len(ancestors)) + copy(ancestorsWithUs, ancestors) + ancestorsWithUs[0] = getAncestorRef("test", "gateway") tests := []struct { tlsPolicy *v1alpha3.BackendTLSPolicy @@ -392,16 +397,7 @@ func TestValidateBackendTLSPolicy(t *testing.T) { t.Run(test.name, func(t *testing.T) { g := NewWithT(t) - gateway := &Gateway{ - Source: &gatewayv1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "gateway", Namespace: "test"}}, - } - - valid, ignored, conds := validateBackendTLSPolicy( - test.tlsPolicy, - configMapResolver, - "test", - gateway, - ) + valid, ignored, conds := validateBackendTLSPolicy(test.tlsPolicy, configMapResolver, "test") g.Expect(valid).To(Equal(test.isValid)) g.Expect(ignored).To(Equal(test.ignored)) diff --git a/internal/mode/static/state/graph/gateway.go b/internal/mode/static/state/graph/gateway.go index 79f0ae9df4..6a826f80fc 100644 --- a/internal/mode/static/state/graph/gateway.go +++ b/internal/mode/static/state/graph/gateway.go @@ -21,6 +21,8 @@ type Gateway struct { Listeners []*Listener // Conditions holds the conditions for the Gateway. Conditions []conditions.Condition + // Policies holds the policies attached to the Gateway. + Policies []*Policy // Valid indicates whether the Gateway Spec is valid. Valid bool } @@ -75,7 +77,7 @@ func processGateways( } sort.Slice(referencedGws, func(i, j int) bool { - return ngfsort.LessObjectMeta(&referencedGws[i].ObjectMeta, &referencedGws[j].ObjectMeta) + return ngfsort.LessClientObject(referencedGws[i], referencedGws[j]) }) ignoredGws := make(map[types.NamespacedName]*v1.Gateway) diff --git a/internal/mode/static/state/graph/gateway_listener.go b/internal/mode/static/state/graph/gateway_listener.go index 432888ba88..7c61d45231 100644 --- a/internal/mode/static/state/graph/gateway_listener.go +++ b/internal/mode/static/state/graph/gateway_listener.go @@ -11,6 +11,7 @@ import ( v1 "sigs.k8s.io/gateway-api/apis/v1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" ) @@ -230,7 +231,7 @@ func getAndValidateListenerSupportedKinds(listener v1.Listener) ( if listener.AllowedRoutes == nil || listener.AllowedRoutes.Kinds == nil { return nil, []v1.RouteGroupKind{ { - Kind: "HTTPRoute", + Kind: kinds.HTTPRoute, }, } } @@ -239,7 +240,7 @@ func getAndValidateListenerSupportedKinds(listener v1.Listener) ( supportedKinds := make([]v1.RouteGroupKind, 0, len(listener.AllowedRoutes.Kinds)) validHTTPProtocolRouteKind := func(kind v1.RouteGroupKind) bool { - if kind.Kind != v1.Kind("HTTPRoute") && kind.Kind != v1.Kind("GRPCRoute") { + if kind.Kind != v1.Kind(kinds.HTTPRoute) && kind.Kind != v1.Kind(kinds.GRPCRoute) { return false } if kind.Group == nil || *kind.Group != v1.GroupName { diff --git a/internal/mode/static/state/graph/gateway_listener_test.go b/internal/mode/static/state/graph/gateway_listener_test.go index dafe63e5a3..96aba1a66c 100644 --- a/internal/mode/static/state/graph/gateway_listener_test.go +++ b/internal/mode/static/state/graph/gateway_listener_test.go @@ -10,6 +10,7 @@ import ( "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" ) @@ -290,7 +291,7 @@ func TestValidateListenerHostname(t *testing.T) { func TestGetAndValidateListenerSupportedKinds(t *testing.T) { HTTPRouteGroupKind := []v1.RouteGroupKind{ { - Kind: "HTTPRoute", + Kind: kinds.HTTPRoute, Group: helpers.GetPointer[v1.Group](v1.GroupName), }, } @@ -318,7 +319,7 @@ func TestGetAndValidateListenerSupportedKinds(t *testing.T) { protocol: v1.HTTPProtocolType, kind: []v1.RouteGroupKind{ { - Kind: "HTTPRoute", + Kind: kinds.HTTPRoute, Group: helpers.GetPointer[v1.Group]("bad-group"), }, }, @@ -353,7 +354,7 @@ func TestGetAndValidateListenerSupportedKinds(t *testing.T) { name: "valid HTTPS no kind specified", expected: []v1.RouteGroupKind{ { - Kind: "HTTPRoute", + Kind: kinds.HTTPRoute, }, }, }, @@ -361,7 +362,7 @@ func TestGetAndValidateListenerSupportedKinds(t *testing.T) { protocol: v1.HTTPProtocolType, kind: []v1.RouteGroupKind{ { - Kind: "HTTPRoute", + Kind: kinds.HTTPRoute, Group: helpers.GetPointer[v1.Group](v1.GroupName), }, { diff --git a/internal/mode/static/state/graph/gateway_test.go b/internal/mode/static/state/graph/gateway_test.go index df10c2aaba..8bb89471c4 100644 --- a/internal/mode/static/state/graph/gateway_test.go +++ b/internal/mode/static/state/graph/gateway_test.go @@ -14,6 +14,7 @@ import ( v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" ) @@ -156,7 +157,7 @@ func TestBuildGateway(t *testing.T) { Protocol: v1.HTTPProtocolType, AllowedRoutes: &v1.AllowedRoutes{ Kinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute", Group: helpers.GetPointer[v1.Group](v1.GroupName)}, + {Kind: kinds.HTTPRoute, Group: helpers.GetPointer[v1.Group](v1.GroupName)}, }, Namespaces: &v1.RouteNamespaces{ From: helpers.GetPointer(v1.NamespacesFromSelector), @@ -297,7 +298,12 @@ func TestBuildGateway(t *testing.T) { 443, tlsConfigInvalidSecret, ) - invalidHTTPSPortListener := createHTTPSListener("invalid-https-port", "foo.example.com", 65536, gatewayTLSConfigSameNs) + invalidHTTPSPortListener := createHTTPSListener( + "invalid-https-port", + "foo.example.com", + 65536, + gatewayTLSConfigSameNs, + ) const ( invalidHostnameMsg = `hostname: Invalid value: "$example.com": a lowercase RFC 1123 subdomain ` + @@ -362,7 +368,7 @@ func TestBuildGateway(t *testing.T) { Attachable: true, Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, { @@ -372,7 +378,7 @@ func TestBuildGateway(t *testing.T) { Attachable: true, Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, }, @@ -396,7 +402,7 @@ func TestBuildGateway(t *testing.T) { Routes: map[RouteKey]*L7Route{}, ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)), SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, { @@ -407,7 +413,7 @@ func TestBuildGateway(t *testing.T) { Routes: map[RouteKey]*L7Route{}, ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)), SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, }, @@ -429,7 +435,7 @@ func TestBuildGateway(t *testing.T) { AllowedRouteLabelSelector: labels.SelectorFromSet(labels.Set(labelSet)), Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute", Group: helpers.GetPointer[v1.Group](v1.GroupName)}, + {Kind: kinds.HTTPRoute, Group: helpers.GetPointer[v1.Group](v1.GroupName)}, }, }, }, @@ -450,7 +456,7 @@ func TestBuildGateway(t *testing.T) { From: []v1beta1.ReferenceGrantFrom{ { Group: v1.GroupName, - Kind: "Gateway", + Kind: kinds.Gateway, Namespace: "test", }, }, @@ -475,7 +481,7 @@ func TestBuildGateway(t *testing.T) { Routes: map[RouteKey]*L7Route{}, ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretDiffNamespace)), SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, }, @@ -499,7 +505,7 @@ func TestBuildGateway(t *testing.T) { ), Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, }, @@ -523,7 +529,7 @@ func TestBuildGateway(t *testing.T) { ), Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute", Group: helpers.GetPointer[v1.Group](v1.GroupName)}, + {Kind: kinds.HTTPRoute, Group: helpers.GetPointer[v1.Group](v1.GroupName)}, }, }, }, @@ -547,7 +553,7 @@ func TestBuildGateway(t *testing.T) { ), Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, }, @@ -579,7 +585,7 @@ func TestBuildGateway(t *testing.T) { ), Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, { @@ -592,7 +598,7 @@ func TestBuildGateway(t *testing.T) { ), Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, { @@ -605,7 +611,7 @@ func TestBuildGateway(t *testing.T) { ), Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, }, @@ -628,7 +634,7 @@ func TestBuildGateway(t *testing.T) { Conditions: staticConds.NewListenerUnsupportedValue(invalidHostnameMsg), Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, { @@ -638,7 +644,7 @@ func TestBuildGateway(t *testing.T) { Conditions: staticConds.NewListenerUnsupportedValue(invalidHostnameMsg), Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, }, @@ -662,7 +668,7 @@ func TestBuildGateway(t *testing.T) { `tls.certificateRefs[0]: Invalid value: test/does-not-exist: secret does not exist`, ), SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, }, @@ -696,7 +702,7 @@ func TestBuildGateway(t *testing.T) { Attachable: true, Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, { @@ -706,7 +712,7 @@ func TestBuildGateway(t *testing.T) { Attachable: true, Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, { @@ -716,7 +722,7 @@ func TestBuildGateway(t *testing.T) { Attachable: true, Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, { @@ -727,7 +733,7 @@ func TestBuildGateway(t *testing.T) { Routes: map[RouteKey]*L7Route{}, ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)), SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, { @@ -738,7 +744,7 @@ func TestBuildGateway(t *testing.T) { Routes: map[RouteKey]*L7Route{}, ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)), SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, { @@ -748,7 +754,7 @@ func TestBuildGateway(t *testing.T) { Attachable: true, Routes: map[RouteKey]*L7Route{}, SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, { @@ -759,7 +765,7 @@ func TestBuildGateway(t *testing.T) { Routes: map[RouteKey]*L7Route{}, ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)), SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, { @@ -770,7 +776,7 @@ func TestBuildGateway(t *testing.T) { Routes: map[RouteKey]*L7Route{}, ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)), SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, }, @@ -803,7 +809,7 @@ func TestBuildGateway(t *testing.T) { Routes: map[RouteKey]*L7Route{}, Conditions: staticConds.NewListenerProtocolConflict(conflict80PortMsg), SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, { @@ -814,7 +820,7 @@ func TestBuildGateway(t *testing.T) { Routes: map[RouteKey]*L7Route{}, Conditions: staticConds.NewListenerProtocolConflict(conflict80PortMsg), SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, { @@ -825,7 +831,7 @@ func TestBuildGateway(t *testing.T) { Routes: map[RouteKey]*L7Route{}, Conditions: staticConds.NewListenerProtocolConflict(conflict443PortMsg), SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, { @@ -837,7 +843,7 @@ func TestBuildGateway(t *testing.T) { Conditions: staticConds.NewListenerProtocolConflict(conflict80PortMsg), ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)), SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, { @@ -849,7 +855,7 @@ func TestBuildGateway(t *testing.T) { Conditions: staticConds.NewListenerProtocolConflict(conflict443PortMsg), ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)), SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, { @@ -861,7 +867,7 @@ func TestBuildGateway(t *testing.T) { Conditions: staticConds.NewListenerProtocolConflict(conflict443PortMsg), ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secretSameNs)), SupportedKinds: []v1.RouteGroupKind{ - {Kind: "HTTPRoute"}, + {Kind: kinds.HTTPRoute}, }, }, }, diff --git a/internal/mode/static/state/graph/gatewayclass.go b/internal/mode/static/state/graph/gatewayclass.go index 23bbb078e6..d54e14e627 100644 --- a/internal/mode/static/state/graph/gatewayclass.go +++ b/internal/mode/static/state/graph/gatewayclass.go @@ -12,6 +12,7 @@ import ( ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/gatewayclass" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation" ) @@ -93,7 +94,7 @@ func validateGatewayClass( var err error path := field.NewPath("spec").Child("parametersRef") if _, ok := supportedParamKinds[string(gc.Spec.ParametersRef.Kind)]; !ok { - err = field.NotSupported(path.Child("kind"), string(gc.Spec.ParametersRef.Kind), []string{"NginxProxy"}) + err = field.NotSupported(path.Child("kind"), string(gc.Spec.ParametersRef.Kind), []string{kinds.NginxProxy}) } else if npCfg == nil { err = field.NotFound(path.Child("name"), gc.Spec.ParametersRef.Name) conds = append(conds, staticConds.NewGatewayClassRefNotFound()) @@ -117,5 +118,5 @@ func validateGatewayClass( } var supportedParamKinds = map[string]struct{}{ - "NginxProxy": {}, + kinds.NginxProxy: {}, } diff --git a/internal/mode/static/state/graph/gatewayclass_test.go b/internal/mode/static/state/graph/gatewayclass_test.go index a60da7eecc..5ec28371ad 100644 --- a/internal/mode/static/state/graph/gatewayclass_test.go +++ b/internal/mode/static/state/graph/gatewayclass_test.go @@ -14,6 +14,7 @@ import ( "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/gatewayclass" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation/validationfakes" @@ -129,7 +130,7 @@ func TestBuildGatewayClass(t *testing.T) { gcWithParams := &v1.GatewayClass{ Spec: v1.GatewayClassSpec{ ParametersRef: &v1.ParametersReference{ - Kind: v1.Kind("NginxProxy"), + Kind: v1.Kind(kinds.NginxProxy), Namespace: helpers.GetPointer(v1.Namespace("test")), Name: "nginx-proxy", }, @@ -207,7 +208,7 @@ func TestBuildGatewayClass(t *testing.T) { gc: gcWithParams, np: &ngfAPI.NginxProxy{ TypeMeta: metav1.TypeMeta{ - Kind: "NginxProxy", + Kind: kinds.NginxProxy, }, Spec: ngfAPI.NginxProxySpec{ Telemetry: &ngfAPI.Telemetry{ @@ -227,7 +228,7 @@ func TestBuildGatewayClass(t *testing.T) { gc: gcWithInvalidKind, np: &ngfAPI.NginxProxy{ TypeMeta: metav1.TypeMeta{ - Kind: "NginxProxy", + Kind: kinds.NginxProxy, }, }, expected: &GatewayClass{ @@ -259,7 +260,7 @@ func TestBuildGatewayClass(t *testing.T) { gc: gcWithParams, np: &ngfAPI.NginxProxy{ TypeMeta: metav1.TypeMeta{ - Kind: "NginxProxy", + Kind: kinds.NginxProxy, }, Spec: ngfAPI.NginxProxySpec{ Telemetry: &ngfAPI.Telemetry{ diff --git a/internal/mode/static/state/graph/graph.go b/internal/mode/static/state/graph/graph.go index 2e23bd25ee..610ea4dc45 100644 --- a/internal/mode/static/state/graph/graph.go +++ b/internal/mode/static/state/graph/graph.go @@ -4,14 +4,18 @@ import ( v1 "k8s.io/api/core/v1" discoveryV1 "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" "sigs.k8s.io/gateway-api/apis/v1alpha3" "sigs.k8s.io/gateway-api/apis/v1beta1" ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/controller/index" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation" ) @@ -29,6 +33,7 @@ type ClusterState struct { ConfigMaps map[types.NamespacedName]*v1.ConfigMap NginxProxies map[types.NamespacedName]*ngfAPI.NginxProxy GRPCRoutes map[types.NamespacedName]*gatewayv1.GRPCRoute + NGFPolicies map[PolicyKey]policies.Policy } // Graph is a Graph-like representation of Gateway API resources. @@ -63,6 +68,8 @@ type Graph struct { BackendTLSPolicies map[types.NamespacedName]*BackendTLSPolicy // NginxProxy holds the NginxProxy config for the GatewayClass. NginxProxy *ngfAPI.NginxProxy + // NGFPolicies holds all NGF Policies. + NGFPolicies map[PolicyKey]*Policy } // ProtectedPorts are the ports that may not be configured by a listener with a descriptive name of each port. @@ -112,6 +119,54 @@ func (g *Graph) IsReferenced(resourceType client.Object, nsname types.Namespaced } } +// IsNGFPolicyRelevant returns whether the NGF Policy is a part of the Graph, or targets a resource in the Graph. +func (g *Graph) IsNGFPolicyRelevant( + policy policies.Policy, + gvk schema.GroupVersionKind, + nsname types.NamespacedName, +) bool { + key := PolicyKey{ + NsName: nsname, + GVK: gvk, + } + + if _, exists := g.NGFPolicies[key]; exists { + return true + } + + if policy == nil { + panic("policy cannot be nil") + } + + ref := policy.GetTargetRef() + + switch ref.Group { + case gatewayv1.GroupName: + return g.gatewayAPIResourceExist(ref, policy.GetNamespace()) + default: + return false + } +} + +func (g *Graph) gatewayAPIResourceExist(ref v1alpha2.LocalPolicyTargetReference, policyNs string) bool { + refNsName := types.NamespacedName{Name: string(ref.Name), Namespace: policyNs} + + switch kind := ref.Kind; kind { + case kinds.Gateway: + if g.Gateway == nil { + return false + } + + return gatewayExists(refNsName, g.Gateway.Source, g.IgnoredGateways) + case kinds.HTTPRoute, kinds.GRPCRoute: + _, exists := g.Routes[routeKeyForKind(kind, refNsName)] + return exists + + default: + return false + } +} + // BuildGraph builds a Graph from a state. func BuildGraph( state ClusterState, @@ -135,6 +190,7 @@ func BuildGraph( processedGws := processGateways(state.Gateways, gcName) refGrantResolver := newReferenceGrantResolver(state.ReferenceGrants) + gw := buildGateway(processedGws.Winner, secretResolver, gc, refGrantResolver, protectedPorts) processedBackendTLSPolicies := processBackendTLSPolicies( @@ -151,6 +207,7 @@ func BuildGraph( processedGws.GetAllNsNames(), npCfg, ) + bindRoutesToListeners(routes, gw, state.Namespaces) addBackendRefsToRouteRules(routes, refGrantResolver, state.Services, processedBackendTLSPolicies) @@ -158,6 +215,8 @@ func BuildGraph( referencedServices := buildReferencedServices(routes) + processedPolicies := processPolicies(state.NGFPolicies, validators.PolicyValidator, processedGws, routes) + g := &Graph{ GatewayClass: gc, Gateway: gw, @@ -170,7 +229,28 @@ func BuildGraph( ReferencedCaCertConfigMaps: configMapResolver.getResolvedConfigMaps(), BackendTLSPolicies: processedBackendTLSPolicies, NginxProxy: npCfg, + NGFPolicies: processedPolicies, } + g.attachPolicies(controllerName) + return g } + +func gatewayExists( + gwNsName types.NamespacedName, + winner *gatewayv1.Gateway, + ignored map[types.NamespacedName]*gatewayv1.Gateway, +) bool { + if winner == nil { + return false + } + + if client.ObjectKeyFromObject(winner) == gwNsName { + return true + } + + _, exists := ignored[gwNsName] + + return exists +} diff --git a/internal/mode/static/state/graph/graph_test.go b/internal/mode/static/state/graph/graph_test.go index 99332fc45f..e8097270c0 100644 --- a/internal/mode/static/state/graph/graph_test.go +++ b/internal/mode/static/state/graph/graph_test.go @@ -9,6 +9,7 @@ import ( discoveryV1 "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" @@ -20,6 +21,9 @@ import ( "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/controller/index" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies/policiesfakes" staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation/validationfakes" @@ -31,6 +35,8 @@ func TestBuildGraph(t *testing.T) { controllerName = "my.controller" ) + testNs := "test" + protectedPorts := ProtectedPorts{ 9113: "MetricsPort", 8081: "HealthPort", @@ -47,9 +53,9 @@ func TestBuildGraph(t *testing.T) { } btpAcceptedConds := []conditions.Condition{ - staticConds.NewBackendTLSPolicyAccepted(), - staticConds.NewBackendTLSPolicyAccepted(), - staticConds.NewBackendTLSPolicyAccepted(), + staticConds.NewPolicyAccepted(), + staticConds.NewPolicyAccepted(), + staticConds.NewPolicyAccepted(), } btp := BackendTLSPolicy{ @@ -82,7 +88,7 @@ func TestBuildGraph(t *testing.T) { }, Valid: true, IsReferenced: true, - Gateway: types.NamespacedName{Namespace: "test", Name: "gateway-1"}, + Gateway: types.NamespacedName{Namespace: testNs, Name: "gateway-1"}, Conditions: btpAcceptedConds, CaCertRef: types.NamespacedName{Namespace: "service", Name: "configmap"}, } @@ -132,14 +138,14 @@ func TestBuildGraph(t *testing.T) { createRoute := func(name string, gatewayName string, listenerName string) *gatewayv1.HTTPRoute { return &gatewayv1.HTTPRoute{ ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", + Namespace: testNs, Name: name, }, Spec: gatewayv1.HTTPRouteSpec{ CommonRouteSpec: gatewayv1.CommonRouteSpec{ ParentRefs: []gatewayv1.ParentReference{ { - Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), + Namespace: (*gatewayv1.Namespace)(helpers.GetPointer(testNs)), Name: gatewayv1.ObjectName(gatewayName), SectionName: (*gatewayv1.SectionName)(helpers.GetPointer(listenerName)), }, @@ -168,14 +174,14 @@ func TestBuildGraph(t *testing.T) { gr := &gatewayv1.GRPCRoute{ ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", + Namespace: testNs, Name: "gr", }, Spec: gatewayv1.GRPCRouteSpec{ CommonRouteSpec: gatewayv1.CommonRouteSpec{ ParentRefs: []gatewayv1.ParentReference{ { - Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), + Namespace: (*gatewayv1.Namespace)(helpers.GetPointer(testNs)), Name: gatewayv1.ObjectName("gateway-1"), SectionName: (*gatewayv1.SectionName)(helpers.GetPointer("listener-80-1")), }, @@ -198,7 +204,7 @@ func TestBuildGraph(t *testing.T) { secret := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", + Namespace: testNs, Name: "secret", }, Data: map[string][]byte{ @@ -210,7 +216,7 @@ func TestBuildGraph(t *testing.T) { ns := &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ - Name: "test", + Name: testNs, Labels: map[string]string{ "app": "allowed", }, @@ -220,7 +226,7 @@ func TestBuildGraph(t *testing.T) { createGateway := func(name string) *gatewayv1.Gateway { return &gatewayv1.Gateway{ ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", + Namespace: testNs, Name: name, }, Spec: gatewayv1.GatewaySpec{ @@ -289,8 +295,8 @@ func TestBuildGraph(t *testing.T) { From: []v1beta1.ReferenceGrantFrom{ { Group: gatewayv1.GroupName, - Kind: "Gateway", - Namespace: "test", + Kind: kinds.Gateway, + Namespace: gatewayv1.Namespace(testNs), }, }, To: []v1beta1.ReferenceGrantTo{ @@ -310,8 +316,8 @@ func TestBuildGraph(t *testing.T) { From: []v1beta1.ReferenceGrantFrom{ { Group: gatewayv1.GroupName, - Kind: "HTTPRoute", - Namespace: "test", + Kind: kinds.HTTPRoute, + Namespace: gatewayv1.Namespace(testNs), }, }, To: []v1beta1.ReferenceGrantTo{ @@ -339,6 +345,70 @@ func TestBuildGraph(t *testing.T) { }, } + // NGF Policies + // + // We have to use real policies here instead of a mocks because the Diff function we use in the test fails when + // using a mock because the mock has unexported fields. + // Testing one type of policy per attachment point should suffice. + polGVK := schema.GroupVersionKind{Kind: kinds.ClientSettingsPolicy} + hrPolicyKey := PolicyKey{GVK: polGVK, NsName: types.NamespacedName{Namespace: testNs, Name: "hrPolicy"}} + hrPolicy := &ngfAPI.ClientSettingsPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hrPolicy", + Namespace: testNs, + }, + TypeMeta: metav1.TypeMeta{Kind: kinds.ClientSettingsPolicy}, + Spec: ngfAPI.ClientSettingsPolicySpec{ + TargetRef: createTestRef(kinds.HTTPRoute, gatewayv1.GroupName, "hr-1"), + }, + } + processedRoutePolicy := &Policy{ + Source: hrPolicy, + Ancestor: &PolicyAncestor{ + Ancestor: gatewayv1.ParentReference{ + Group: helpers.GetPointer[gatewayv1.Group](gatewayv1.GroupName), + Kind: helpers.GetPointer[gatewayv1.Kind](kinds.HTTPRoute), + Namespace: (*gatewayv1.Namespace)(&testNs), + Name: "hr-1", + }, + }, + TargetRef: PolicyTargetRef{ + Kind: kinds.HTTPRoute, + Group: gatewayv1.GroupName, + Nsname: types.NamespacedName{Namespace: testNs, Name: "hr-1"}, + }, + Valid: true, + } + + gwPolicyKey := PolicyKey{GVK: polGVK, NsName: types.NamespacedName{Namespace: testNs, Name: "gwPolicy"}} + gwPolicy := &ngfAPI.ClientSettingsPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gwPolicy", + Namespace: testNs, + }, + TypeMeta: metav1.TypeMeta{Kind: kinds.ClientSettingsPolicy}, + Spec: ngfAPI.ClientSettingsPolicySpec{ + TargetRef: createTestRef(kinds.Gateway, gatewayv1.GroupName, "gateway-1"), + }, + } + processedGwPolicy := &Policy{ + Source: gwPolicy, + Ancestor: &PolicyAncestor{ + Ancestor: gatewayv1.ParentReference{ + Group: helpers.GetPointer[gatewayv1.Group](gatewayv1.GroupName), + Kind: helpers.GetPointer[gatewayv1.Kind](kinds.Gateway), + Namespace: (*gatewayv1.Namespace)(&testNs), + Name: "gateway-1", + }, + }, + TargetRef: PolicyTargetRef{ + Kind: kinds.Gateway, + Group: gatewayv1.GroupName, + Nsname: types.NamespacedName{Namespace: testNs, Name: "gateway-1"}, + }, + Valid: true, + } + createStateWithGatewayClass := func(gc *gatewayv1.GatewayClass) ClusterState { return ClusterState{ GatewayClasses: map[types.NamespacedName]*gatewayv1.GatewayClass{ @@ -378,6 +448,10 @@ func TestBuildGraph(t *testing.T) { NginxProxies: map[types.NamespacedName]*ngfAPI.NginxProxy{ client.ObjectKeyFromObject(proxy): proxy, }, + NGFPolicies: map[PolicyKey]policies.Policy{ + hrPolicyKey: hrPolicy, + gwPolicyKey: gwPolicy, + }, } } @@ -401,6 +475,7 @@ func TestBuildGraph(t *testing.T) { Hostnames: hr1.Spec.Hostnames, Rules: []RouteRule{createValidRuleWithBackendRefs(routeMatches)}, }, + Policies: []*Policy{processedRoutePolicy}, } routeGR := &L7Route{ @@ -468,7 +543,7 @@ func TestBuildGraph(t *testing.T) { CreateRouteKey(hr1): routeHR1, CreateRouteKey(gr): routeGR, }, - SupportedKinds: []gatewayv1.RouteGroupKind{{Kind: "HTTPRoute"}}, + SupportedKinds: []gatewayv1.RouteGroupKind{{Kind: kinds.HTTPRoute}}, AllowedRouteLabelSelector: labels.SelectorFromSet(map[string]string{"app": "allowed"}), }, { @@ -478,13 +553,14 @@ func TestBuildGraph(t *testing.T) { Attachable: true, Routes: map[RouteKey]*L7Route{CreateRouteKey(hr3): routeHR3}, ResolvedSecret: helpers.GetPointer(client.ObjectKeyFromObject(secret)), - SupportedKinds: []gatewayv1.RouteGroupKind{{Kind: "HTTPRoute"}}, + SupportedKinds: []gatewayv1.RouteGroupKind{{Kind: kinds.HTTPRoute}}, }, }, - Valid: true, + Valid: true, + Policies: []*Policy{processedGwPolicy}, }, IgnoredGateways: map[types.NamespacedName]*gatewayv1.Gateway{ - {Namespace: "test", Name: "gateway-2"}: gw2, + {Namespace: testNs, Name: "gateway-2"}: gw2, }, Routes: map[RouteKey]*L7Route{ CreateRouteKey(hr1): routeHR1, @@ -512,6 +588,10 @@ func TestBuildGraph(t *testing.T) { client.ObjectKeyFromObject(btp.Source): &btp, }, NginxProxy: proxy, + NGFPolicies: map[PolicyKey]*Policy{ + hrPolicyKey: processedRoutePolicy, + gwPolicyKey: processedGwPolicy, + }, } } @@ -523,7 +603,7 @@ func TestBuildGraph(t *testing.T) { ControllerName: controllerName, ParametersRef: &gatewayv1.ParametersReference{ Group: gatewayv1.Group("gateway.nginx.org"), - Kind: gatewayv1.Kind("NginxProxy"), + Kind: gatewayv1.Kind(kinds.NginxProxy), Name: "nginx-proxy", }, }, @@ -565,6 +645,7 @@ func TestBuildGraph(t *testing.T) { validation.Validators{ HTTPFieldsValidator: &validationfakes.FakeHTTPFieldsValidator{}, GenericValidator: &validationfakes.FakeGenericValidator{}, + PolicyValidator: &validationfakes.FakePolicyValidator{}, }, protectedPorts, ) @@ -577,13 +658,13 @@ func TestBuildGraph(t *testing.T) { func TestIsReferenced(t *testing.T) { baseSecret := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", + Namespace: testNs, Name: "secret", }, } sameNamespaceDifferentNameSecret := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", + Namespace: testNs, Name: "secret-different-name", }, } @@ -596,7 +677,7 @@ func TestIsReferenced(t *testing.T) { nsInGraph := &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ - Name: "test", + Name: testNs, Labels: map[string]string{ "app": "allowed", }, @@ -666,13 +747,13 @@ func TestIsReferenced(t *testing.T) { baseConfigMap := &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", + Namespace: testNs, Name: "configmap", }, } sameNamespaceDifferentNameConfigMap := &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", + Namespace: testNs, Name: "configmap-different-name", }, } @@ -688,7 +769,7 @@ func TestIsReferenced(t *testing.T) { Spec: gatewayv1.GatewayClassSpec{ ParametersRef: &gatewayv1.ParametersReference{ Group: ngfAPI.GroupName, - Kind: gatewayv1.Kind("NginxProxy"), + Kind: gatewayv1.Kind(kinds.NginxProxy), Name: "nginx-proxy-in-gc", }, }, @@ -876,3 +957,167 @@ func TestIsReferenced(t *testing.T) { }) } } + +func TestIsNGFPolicyRelevant(t *testing.T) { + policyGVK := schema.GroupVersionKind{Kind: "MyKind"} + existingPolicyNsName := types.NamespacedName{Namespace: "test", Name: "pol"} + + hrKey := RouteKey{RouteType: RouteTypeHTTP, NamespacedName: types.NamespacedName{Namespace: "test", Name: "hr"}} + grKey := RouteKey{RouteType: RouteTypeGRPC, NamespacedName: types.NamespacedName{Namespace: "test", Name: "gr"}} + + getGraph := func() *Graph { + return &Graph{ + Gateway: &Gateway{ + Source: &gatewayv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gw", + Namespace: "test", + }, + }, + }, + IgnoredGateways: map[types.NamespacedName]*gatewayv1.Gateway{ + {Namespace: "test", Name: "ignored"}: {}, + }, + Routes: map[RouteKey]*L7Route{ + hrKey: {}, + grKey: {}, + }, + NGFPolicies: map[PolicyKey]*Policy{ + {GVK: policyGVK, NsName: existingPolicyNsName}: { + Source: &policiesfakes.FakePolicy{}, + }, + }, + } + } + + type modFunc func(g *Graph) *Graph + + getModifiedGraph := func(mod modFunc) *Graph { + return mod(getGraph()) + } + + getPolicy := func(ref v1alpha2.LocalPolicyTargetReference) policies.Policy { + return &policiesfakes.FakePolicy{ + GetNamespaceStub: func() string { + return testNs + }, + GetTargetRefStub: func() v1alpha2.LocalPolicyTargetReference { + return ref + }, + } + } + + tests := []struct { + name string + graph *Graph + policy policies.Policy + nsname types.NamespacedName + expRelevant bool + }{ + { + name: "relevant; policy exists in graph", + graph: getGraph(), + policy: &policiesfakes.FakePolicy{}, + nsname: existingPolicyNsName, + expRelevant: true, + }, + { + name: "irrelevant; policy does not exist in graph and is empty (delete event)", + graph: getGraph(), + policy: &policiesfakes.FakePolicy{}, + nsname: types.NamespacedName{Namespace: "diff", Name: "diff"}, + expRelevant: false, + }, + { + name: "relevant; policy references the winning gateway", + graph: getGraph(), + policy: getPolicy(createTestRef(kinds.Gateway, gatewayv1.GroupName, "gw")), + nsname: types.NamespacedName{Namespace: "test", Name: "ref-gw"}, + expRelevant: true, + }, + { + name: "relevant; policy references an ignored gateway", + graph: getGraph(), + policy: getPolicy(createTestRef(kinds.Gateway, gatewayv1.GroupName, "ignored")), + nsname: types.NamespacedName{Namespace: "test", Name: "ref-ignored"}, + expRelevant: true, + }, + { + name: "relevant; policy references an httproute in the graph", + graph: getGraph(), + policy: getPolicy(createTestRef(kinds.HTTPRoute, gatewayv1.GroupName, "hr")), + nsname: types.NamespacedName{Namespace: "test", Name: "ref-hr"}, + expRelevant: true, + }, + { + name: "relevant; policy references a grpcroute in the graph", + graph: getGraph(), + policy: getPolicy(createTestRef(kinds.GRPCRoute, gatewayv1.GroupName, "gr")), + nsname: types.NamespacedName{Namespace: "test", Name: "ref-gr"}, + expRelevant: true, + }, + { + name: "irrelevant; policy does not reference a relevant gw or route in the graph", + graph: getGraph(), + policy: getPolicy(createTestRef(kinds.Gateway, gatewayv1.GroupName, "diff")), + nsname: types.NamespacedName{Namespace: "test", Name: "not-relevant"}, + expRelevant: false, + }, + { + name: "irrelevant; policy references an unsupported kind in the Gateway group", + graph: getGraph(), + policy: getPolicy(createTestRef("GatewayClass", gatewayv1.GroupName, "diff")), + nsname: types.NamespacedName{Namespace: "test", Name: "unsupported-kind"}, + expRelevant: false, + }, + { + name: "irrelevant; policy references an unsupported group", + graph: getGraph(), + policy: getPolicy(createTestRef(kinds.Gateway, "SomeGroup", "diff")), + nsname: types.NamespacedName{Namespace: "test", Name: "unsupported-group"}, + expRelevant: false, + }, + { + name: "irrelevant; policy references a Gateway, but the graph's Gateway is nil", + graph: getModifiedGraph(func(g *Graph) *Graph { + g.Gateway = nil + return g + }), + policy: getPolicy(createTestRef(kinds.Gateway, gatewayv1.GroupName, "diff")), + nsname: types.NamespacedName{Namespace: "test", Name: "nil-gw"}, + expRelevant: false, + }, + { + name: "irrelevant; policy references a Gateway, but the graph's Gateway.Source is nil", + graph: getModifiedGraph(func(g *Graph) *Graph { + g.Gateway.Source = nil + return g + }), + policy: getPolicy(createTestRef(kinds.Gateway, gatewayv1.GroupName, "diff")), + nsname: types.NamespacedName{Namespace: "test", Name: "nil-gw-source"}, + expRelevant: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + relevant := test.graph.IsNGFPolicyRelevant(test.policy, policyGVK, test.nsname) + g.Expect(relevant).To(Equal(test.expRelevant)) + }) + } +} + +func TestIsNGFPolicyRelevantPanics(t *testing.T) { + g := NewWithT(t) + graph := &Graph{} + nsname := types.NamespacedName{Namespace: "test", Name: "pol"} + gvk := schema.GroupVersionKind{Kind: "MyKind"} + + isRelevant := func() { + _ = graph.IsNGFPolicyRelevant(nil, gvk, nsname) + } + + g.Expect(isRelevant).To(Panic()) +} diff --git a/internal/mode/static/state/graph/nginxproxy.go b/internal/mode/static/state/graph/nginxproxy.go index 327d6cca89..7df28d2615 100644 --- a/internal/mode/static/state/graph/nginxproxy.go +++ b/internal/mode/static/state/graph/nginxproxy.go @@ -6,6 +6,7 @@ import ( v1 "sigs.k8s.io/gateway-api/apis/v1" ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation" ) @@ -30,7 +31,7 @@ func isNginxProxyReferenced(npNSName types.NamespacedName, gc *GatewayClass) boo func gcReferencesAnyNginxProxy(gc *v1.GatewayClass) bool { if gc != nil { ref := gc.Spec.ParametersRef - return ref != nil && ref.Group == ngfAPI.GroupName && ref.Kind == v1.Kind("NginxProxy") + return ref != nil && ref.Group == ngfAPI.GroupName && ref.Kind == v1.Kind(kinds.NginxProxy) } return false @@ -49,7 +50,10 @@ func validateNginxProxy( telPath := spec.Child("telemetry") if telemetry.ServiceName != nil { if err := validator.ValidateServiceName(*telemetry.ServiceName); err != nil { - allErrs = append(allErrs, field.Invalid(telPath.Child("serviceName"), *telemetry.ServiceName, err.Error())) + allErrs = append( + allErrs, + field.Invalid(telPath.Child("serviceName"), *telemetry.ServiceName, err.Error()), + ) } } diff --git a/internal/mode/static/state/graph/nginxproxy_test.go b/internal/mode/static/state/graph/nginxproxy_test.go index 80e66da56a..6c0f3d52b4 100644 --- a/internal/mode/static/state/graph/nginxproxy_test.go +++ b/internal/mode/static/state/graph/nginxproxy_test.go @@ -11,6 +11,7 @@ import ( ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation/validationfakes" ) @@ -35,7 +36,7 @@ func TestGetNginxProxy(t *testing.T) { Spec: v1.GatewayClassSpec{ ParametersRef: &v1.ParametersReference{ Group: ngfAPI.GroupName, - Kind: v1.Kind("NginxProxy"), + Kind: v1.Kind(kinds.NginxProxy), Name: "np1", }, }, @@ -60,7 +61,7 @@ func TestGetNginxProxy(t *testing.T) { Spec: v1.GatewayClassSpec{ ParametersRef: &v1.ParametersReference{ Group: ngfAPI.GroupName, - Kind: v1.Kind("NginxProxy"), + Kind: v1.Kind(kinds.NginxProxy), Name: "np2", }, }, @@ -96,7 +97,7 @@ func TestIsNginxProxyReferenced(t *testing.T) { Spec: v1.GatewayClassSpec{ ParametersRef: &v1.ParametersReference{ Group: ngfAPI.GroupName, - Kind: v1.Kind("NginxProxy"), + Kind: v1.Kind(kinds.NginxProxy), Name: "nginx-proxy", }, }, @@ -126,7 +127,7 @@ func TestIsNginxProxyReferenced(t *testing.T) { Spec: v1.GatewayClassSpec{ ParametersRef: &v1.ParametersReference{ Group: ngfAPI.GroupName, - Kind: v1.Kind("NginxProxy"), + Kind: v1.Kind(kinds.NginxProxy), Name: "nginx-proxy", }, }, @@ -170,7 +171,7 @@ func TestGCReferencesAnyNginxProxy(t *testing.T) { Spec: v1.GatewayClassSpec{ ParametersRef: &v1.ParametersReference{ Group: v1.Group("wrong-group"), - Kind: v1.Kind("NginxProxy"), + Kind: v1.Kind(kinds.NginxProxy), Name: "wrong-group", }, }, @@ -196,7 +197,7 @@ func TestGCReferencesAnyNginxProxy(t *testing.T) { Spec: v1.GatewayClassSpec{ ParametersRef: &v1.ParametersReference{ Group: ngfAPI.GroupName, - Kind: v1.Kind("NginxProxy"), + Kind: v1.Kind(kinds.NginxProxy), Name: "nginx-proxy", }, }, @@ -297,7 +298,9 @@ func TestValidateNginxProxy(t *testing.T) { Spec: ngfAPI.NginxProxySpec{ Telemetry: &ngfAPI.Telemetry{ Exporter: &ngfAPI.TelemetryExporter{ - Interval: helpers.GetPointer[ngfAPI.Duration]("my-interval"), // any value is invalid by the validator + Interval: helpers.GetPointer[ngfAPI.Duration]( + "my-interval", + ), // any value is invalid by the validator }, }, }, diff --git a/internal/mode/static/state/graph/policies.go b/internal/mode/static/state/graph/policies.go new file mode 100644 index 0000000000..e8cbb50a5a --- /dev/null +++ b/internal/mode/static/state/graph/policies.go @@ -0,0 +1,287 @@ +package graph + +import ( + "fmt" + "sort" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + v1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies" + ngfsort "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/sort" + staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation" +) + +// Policy represents an NGF Policy. +type Policy struct { + // Source is the corresponding Policy resource. + Source policies.Policy + // Ancestor is the ancestor object of the Policy. Used in status. + Ancestor *PolicyAncestor + // TargetRef is the resource that the Policy targets. + TargetRef PolicyTargetRef + // Conditions holds the conditions for the Policy. + // These conditions apply to the entire Policy. + // The conditions in the Ancestor apply only to the Policy in regard to the Ancestor. + Conditions []conditions.Condition + // Valid indicates whether the Policy is valid. + Valid bool +} + +// PolicyAncestor represents an ancestor of a Policy. +type PolicyAncestor struct { + // Ancestor is the ancestor object. + Ancestor v1.ParentReference + // Conditions contains the list of conditions of the Policy in relation to the ancestor. + Conditions []conditions.Condition +} + +// PolicyTargetRef represents the object that the Policy is targeting. +type PolicyTargetRef struct { + // Kind is the Kind of the object. + Kind v1.Kind + // Group is the Group of the object. + Group v1.Group + // Nsname is the NamespacedName of the object. + Nsname types.NamespacedName +} + +// PolicyKey is a unique identifier for an NGF Policy. +type PolicyKey struct { + // Nsname is the NamespacedName of the Policy. + NsName types.NamespacedName + // GVK is the GroupVersionKind of the Policy. + GVK schema.GroupVersionKind +} + +const ( + gatewayGroupKind = v1.GroupName + "/" + kinds.Gateway + hrGroupKind = v1.GroupName + "/" + kinds.HTTPRoute + grpcGroupKind = v1.GroupName + "/" + kinds.GRPCRoute +) + +// attachPolicies attaches the graph's processed policies to the resources they target. It modifies the graph in place. +func (g *Graph) attachPolicies(ctlrName string) { + if g.Gateway == nil { + return + } + + for _, policy := range g.NGFPolicies { + ref := policy.TargetRef + + switch ref.Kind { + case kinds.Gateway: + attachPolicyToGateway(policy, g.Gateway, g.IgnoredGateways, ctlrName) + case kinds.HTTPRoute, kinds.GRPCRoute: + route, exists := g.Routes[routeKeyForKind(ref.Kind, ref.Nsname)] + if !exists { + continue + } + + attachPolicyToRoute(policy, route, ctlrName) + } + } +} + +func attachPolicyToRoute(policy *Policy, route *L7Route, ctlrName string) { + kind := v1.Kind(kinds.HTTPRoute) + if route.RouteType == RouteTypeGRPC { + kind = kinds.GRPCRoute + } + + routeNsName := types.NamespacedName{Namespace: route.Source.GetNamespace(), Name: route.Source.GetName()} + + ancestor := &PolicyAncestor{ + Ancestor: createParentReference(v1.GroupName, kind, routeNsName), + } + + curAncestorStatus := policy.Source.GetPolicyStatus().Ancestors + if ancestorsFull(curAncestorStatus, ctlrName) { + // FIXME (kate-osborn): https://github.com/nginxinc/nginx-gateway-fabric/issues/1987 + return + } + + policy.Ancestor = ancestor + + if !route.Valid || !route.Attachable || len(route.ParentRefs) == 0 { + policy.Ancestor.Conditions = []conditions.Condition{staticConds.NewPolicyTargetNotFound("TargetRef is invalid")} + + return + } + + route.Policies = append(route.Policies, policy) +} + +func attachPolicyToGateway( + policy *Policy, + gw *Gateway, + ignoredGateways map[types.NamespacedName]*v1.Gateway, + ctlrName string, +) { + ref := policy.TargetRef + + _, ignored := ignoredGateways[ref.Nsname] + + if !ignored && ref.Nsname != client.ObjectKeyFromObject(gw.Source) { + return + } + + ancestor := &PolicyAncestor{ + Ancestor: createParentReference(v1.GroupName, kinds.Gateway, ref.Nsname), + } + + curAncestorStatus := policy.Source.GetPolicyStatus().Ancestors + if ancestorsFull(curAncestorStatus, ctlrName) { + // FIXME (kate-osborn): https://github.com/nginxinc/nginx-gateway-fabric/issues/1987 + return + } + + policy.Ancestor = ancestor + + if ignored { + policy.Ancestor.Conditions = []conditions.Condition{staticConds.NewPolicyTargetNotFound("TargetRef is ignored")} + return + } + + if !gw.Valid { + policy.Ancestor.Conditions = []conditions.Condition{staticConds.NewPolicyTargetNotFound("TargetRef is invalid")} + return + } + + gw.Policies = append(gw.Policies, policy) +} + +func processPolicies( + policies map[PolicyKey]policies.Policy, + validator validation.PolicyValidator, + gateways processedGateways, + routes map[RouteKey]*L7Route, +) map[PolicyKey]*Policy { + if len(policies) == 0 || gateways.Winner == nil { + return nil + } + + processedPolicies := make(map[PolicyKey]*Policy) + + for key, policy := range policies { + ref := policy.GetTargetRef() + refNsName := types.NamespacedName{Name: string(ref.Name), Namespace: policy.GetNamespace()} + + refGroupKind := fmt.Sprintf("%s/%s", ref.Group, ref.Kind) + + switch refGroupKind { + case gatewayGroupKind: + if !gatewayExists(refNsName, gateways.Winner, gateways.Ignored) { + continue + } + case hrGroupKind, grpcGroupKind: + if _, exists := routes[routeKeyForKind(ref.Kind, refNsName)]; !exists { + continue + } + default: + continue + } + + var conds []conditions.Condition + + if err := validator.Validate(policy); err != nil { + conds = append(conds, staticConds.NewPolicyInvalid(err.Error())) + } + + processedPolicies[key] = &Policy{ + Source: policy, + Valid: len(conds) == 0, + Conditions: conds, + TargetRef: PolicyTargetRef{ + Kind: ref.Kind, + Group: ref.Group, + Nsname: refNsName, + }, + } + } + + markConflictedPolicies(processedPolicies, validator) + + return processedPolicies +} + +// markConflictedPolicies marks policies that conflict with a policy of greater precedence as invalid. +// Policies are sorted by timestamp and then alphabetically. +func markConflictedPolicies(policies map[PolicyKey]*Policy, validator validation.PolicyValidator) { + // Policies can only conflict if they are the same policy type (gvk) and they target the same resource. + type key struct { + policyGVK schema.GroupVersionKind + PolicyTargetRef + } + + possibles := make(map[key][]*Policy) + + for pk, p := range policies { + // If a policy is invalid, it cannot conflict with another policy. + if p.Valid { + ak := key{ + PolicyTargetRef: p.TargetRef, + policyGVK: pk.GVK, + } + if possibles[ak] == nil { + possibles[ak] = make([]*Policy, 0) + } + possibles[ak] = append(possibles[ak], p) + } + } + + for _, policyList := range possibles { + if len(policyList) == 1 { + // if the policyList only has one entry, then we don't need to check for conflicts. + continue + } + + // First, we sort the policyList according to the rules in the spec. + // This will put them in priority-order. + sort.Slice( + policyList, func(i, j int) bool { + return ngfsort.LessClientObject(policyList[i].Source, policyList[j].Source) + }, + ) + + // Second, we range over the policyList, starting with the highest priority policy. + for i := range policyList { + if !policyList[i].Valid { + // Ignore policy that has already been marked as invalid. + continue + } + + // Next, we compare the ith policy (policyList[i]) to the rest of the policies in the list. + // The ith policy takes precedence over polices that follow it, so if there is a conflict between + // it and a subsequent policy, the ith policy wins, and we mark the subsequent policy as invalid. + // Example: policyList = [A, B, C] where B conflicts with A. + // i=A, j=B => conflict, B's marked as invalid. + // i=A, j=C => no conflict. + // i=B, j=C => B's already invalid, so we hit the continue. + // i=C => j loop terminates. + // Results: A, and C are valid. B is invalid. + for j := i + 1; j < len(policyList); j++ { + if !policyList[j].Valid { + // Ignore policy that has already been marked as invalid. + continue + } + + if validator.Conflicts(policyList[i].Source, policyList[j].Source) { + conflicted := policyList[j] + conflicted.Valid = false + conflicted.Conditions = append(conflicted.Conditions, staticConds.NewPolicyConflicted( + fmt.Sprintf( + "Conflicts with another %s", + conflicted.Source.GetObjectKind().GroupVersionKind().Kind, + ), + )) + } + } + } + } +} diff --git a/internal/mode/static/state/graph/policies_test.go b/internal/mode/static/state/graph/policies_test.go new file mode 100644 index 0000000000..a2ee60204a --- /dev/null +++ b/internal/mode/static/state/graph/policies_test.go @@ -0,0 +1,870 @@ +package graph + +import ( + "errors" + "slices" + "testing" + + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + v1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" + + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies/policiesfakes" + staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation" +) + +var testNs = "test" + +func TestAttachPolicies(t *testing.T) { + policyGVK := schema.GroupVersionKind{Group: "Group", Version: "Version", Kind: "Policy"} + + gwPolicyKey := createTestPolicyKey(policyGVK, "gw-policy") + gwPolicy := &Policy{ + Valid: true, + Source: &policiesfakes.FakePolicy{}, + TargetRef: PolicyTargetRef{ + Kind: kinds.Gateway, + Group: v1.GroupName, + Nsname: types.NamespacedName{Namespace: testNs, Name: "gateway"}, + }, + } + + routePolicyKey := createTestPolicyKey(policyGVK, "route-policy") + routePolicy := &Policy{ + Valid: true, + Source: &policiesfakes.FakePolicy{}, + TargetRef: PolicyTargetRef{ + Kind: kinds.HTTPRoute, + Group: v1.GroupName, + Nsname: types.NamespacedName{Namespace: testNs, Name: "hr-route"}, + }, + } + + grpcRoutePolicyKey := createTestPolicyKey(policyGVK, "grpc-route-policy") + grpcRoutePolicy := &Policy{ + Valid: true, + Source: &policiesfakes.FakePolicy{}, + TargetRef: PolicyTargetRef{ + Kind: kinds.GRPCRoute, + Group: v1.GroupName, + Nsname: types.NamespacedName{Namespace: testNs, Name: "grpc-route"}, + }, + } + + ngfPolicies := map[PolicyKey]*Policy{ + gwPolicyKey: gwPolicy, + routePolicyKey: routePolicy, + grpcRoutePolicyKey: grpcRoutePolicy, + } + + createRouteKey := func(name string, routeType RouteType) RouteKey { + return RouteKey{ + NamespacedName: types.NamespacedName{Name: name, Namespace: testNs}, + RouteType: routeType, + } + } + + newGraph := func() *Graph { + return &Graph{ + Gateway: &Gateway{ + Source: &v1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gateway", + Namespace: testNs, + }, + }, + Valid: true, + }, + Routes: map[RouteKey]*L7Route{ + createRouteKey("hr-route", RouteTypeHTTP): { + Source: &v1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hr-route", + Namespace: testNs, + }, + }, + ParentRefs: []ParentRef{ + { + Attachment: &ParentRefAttachmentStatus{ + Attached: true, + }, + }, + }, + Valid: true, + Attachable: true, + }, + createRouteKey("grpc-route", RouteTypeGRPC): { + Source: &v1alpha2.GRPCRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "grpc-route", + Namespace: testNs, + }, + }, + ParentRefs: []ParentRef{ + { + Attachment: &ParentRefAttachmentStatus{ + Attached: true, + }, + }, + }, + Valid: true, + Attachable: true, + }, + }, + + NGFPolicies: ngfPolicies, + } + } + + newModifiedGraph := func(mod func(g *Graph) *Graph) *Graph { + return mod(newGraph()) + } + + expectNoPolicyAttachment := func(g *WithT, graph *Graph) { + if graph.Gateway != nil { + g.Expect(graph.Gateway.Policies).To(BeNil()) + } + + for _, r := range graph.Routes { + g.Expect(r.Policies).To(BeNil()) + } + } + + expectPolicyAttachment := func(g *WithT, graph *Graph) { + if graph.Gateway != nil { + g.Expect(graph.Gateway.Policies).To(HaveLen(1)) + } + + for _, r := range graph.Routes { + g.Expect(r.Policies).To(HaveLen(1)) + } + } + + expectGatewayPolicyAttachment := func(g *WithT, graph *Graph) { + if graph.Gateway != nil { + g.Expect(graph.Gateway.Policies).To(HaveLen(1)) + } + + for _, r := range graph.Routes { + g.Expect(r.Policies).To(BeNil()) + } + } + + tests := []struct { + graph *Graph + expect func(g *WithT, graph *Graph) + name string + }{ + { + name: "nil Gateway", + graph: newModifiedGraph(func(g *Graph) *Graph { + g.Gateway = nil + return g + }), + expect: expectNoPolicyAttachment, + }, + { + name: "nil routes", + graph: newModifiedGraph(func(g *Graph) *Graph { + g.Routes = nil + return g + }), + expect: expectGatewayPolicyAttachment, + }, + { + name: "normal", + graph: newGraph(), + expect: expectPolicyAttachment, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + test.graph.attachPolicies("nginx-gateway") + test.expect(g, test.graph) + }) + } +} + +func TestAttachPolicyToRoute(t *testing.T) { + routeNsName := types.NamespacedName{Namespace: testNs, Name: "hr-route"} + + createRoute := func(routeType RouteType, valid, attachable, parentRefs bool) *L7Route { + route := &L7Route{ + Source: &v1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: routeNsName.Name, + Namespace: routeNsName.Namespace, + }, + }, + Valid: valid, + Attachable: attachable, + RouteType: routeType, + } + + if parentRefs { + route.ParentRefs = []ParentRef{ + { + Attachment: &ParentRefAttachmentStatus{ + Attached: true, + }, + }, + } + } + + return route + } + + createGRPCRoute := func(valid, attachable, parentRefs bool) *L7Route { + return createRoute(RouteTypeGRPC, valid, attachable, parentRefs) + } + + createHTTPRoute := func(valid, attachable, parentRefs bool) *L7Route { + return createRoute(RouteTypeHTTP, valid, attachable, parentRefs) + } + + createExpAncestor := func(kind v1.Kind) v1.ParentReference { + return v1.ParentReference{ + Group: helpers.GetPointer[v1.Group](v1.GroupName), + Kind: helpers.GetPointer[v1.Kind](kind), + Namespace: (*v1.Namespace)(&routeNsName.Namespace), + Name: v1.ObjectName(routeNsName.Name), + } + } + + tests := []struct { + route *L7Route + policy *Policy + expAncestor *PolicyAncestor + name string + expAttached bool + }{ + { + name: "policy attaches to http route", + route: createHTTPRoute(true /*valid*/, true /*attachable*/, true /*parentRefs*/), + policy: &Policy{Source: &policiesfakes.FakePolicy{}}, + expAncestor: &PolicyAncestor{ + Ancestor: createExpAncestor(kinds.HTTPRoute), + }, + expAttached: true, + }, + { + name: "policy attaches to grpc route", + route: createGRPCRoute(true /*valid*/, true /*attachable*/, true /*parentRefs*/), + policy: &Policy{Source: &policiesfakes.FakePolicy{}}, + expAncestor: &PolicyAncestor{ + Ancestor: createExpAncestor(kinds.GRPCRoute), + }, + expAttached: true, + }, + { + name: "no attachment; unattachable route", + route: createHTTPRoute(true /*valid*/, false /*attachable*/, true /*parentRefs*/), + policy: &Policy{Source: &policiesfakes.FakePolicy{}}, + expAncestor: &PolicyAncestor{ + Ancestor: createExpAncestor(kinds.HTTPRoute), + Conditions: []conditions.Condition{staticConds.NewPolicyTargetNotFound("TargetRef is invalid")}, + }, + expAttached: false, + }, + { + name: "no attachment; missing parentRefs", + route: createHTTPRoute(true /*valid*/, true /*attachable*/, false /*parentRefs*/), + policy: &Policy{Source: &policiesfakes.FakePolicy{}}, + expAncestor: &PolicyAncestor{ + Ancestor: createExpAncestor(kinds.HTTPRoute), + Conditions: []conditions.Condition{staticConds.NewPolicyTargetNotFound("TargetRef is invalid")}, + }, + expAttached: false, + }, + { + name: "no attachment; invalid route", + route: createHTTPRoute(false /*valid*/, true /*attachable*/, true /*parentRefs*/), + policy: &Policy{Source: &policiesfakes.FakePolicy{}}, + expAncestor: &PolicyAncestor{ + Ancestor: createExpAncestor(kinds.HTTPRoute), + Conditions: []conditions.Condition{staticConds.NewPolicyTargetNotFound("TargetRef is invalid")}, + }, + expAttached: false, + }, + { + name: "no attachment; max ancestors", + route: createHTTPRoute(true /*valid*/, true /*attachable*/, true /*parentRefs*/), + policy: &Policy{Source: createTestPolicyWithAncestors(16)}, + expAncestor: nil, + expAttached: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + attachPolicyToRoute(test.policy, test.route, "nginx-gateway") + + if test.expAttached { + g.Expect(test.route.Policies).To(HaveLen(1)) + } else { + g.Expect(test.route.Policies).To(BeEmpty()) + } + + g.Expect(test.policy.Ancestor).To(BeEquivalentTo(test.expAncestor)) + }) + } +} + +func TestAttachPolicyToGateway(t *testing.T) { + gatewayNsName := types.NamespacedName{Namespace: testNs, Name: "gateway"} + gateway2NsName := types.NamespacedName{Namespace: testNs, Name: "gateway2"} + ignoredGatewayNsName := types.NamespacedName{Namespace: testNs, Name: "ignored"} + + newGateway := func(valid bool, nsname types.NamespacedName) *Gateway { + return &Gateway{ + Source: &v1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: nsname.Namespace, + Name: nsname.Name, + }, + }, + Valid: valid, + } + } + + getGatewayParentRef := func(gwNsName types.NamespacedName) v1.ParentReference { + return v1.ParentReference{ + Group: helpers.GetPointer[v1.Group](v1.GroupName), + Kind: helpers.GetPointer[v1.Kind]("Gateway"), + Namespace: (*v1.Namespace)(&gwNsName.Namespace), + Name: v1.ObjectName(gwNsName.Name), + } + } + + tests := []struct { + policy *Policy + gw *Gateway + expAncestor *PolicyAncestor + name string + expAttached bool + }{ + { + name: "attached", + policy: &Policy{ + Source: &policiesfakes.FakePolicy{}, + TargetRef: PolicyTargetRef{ + Nsname: gatewayNsName, + Kind: "Gateway", + }, + }, + gw: newGateway(true, gatewayNsName), + expAncestor: &PolicyAncestor{ + Ancestor: getGatewayParentRef(gatewayNsName), + }, + expAttached: true, + }, + { + name: "not attached; gateway ignored", + policy: &Policy{ + Source: &policiesfakes.FakePolicy{}, + TargetRef: PolicyTargetRef{ + Nsname: ignoredGatewayNsName, + Kind: "Gateway", + }, + }, + gw: newGateway(true, gatewayNsName), + expAncestor: &PolicyAncestor{ + Ancestor: getGatewayParentRef(ignoredGatewayNsName), + Conditions: []conditions.Condition{staticConds.NewPolicyTargetNotFound("TargetRef is ignored")}, + }, + expAttached: false, + }, + { + name: "not attached; invalid gateway", + policy: &Policy{ + Source: &policiesfakes.FakePolicy{}, + TargetRef: PolicyTargetRef{ + Nsname: gatewayNsName, + Kind: "Gateway", + }, + }, + gw: newGateway(false, gatewayNsName), + expAncestor: &PolicyAncestor{ + Ancestor: getGatewayParentRef(gatewayNsName), + Conditions: []conditions.Condition{staticConds.NewPolicyTargetNotFound("TargetRef is invalid")}, + }, + expAttached: false, + }, + { + name: "not attached; non-NGF gateway", + policy: &Policy{ + Source: &policiesfakes.FakePolicy{}, + TargetRef: PolicyTargetRef{ + Nsname: gateway2NsName, + Kind: "Gateway", + }, + }, + gw: newGateway(true, gatewayNsName), + expAncestor: nil, + expAttached: false, + }, + { + name: "not attached; max ancestors", + policy: &Policy{ + Source: createTestPolicyWithAncestors(16), + TargetRef: PolicyTargetRef{ + Nsname: gatewayNsName, + Kind: "Gateway", + }, + }, + gw: newGateway(true, gatewayNsName), + expAncestor: nil, + expAttached: false, + }, + } + + for _, test := range tests { + ignoredGateways := map[types.NamespacedName]*v1.Gateway{ + ignoredGatewayNsName: nil, + } + + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + attachPolicyToGateway(test.policy, test.gw, ignoredGateways, "nginx-gateway") + + if test.expAttached { + g.Expect(test.gw.Policies).To(HaveLen(1)) + } else { + g.Expect(test.gw.Policies).To(BeEmpty()) + } + + g.Expect(test.policy.Ancestor).To(BeEquivalentTo(test.expAncestor)) + }) + } +} + +func TestProcessPolicies(t *testing.T) { + policyGVK := schema.GroupVersionKind{Group: "Group", Version: "Version", Kind: "MyPolicy"} + + // These refs reference objects that belong to NGF. + // Policies that contain these refs should be processed. + hrRef := createTestRef(kinds.HTTPRoute, v1.GroupName, "hr") + grpcRef := createTestRef(kinds.GRPCRoute, v1.GroupName, "grpc") + gatewayRef := createTestRef(kinds.Gateway, v1.GroupName, "gw") + ignoredGatewayRef := createTestRef(kinds.Gateway, v1.GroupName, "ignored") + + // These refs reference objects that do not belong to NGF. + // Policies that contain these refs should NOT be processed. + hrDoesNotExistRef := createTestRef(kinds.HTTPRoute, v1.GroupName, "dne") + hrWrongGroup := createTestRef(kinds.HTTPRoute, "WrongGroup", "hr") + gatewayWrongGroupRef := createTestRef(kinds.Gateway, "WrongGroup", "gw") + nonNGFGatewayRef := createTestRef(kinds.Gateway, v1.GroupName, "not-ours") + + pol1, pol1Key := createTestPolicyAndKey(policyGVK, hrRef, "pol1") + pol2, pol2Key := createTestPolicyAndKey(policyGVK, grpcRef, "pol2") + pol3, pol3Key := createTestPolicyAndKey(policyGVK, gatewayRef, "pol3") + pol4, pol4Key := createTestPolicyAndKey(policyGVK, ignoredGatewayRef, "pol4") + pol5, pol5Key := createTestPolicyAndKey(policyGVK, hrDoesNotExistRef, "pol5") + pol6, pol6Key := createTestPolicyAndKey(policyGVK, hrWrongGroup, "pol6") + pol7, pol7Key := createTestPolicyAndKey(policyGVK, gatewayWrongGroupRef, "pol7") + pol8, pol8Key := createTestPolicyAndKey(policyGVK, nonNGFGatewayRef, "pol8") + + pol1Conflict, pol1ConflictKey := createTestPolicyAndKey(policyGVK, hrRef, "pol1-conflict") + + allValidValidator := &policiesfakes.FakeValidator{} + + tests := []struct { + validator validation.PolicyValidator + policies map[PolicyKey]policies.Policy + expProcessedPolicies map[PolicyKey]*Policy + name string + }{ + { + name: "nil policies", + expProcessedPolicies: nil, + }, + { + name: "mix of relevant and irrelevant policies", + validator: allValidValidator, + policies: map[PolicyKey]policies.Policy{ + pol1Key: pol1, + pol2Key: pol2, + pol3Key: pol3, + pol4Key: pol4, + pol5Key: pol5, + pol6Key: pol6, + pol7Key: pol7, + pol8Key: pol8, + }, + expProcessedPolicies: map[PolicyKey]*Policy{ + pol1Key: { + Source: pol1, + TargetRef: PolicyTargetRef{ + Nsname: types.NamespacedName{Namespace: testNs, Name: "hr"}, + Kind: kinds.HTTPRoute, + Group: v1.GroupName, + }, + Valid: true, + }, + pol2Key: { + Source: pol2, + TargetRef: PolicyTargetRef{ + Nsname: types.NamespacedName{Namespace: testNs, Name: "grpc"}, + Kind: kinds.GRPCRoute, + Group: v1.GroupName, + }, + Valid: true, + }, + pol3Key: { + Source: pol3, + TargetRef: PolicyTargetRef{ + Nsname: types.NamespacedName{Namespace: testNs, Name: "gw"}, + Kind: kinds.Gateway, + Group: v1.GroupName, + }, + Valid: true, + }, + pol4Key: { + Source: pol4, + TargetRef: PolicyTargetRef{ + Nsname: types.NamespacedName{Namespace: testNs, Name: "ignored"}, + Kind: kinds.Gateway, + Group: v1.GroupName, + }, + Valid: true, + }, + }, + }, + { + name: "invalid and valid policies", + validator: &policiesfakes.FakeValidator{ + ValidateStub: func(policy policies.Policy) error { + if policy.GetName() == "pol1" { + return errors.New("invalid error") + } + + return nil + }, + }, + policies: map[PolicyKey]policies.Policy{ + pol1Key: pol1, + pol2Key: pol2, + }, + expProcessedPolicies: map[PolicyKey]*Policy{ + pol1Key: { + Source: pol1, + TargetRef: PolicyTargetRef{ + Nsname: types.NamespacedName{Namespace: testNs, Name: "hr"}, + Kind: kinds.HTTPRoute, + Group: v1.GroupName, + }, + Conditions: []conditions.Condition{ + staticConds.NewPolicyInvalid("invalid error"), + }, + Valid: false, + }, + pol2Key: { + Source: pol2, + TargetRef: PolicyTargetRef{ + Nsname: types.NamespacedName{Namespace: testNs, Name: "grpc"}, + Kind: kinds.GRPCRoute, + Group: v1.GroupName, + }, + Valid: true, + }, + }, + }, + { + name: "conflicted policies", + validator: &policiesfakes.FakeValidator{ + ConflictsStub: func(_ policies.Policy, _ policies.Policy) bool { + return true + }, + }, + policies: map[PolicyKey]policies.Policy{ + pol1Key: pol1, + pol1ConflictKey: pol1Conflict, + }, + expProcessedPolicies: map[PolicyKey]*Policy{ + pol1Key: { + Source: pol1, + TargetRef: PolicyTargetRef{ + Nsname: types.NamespacedName{Namespace: testNs, Name: "hr"}, + Kind: kinds.HTTPRoute, + Group: v1.GroupName, + }, + Valid: true, + }, + pol1ConflictKey: { + Source: pol1Conflict, + TargetRef: PolicyTargetRef{ + Nsname: types.NamespacedName{Namespace: testNs, Name: "hr"}, + Kind: kinds.HTTPRoute, + Group: v1.GroupName, + }, + Conditions: []conditions.Condition{ + staticConds.NewPolicyConflicted("Conflicts with another MyPolicy"), + }, + Valid: false, + }, + }, + }, + } + + gateways := processedGateways{ + Winner: &v1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gw", + Namespace: testNs, + }, + }, + Ignored: map[types.NamespacedName]*v1.Gateway{ + {Namespace: testNs, Name: "ignored"}: { + ObjectMeta: metav1.ObjectMeta{ + Name: "gw", + Namespace: testNs, + }, + }, + }, + } + + routes := map[RouteKey]*L7Route{ + {RouteType: RouteTypeHTTP, NamespacedName: types.NamespacedName{Namespace: testNs, Name: "hr"}}: {}, + {RouteType: RouteTypeGRPC, NamespacedName: types.NamespacedName{Namespace: testNs, Name: "grpc"}}: {}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + processed := processPolicies(test.policies, test.validator, gateways, routes) + g.Expect(processed).To(BeEquivalentTo(test.expProcessedPolicies)) + }) + } +} + +func TestMarkConflictedPolicies(t *testing.T) { + hrRef := createTestRef(kinds.HTTPRoute, v1.GroupName, "hr") + hrTargetRef := PolicyTargetRef{ + Kind: hrRef.Kind, + Group: hrRef.Group, + Nsname: types.NamespacedName{Namespace: testNs, Name: string(hrRef.Name)}, + } + + grpcRef := createTestRef(kinds.GRPCRoute, v1.GroupName, "grpc") + grpcTargetRef := PolicyTargetRef{ + Kind: grpcRef.Kind, + Group: grpcRef.Group, + Nsname: types.NamespacedName{Namespace: testNs, Name: string(grpcRef.Name)}, + } + + orangeGVK := schema.GroupVersionKind{Group: "Fruits", Version: "Fresh", Kind: "OrangePolicy"} + appleGVK := schema.GroupVersionKind{Group: "Fruits", Version: "Fresh", Kind: "ApplePolicy"} + + tests := []struct { + name string + policies map[PolicyKey]*Policy + fakeValidator *policiesfakes.FakeValidator + conflictedNames []string + expConflictToBeCalled bool + }{ + { + name: "different policy types can not conflict", + policies: map[PolicyKey]*Policy{ + createTestPolicyKey(orangeGVK, "orange"): { + Source: createTestPolicy(orangeGVK, hrRef, "orange"), + TargetRef: hrTargetRef, + Valid: true, + }, + createTestPolicyKey(appleGVK, "apple"): { + Source: createTestPolicy(appleGVK, hrRef, "apple"), + TargetRef: hrTargetRef, + Valid: true, + }, + }, + fakeValidator: &policiesfakes.FakeValidator{}, + expConflictToBeCalled: false, + }, + { + name: "policies of the same type but with different target refs can not conflict", + policies: map[PolicyKey]*Policy{ + createTestPolicyKey(orangeGVK, "orange1"): { + Source: createTestPolicy(orangeGVK, hrRef, "orange1"), + TargetRef: hrTargetRef, + Valid: true, + }, + createTestPolicyKey(orangeGVK, "orange2"): { + Source: createTestPolicy(orangeGVK, grpcRef, "orange2"), + TargetRef: grpcTargetRef, + Valid: true, + }, + }, + fakeValidator: &policiesfakes.FakeValidator{}, + expConflictToBeCalled: false, + }, + { + name: "invalid policies can not conflict", + policies: map[PolicyKey]*Policy{ + createTestPolicyKey(orangeGVK, "valid"): { + Source: createTestPolicy(orangeGVK, hrRef, "valid"), + TargetRef: hrTargetRef, + Valid: true, + }, + createTestPolicyKey(orangeGVK, "invalid"): { + Source: createTestPolicy(orangeGVK, hrRef, "invalid"), + TargetRef: hrTargetRef, + Valid: false, + }, + }, + fakeValidator: &policiesfakes.FakeValidator{}, + expConflictToBeCalled: false, + }, + { + name: "when a policy conflicts with a policy that has greater precedence it's marked as invalid and a" + + " condition is added", + policies: map[PolicyKey]*Policy{ + createTestPolicyKey(orangeGVK, "orange1"): { + Source: createTestPolicy(orangeGVK, hrRef, "orange1"), + TargetRef: hrTargetRef, + Valid: true, + }, + createTestPolicyKey(orangeGVK, "orange2"): { + Source: createTestPolicy(orangeGVK, hrRef, "orange2"), + TargetRef: hrTargetRef, + Valid: true, + }, + createTestPolicyKey(orangeGVK, "orange3-conflicts-with-1"): { + Source: createTestPolicy(orangeGVK, hrRef, "orange3-conflicts-with-1"), + TargetRef: hrTargetRef, + Valid: true, + }, + createTestPolicyKey(orangeGVK, "orange4"): { + Source: createTestPolicy(orangeGVK, hrRef, "orange4"), + TargetRef: hrTargetRef, + Valid: true, + }, + createTestPolicyKey(orangeGVK, "orange5-conflicts-with-4"): { + Source: createTestPolicy(orangeGVK, hrRef, "orange5-conflicts-with-4"), + TargetRef: hrTargetRef, + Valid: true, + }, + }, + fakeValidator: &policiesfakes.FakeValidator{ + ConflictsStub: func(policy policies.Policy, policy2 policies.Policy) bool { + pol1Name := policy.GetName() + pol2Name := policy2.GetName() + + if pol1Name == "orange1" && pol2Name == "orange3-conflicts-with-1" { + return true + } + + if pol1Name == "orange4" && pol2Name == "orange5-conflicts-with-4" { + return true + } + + return false + }, + }, + conflictedNames: []string{"orange3-conflicts-with-1", "orange5-conflicts-with-4"}, + expConflictToBeCalled: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + markConflictedPolicies(test.policies, test.fakeValidator) + + if !test.expConflictToBeCalled { + g.Expect(test.fakeValidator.ConflictsCallCount()).To(BeZero()) + } else { + g.Expect(test.fakeValidator.ConflictsCallCount()).To(Not(BeZero())) + expConflictCond := staticConds.NewPolicyConflicted("Conflicts with another OrangePolicy") + + for key, policy := range test.policies { + if slices.Contains(test.conflictedNames, key.NsName.Name) { + g.Expect(policy.Valid).To(BeFalse()) + g.Expect(policy.Conditions).To(ConsistOf(expConflictCond)) + } else { + g.Expect(policy.Valid).To(BeTrue()) + g.Expect(policy.Conditions).To(BeEmpty()) + } + } + } + }) + } +} + +func createTestPolicyWithAncestors(numAncestors int) policies.Policy { + policy := &policiesfakes.FakePolicy{} + + ancestors := make([]v1alpha2.PolicyAncestorStatus, numAncestors) + + for i := 0; i < numAncestors; i++ { + ancestors[i] = v1alpha2.PolicyAncestorStatus{ControllerName: "some-other-controller"} + } + + policy.GetPolicyStatusReturns(v1alpha2.PolicyStatus{Ancestors: ancestors}) + return policy +} + +func createTestPolicyAndKey( + gvk schema.GroupVersionKind, + ref v1alpha2.LocalPolicyTargetReference, + name string, +) (policies.Policy, PolicyKey) { + pol := createTestPolicy(gvk, ref, name) + key := createTestPolicyKey(gvk, name) + + return pol, key +} + +func createTestPolicy( + gvk schema.GroupVersionKind, + ref v1alpha2.LocalPolicyTargetReference, + name string, +) policies.Policy { + return &policiesfakes.FakePolicy{ + GetNameStub: func() string { + return name + }, + GetNamespaceStub: func() string { + return testNs + }, + GetTargetRefStub: func() v1alpha2.LocalPolicyTargetReference { + return ref + }, + GetObjectKindStub: func() schema.ObjectKind { + return &policiesfakes.FakeObjectKind{ + GroupVersionKindStub: func() schema.GroupVersionKind { + return gvk + }, + } + }, + } +} + +func createTestPolicyKey(gvk schema.GroupVersionKind, name string) PolicyKey { + return PolicyKey{ + NsName: types.NamespacedName{Namespace: testNs, Name: name}, + GVK: gvk, + } +} + +func createTestRef(kind v1.Kind, group v1.Group, name string) v1alpha2.LocalPolicyTargetReference { + return v1alpha2.LocalPolicyTargetReference{ + Group: group, + Kind: kind, + Name: v1.ObjectName(name), + } +} diff --git a/internal/mode/static/state/graph/policy_ancestor.go b/internal/mode/static/state/graph/policy_ancestor.go new file mode 100644 index 0000000000..b373354135 --- /dev/null +++ b/internal/mode/static/state/graph/policy_ancestor.go @@ -0,0 +1,39 @@ +package graph + +import ( + "k8s.io/apimachinery/pkg/types" + v1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +const maxAncestors = 16 + +func ancestorsFull( + ancestors []v1alpha2.PolicyAncestorStatus, + ctlrName string, +) bool { + if len(ancestors) < maxAncestors { + return false + } + + for _, ancestor := range ancestors { + if string(ancestor.ControllerName) == ctlrName { + return false + } + } + + return true +} + +func createParentReference( + group v1.Group, + kind v1.Kind, + nsname types.NamespacedName, +) v1.ParentReference { + return v1.ParentReference{ + Group: &group, + Kind: &kind, + Namespace: (*v1.Namespace)(&nsname.Namespace), + Name: v1.ObjectName(nsname.Name), + } +} diff --git a/internal/mode/static/state/graph/policy_ancestor_test.go b/internal/mode/static/state/graph/policy_ancestor_test.go new file mode 100644 index 0000000000..114ce80ddb --- /dev/null +++ b/internal/mode/static/state/graph/policy_ancestor_test.go @@ -0,0 +1,54 @@ +package graph + +import ( + "testing" + + . "github.com/onsi/gomega" + v1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +func TestAncestorsFull(t *testing.T) { + createCurStatus := func(numAncestors int, ctlrName string) []v1alpha2.PolicyAncestorStatus { + statuses := make([]v1alpha2.PolicyAncestorStatus, 0, numAncestors) + + for i := 0; i < numAncestors; i++ { + statuses = append(statuses, v1alpha2.PolicyAncestorStatus{ + ControllerName: v1.GatewayController(ctlrName), + }) + } + + return statuses + } + + tests := []struct { + name string + curStatus []v1alpha2.PolicyAncestorStatus + expFull bool + }{ + { + name: "not full", + curStatus: createCurStatus(15, "controller"), + expFull: false, + }, + { + name: "full; ancestor does not exist in current status", + curStatus: createCurStatus(16, "controller"), + expFull: true, + }, + { + name: "full, but ancestor does exist in current status", + curStatus: createCurStatus(16, "nginx-gateway"), + expFull: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + full := ancestorsFull(test.curStatus, "nginx-gateway") + g.Expect(full).To(Equal(test.expFull)) + }) + } +} diff --git a/internal/mode/static/state/graph/reference_grant.go b/internal/mode/static/state/graph/reference_grant.go index c0db702732..b833825d73 100644 --- a/internal/mode/static/state/graph/reference_grant.go +++ b/internal/mode/static/state/graph/reference_grant.go @@ -4,6 +4,8 @@ import ( "k8s.io/apimachinery/pkg/types" v1 "sigs.k8s.io/gateway-api/apis/v1" v1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" ) // referenceGrantResolver resolves references from one resource to another. @@ -58,7 +60,7 @@ func toService(nsname types.NamespacedName) toResource { func fromGateway(namespace string) fromResource { return fromResource{ group: v1.GroupName, - kind: "Gateway", + kind: kinds.Gateway, namespace: namespace, } } @@ -66,7 +68,7 @@ func fromGateway(namespace string) fromResource { func fromHTTPRoute(namespace string) fromResource { return fromResource{ group: v1.GroupName, - kind: "HTTPRoute", + kind: kinds.HTTPRoute, namespace: namespace, } } diff --git a/internal/mode/static/state/graph/reference_grant_test.go b/internal/mode/static/state/graph/reference_grant_test.go index aa84446b92..3b61e6f1c5 100644 --- a/internal/mode/static/state/graph/reference_grant_test.go +++ b/internal/mode/static/state/graph/reference_grant_test.go @@ -8,6 +8,7 @@ import ( "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" ) func TestReferenceGrantResolver(t *testing.T) { @@ -20,7 +21,7 @@ func TestReferenceGrantResolver(t *testing.T) { From: []v1beta1.ReferenceGrantFrom{ { Group: v1beta1.GroupName, - Kind: "Gateway", + Kind: kinds.Gateway, Namespace: v1beta1.Namespace(gwNs), }, }, @@ -45,7 +46,7 @@ func TestReferenceGrantResolver(t *testing.T) { From: []v1beta1.ReferenceGrantFrom{ { Group: v1beta1.GroupName, - Kind: "Gateway", + Kind: kinds.Gateway, Namespace: v1beta1.Namespace(gwNs), }, }, @@ -68,17 +69,17 @@ func TestReferenceGrantResolver(t *testing.T) { From: []v1beta1.ReferenceGrantFrom{ { Group: v1beta1.GroupName, - Kind: "Gateway", + Kind: kinds.Gateway, Namespace: "wrong-ns1", }, { Group: v1beta1.GroupName, - Kind: "Gateway", + Kind: kinds.Gateway, Namespace: "wrong-ns2", }, { Group: v1beta1.GroupName, - Kind: "Gateway", + Kind: kinds.Gateway, Namespace: v1beta1.Namespace(gwNs), }, }, @@ -87,7 +88,7 @@ func TestReferenceGrantResolver(t *testing.T) { } normalTo := toResource{kind: "Secret", name: secretNsName.Name, namespace: secretNsName.Namespace} - normalFrom := fromResource{group: v1beta1.GroupName, kind: "Gateway", namespace: gwNs} + normalFrom := fromResource{group: v1beta1.GroupName, kind: kinds.Gateway, namespace: gwNs} tests := []struct { to toResource @@ -127,13 +128,13 @@ func TestReferenceGrantResolver(t *testing.T) { { msg: "wrong 'from' group", to: normalTo, - from: fromResource{group: "wrong.group", kind: "Gateway", namespace: gwNs}, + from: fromResource{group: "wrong.group", kind: kinds.Gateway, namespace: gwNs}, allowed: false, }, { msg: "wrong 'from' namespace", to: normalTo, - from: fromResource{group: v1beta1.GroupName, kind: "Gateway", namespace: "wrong-ns"}, + from: fromResource{group: v1beta1.GroupName, kind: kinds.Gateway, namespace: "wrong-ns"}, allowed: false, }, { @@ -198,7 +199,7 @@ func TestFromGateway(t *testing.T) { exp := fromResource{ group: v1beta1.GroupName, - kind: "Gateway", + kind: kinds.Gateway, namespace: "ns", } @@ -211,7 +212,7 @@ func TestFromHTTPRoute(t *testing.T) { exp := fromResource{ group: v1beta1.GroupName, - kind: "HTTPRoute", + kind: kinds.HTTPRoute, namespace: "ns", } diff --git a/internal/mode/static/state/graph/route_common.go b/internal/mode/static/state/graph/route_common.go index 7fdbaee477..c68152fdea 100644 --- a/internal/mode/static/state/graph/route_common.go +++ b/internal/mode/static/state/graph/route_common.go @@ -13,6 +13,7 @@ import ( ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation" ) @@ -57,8 +58,10 @@ const ( // RouteKey is the unique identifier for a L7Route type RouteKey struct { + // NamespacedName is the NamespacedName of the Route. NamespacedName types.NamespacedName - RouteType RouteType + // RouteType is the type of the Route. + RouteType RouteType } // L7Route is the generic type for the layer 7 routes, HTTPRoute and GRPCRoute @@ -73,6 +76,8 @@ type L7Route struct { ParentRefs []ParentRef // Conditions define the conditions to be reported in the status of the Route. Conditions []conditions.Condition + // Policies holds the policies that are attached to the Route. + Policies []*Policy // Valid indicates if the Route is valid. Valid bool // Attachable indicates if the Route is attachable to any Listener. @@ -218,7 +223,7 @@ func findGatewayForParentRef( routeNamespace string, gatewayNsNames []types.NamespacedName, ) (gwNsName types.NamespacedName, found bool) { - if ref.Kind != nil && *ref.Kind != "Gateway" { + if ref.Kind != nil && *ref.Kind != kinds.Gateway { return types.NamespacedName{}, false } if ref.Group != nil && *ref.Group != v1.GroupName { @@ -698,7 +703,8 @@ func validateResponseHeaders( for _, h := range headers { valErr := field.Invalid(path, h, "header name is not allowed") name := strings.ToLower(string(h.Name)) - if _, exists := disallowedResponseHeaderSet[name]; exists || strings.HasPrefix(name, strings.ToLower(invalidPrefix)) { + if _, exists := disallowedResponseHeaderSet[name]; exists || + strings.HasPrefix(name, strings.ToLower(invalidPrefix)) { allErrs = append(allErrs, valErr) } } @@ -742,3 +748,17 @@ func validateRequestHeaderStringCaseInsensitiveUnique(headers []string, path *fi return allErrs } + +func routeKeyForKind(kind v1.Kind, nsname types.NamespacedName) RouteKey { + key := RouteKey{NamespacedName: nsname} + switch kind { + case kinds.HTTPRoute: + key.RouteType = RouteTypeHTTP + case kinds.GRPCRoute: + key.RouteType = RouteTypeGRPC + default: + panic(fmt.Sprintf("unsupported route kind: %s", kind)) + } + + return key +} diff --git a/internal/mode/static/state/graph/route_common_test.go b/internal/mode/static/state/graph/route_common_test.go index 85d5802b2e..3eb96f6f69 100644 --- a/internal/mode/static/state/graph/route_common_test.go +++ b/internal/mode/static/state/graph/route_common_test.go @@ -15,6 +15,7 @@ import ( "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation/validationfakes" ) @@ -156,7 +157,7 @@ func TestFindGatewayForParentRef(t *testing.T) { { ref: gatewayv1.ParentReference{ Group: helpers.GetPointer[gatewayv1.Group](gatewayv1.GroupName), - Kind: helpers.GetPointer[gatewayv1.Kind]("Gateway"), + Kind: helpers.GetPointer[gatewayv1.Kind](kinds.Gateway), Namespace: helpers.GetPointer(gatewayv1.Namespace(gwNsName1.Namespace)), Name: gatewayv1.ObjectName(gwNsName1.Name), }, @@ -1565,3 +1566,21 @@ func TestValidateFilterResponseHeaderModifier(t *testing.T) { }) } } + +func TestRouteKeyForKind(t *testing.T) { + nsname := types.NamespacedName{Namespace: testNs, Name: "route"} + + g := NewWithT(t) + + key := routeKeyForKind(kinds.HTTPRoute, nsname) + g.Expect(key).To(Equal(RouteKey{RouteType: RouteTypeHTTP, NamespacedName: nsname})) + + key = routeKeyForKind(kinds.GRPCRoute, nsname) + g.Expect(key).To(Equal(RouteKey{RouteType: RouteTypeGRPC, NamespacedName: nsname})) + + rk := func() { + _ = routeKeyForKind(kinds.Gateway, nsname) + } + + g.Expect(rk).To(Panic()) +} diff --git a/internal/mode/static/state/store.go b/internal/mode/static/state/store.go index 02a2585580..163d051972 100644 --- a/internal/mode/static/state/store.go +++ b/internal/mode/static/state/store.go @@ -7,6 +7,10 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/graph" ) // Updater updates the cluster state. @@ -17,9 +21,59 @@ type Updater interface { // objectStore is a store of client.Object type objectStore interface { - get(nsname types.NamespacedName) client.Object + get(objType client.Object, nsname types.NamespacedName) client.Object upsert(obj client.Object) - delete(nsname types.NamespacedName) + delete(objType client.Object, nsname types.NamespacedName) +} + +// ngfPolicyObjectStore is a store of policies.Policy. +// A single store should be used to store all types of policies.Policy. +type ngfPolicyObjectStore struct { + policies map[graph.PolicyKey]policies.Policy + extractGVKFunc kinds.MustExtractGVK +} + +// newNGFPolicyObjectStore returns a new ngfPolicyObjectStore. +func newNGFPolicyObjectStore( + policies map[graph.PolicyKey]policies.Policy, + gvkFunc kinds.MustExtractGVK, +) *ngfPolicyObjectStore { + return &ngfPolicyObjectStore{ + policies: policies, + extractGVKFunc: gvkFunc, + } +} + +func (p *ngfPolicyObjectStore) get(objType client.Object, nsname types.NamespacedName) client.Object { + key := graph.PolicyKey{ + NsName: nsname, + GVK: p.extractGVKFunc(objType), + } + + return p.policies[key] +} + +func (p *ngfPolicyObjectStore) upsert(obj client.Object) { + key := graph.PolicyKey{ + NsName: client.ObjectKeyFromObject(obj), + GVK: p.extractGVKFunc(obj), + } + + pol, ok := obj.(policies.Policy) + if !ok { + panic(fmt.Sprintf("expected NGF Policy, got %T", obj)) + } + + p.policies[key] = pol +} + +func (p *ngfPolicyObjectStore) delete(objType client.Object, nsname types.NamespacedName) { + key := graph.PolicyKey{ + NsName: nsname, + GVK: p.extractGVKFunc(objType), + } + + delete(p.policies, key) } // objectStoreMapAdapter wraps maps of types.NamespacedName to Kubernetes resources @@ -34,7 +88,7 @@ func newObjectStoreMapAdapter[T client.Object](objects map[types.NamespacedName] } } -func (m *objectStoreMapAdapter[T]) get(nsname types.NamespacedName) client.Object { +func (m *objectStoreMapAdapter[T]) get(_ client.Object, nsname types.NamespacedName) client.Object { obj, exist := m.objects[nsname] if !exist { return nil @@ -51,7 +105,7 @@ func (m *objectStoreMapAdapter[T]) upsert(obj client.Object) { m.objects[client.ObjectKeyFromObject(obj)] = t } -func (m *objectStoreMapAdapter[T]) delete(nsname types.NamespacedName) { +func (m *objectStoreMapAdapter[T]) delete(_ client.Object, nsname types.NamespacedName) { delete(m.objects, nsname) } @@ -69,13 +123,13 @@ func (list gvkList) contains(gvk schema.GroupVersionKind) bool { type multiObjectStore struct { stores map[schema.GroupVersionKind]objectStore - extractGVK extractGVKFunc + extractGVK kinds.MustExtractGVK persistedGVKs gvkList } func newMultiObjectStore( stores map[schema.GroupVersionKind]objectStore, - extractGVK extractGVKFunc, + extractGVK kinds.MustExtractGVK, persistedGVKs gvkList, ) *multiObjectStore { return &multiObjectStore{ @@ -97,7 +151,7 @@ func (m *multiObjectStore) mustFindStoreForObj(obj client.Object) objectStore { } func (m *multiObjectStore) get(objType client.Object, nsname types.NamespacedName) client.Object { - return m.mustFindStoreForObj(objType).get(nsname) + return m.mustFindStoreForObj(objType).get(objType, nsname) } func (m *multiObjectStore) upsert(obj client.Object) { @@ -105,7 +159,7 @@ func (m *multiObjectStore) upsert(obj client.Object) { } func (m *multiObjectStore) delete(objType client.Object, nsname types.NamespacedName) { - m.mustFindStoreForObj(objType).delete(nsname) + m.mustFindStoreForObj(objType).delete(objType, nsname) } func (m *multiObjectStore) persists(objTypeGVK schema.GroupVersionKind) bool { @@ -130,14 +184,14 @@ type changeTrackingUpdater struct { store *multiObjectStore stateChangedPredicates map[schema.GroupVersionKind]stateChangedPredicate - extractGVK extractGVKFunc + extractGVK kinds.MustExtractGVK supportedGVKs gvkList changeType ChangeType } func newChangeTrackingUpdater( - extractGVK extractGVKFunc, + extractGVK kinds.MustExtractGVK, objectTypeCfgs []changeTrackingUpdaterObjectTypeCfg, ) *changeTrackingUpdater { var ( diff --git a/internal/mode/static/state/validation/validationfakes/fake_generic_validator.go b/internal/mode/static/state/validation/validationfakes/fake_generic_validator.go index 294b7dc526..faa9ed1bc8 100644 --- a/internal/mode/static/state/validation/validationfakes/fake_generic_validator.go +++ b/internal/mode/static/state/validation/validationfakes/fake_generic_validator.go @@ -41,6 +41,17 @@ type FakeGenericValidator struct { validateNginxDurationReturnsOnCall map[int]struct { result1 error } + ValidateNginxSizeStub func(string) error + validateNginxSizeMutex sync.RWMutex + validateNginxSizeArgsForCall []struct { + arg1 string + } + validateNginxSizeReturns struct { + result1 error + } + validateNginxSizeReturnsOnCall map[int]struct { + result1 error + } ValidateServiceNameStub func(string) error validateServiceNameMutex sync.RWMutex validateServiceNameArgsForCall []struct { @@ -239,6 +250,67 @@ func (fake *FakeGenericValidator) ValidateNginxDurationReturnsOnCall(i int, resu }{result1} } +func (fake *FakeGenericValidator) ValidateNginxSize(arg1 string) error { + fake.validateNginxSizeMutex.Lock() + ret, specificReturn := fake.validateNginxSizeReturnsOnCall[len(fake.validateNginxSizeArgsForCall)] + fake.validateNginxSizeArgsForCall = append(fake.validateNginxSizeArgsForCall, struct { + arg1 string + }{arg1}) + stub := fake.ValidateNginxSizeStub + fakeReturns := fake.validateNginxSizeReturns + fake.recordInvocation("ValidateNginxSize", []interface{}{arg1}) + fake.validateNginxSizeMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeGenericValidator) ValidateNginxSizeCallCount() int { + fake.validateNginxSizeMutex.RLock() + defer fake.validateNginxSizeMutex.RUnlock() + return len(fake.validateNginxSizeArgsForCall) +} + +func (fake *FakeGenericValidator) ValidateNginxSizeCalls(stub func(string) error) { + fake.validateNginxSizeMutex.Lock() + defer fake.validateNginxSizeMutex.Unlock() + fake.ValidateNginxSizeStub = stub +} + +func (fake *FakeGenericValidator) ValidateNginxSizeArgsForCall(i int) string { + fake.validateNginxSizeMutex.RLock() + defer fake.validateNginxSizeMutex.RUnlock() + argsForCall := fake.validateNginxSizeArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeGenericValidator) ValidateNginxSizeReturns(result1 error) { + fake.validateNginxSizeMutex.Lock() + defer fake.validateNginxSizeMutex.Unlock() + fake.ValidateNginxSizeStub = nil + fake.validateNginxSizeReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeGenericValidator) ValidateNginxSizeReturnsOnCall(i int, result1 error) { + fake.validateNginxSizeMutex.Lock() + defer fake.validateNginxSizeMutex.Unlock() + fake.ValidateNginxSizeStub = nil + if fake.validateNginxSizeReturnsOnCall == nil { + fake.validateNginxSizeReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.validateNginxSizeReturnsOnCall[i] = struct { + result1 error + }{result1} +} + func (fake *FakeGenericValidator) ValidateServiceName(arg1 string) error { fake.validateServiceNameMutex.Lock() ret, specificReturn := fake.validateServiceNameReturnsOnCall[len(fake.validateServiceNameArgsForCall)] @@ -309,6 +381,8 @@ func (fake *FakeGenericValidator) Invocations() map[string][][]interface{} { defer fake.validateEscapedStringNoVarExpansionMutex.RUnlock() fake.validateNginxDurationMutex.RLock() defer fake.validateNginxDurationMutex.RUnlock() + fake.validateNginxSizeMutex.RLock() + defer fake.validateNginxSizeMutex.RUnlock() fake.validateServiceNameMutex.RLock() defer fake.validateServiceNameMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} diff --git a/internal/mode/static/state/validation/validationfakes/fake_policy_validator.go b/internal/mode/static/state/validation/validationfakes/fake_policy_validator.go new file mode 100644 index 0000000000..9d08181f74 --- /dev/null +++ b/internal/mode/static/state/validation/validationfakes/fake_policy_validator.go @@ -0,0 +1,188 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package validationfakes + +import ( + "sync" + + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation" +) + +type FakePolicyValidator struct { + ConflictsStub func(policies.Policy, policies.Policy) bool + conflictsMutex sync.RWMutex + conflictsArgsForCall []struct { + arg1 policies.Policy + arg2 policies.Policy + } + conflictsReturns struct { + result1 bool + } + conflictsReturnsOnCall map[int]struct { + result1 bool + } + ValidateStub func(policies.Policy) error + validateMutex sync.RWMutex + validateArgsForCall []struct { + arg1 policies.Policy + } + validateReturns struct { + result1 error + } + validateReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakePolicyValidator) Conflicts(arg1 policies.Policy, arg2 policies.Policy) bool { + fake.conflictsMutex.Lock() + ret, specificReturn := fake.conflictsReturnsOnCall[len(fake.conflictsArgsForCall)] + fake.conflictsArgsForCall = append(fake.conflictsArgsForCall, struct { + arg1 policies.Policy + arg2 policies.Policy + }{arg1, arg2}) + stub := fake.ConflictsStub + fakeReturns := fake.conflictsReturns + fake.recordInvocation("Conflicts", []interface{}{arg1, arg2}) + fake.conflictsMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakePolicyValidator) ConflictsCallCount() int { + fake.conflictsMutex.RLock() + defer fake.conflictsMutex.RUnlock() + return len(fake.conflictsArgsForCall) +} + +func (fake *FakePolicyValidator) ConflictsCalls(stub func(policies.Policy, policies.Policy) bool) { + fake.conflictsMutex.Lock() + defer fake.conflictsMutex.Unlock() + fake.ConflictsStub = stub +} + +func (fake *FakePolicyValidator) ConflictsArgsForCall(i int) (policies.Policy, policies.Policy) { + fake.conflictsMutex.RLock() + defer fake.conflictsMutex.RUnlock() + argsForCall := fake.conflictsArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakePolicyValidator) ConflictsReturns(result1 bool) { + fake.conflictsMutex.Lock() + defer fake.conflictsMutex.Unlock() + fake.ConflictsStub = nil + fake.conflictsReturns = struct { + result1 bool + }{result1} +} + +func (fake *FakePolicyValidator) ConflictsReturnsOnCall(i int, result1 bool) { + fake.conflictsMutex.Lock() + defer fake.conflictsMutex.Unlock() + fake.ConflictsStub = nil + if fake.conflictsReturnsOnCall == nil { + fake.conflictsReturnsOnCall = make(map[int]struct { + result1 bool + }) + } + fake.conflictsReturnsOnCall[i] = struct { + result1 bool + }{result1} +} + +func (fake *FakePolicyValidator) Validate(arg1 policies.Policy) error { + fake.validateMutex.Lock() + ret, specificReturn := fake.validateReturnsOnCall[len(fake.validateArgsForCall)] + fake.validateArgsForCall = append(fake.validateArgsForCall, struct { + arg1 policies.Policy + }{arg1}) + stub := fake.ValidateStub + fakeReturns := fake.validateReturns + fake.recordInvocation("Validate", []interface{}{arg1}) + fake.validateMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakePolicyValidator) ValidateCallCount() int { + fake.validateMutex.RLock() + defer fake.validateMutex.RUnlock() + return len(fake.validateArgsForCall) +} + +func (fake *FakePolicyValidator) ValidateCalls(stub func(policies.Policy) error) { + fake.validateMutex.Lock() + defer fake.validateMutex.Unlock() + fake.ValidateStub = stub +} + +func (fake *FakePolicyValidator) ValidateArgsForCall(i int) policies.Policy { + fake.validateMutex.RLock() + defer fake.validateMutex.RUnlock() + argsForCall := fake.validateArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakePolicyValidator) ValidateReturns(result1 error) { + fake.validateMutex.Lock() + defer fake.validateMutex.Unlock() + fake.ValidateStub = nil + fake.validateReturns = struct { + result1 error + }{result1} +} + +func (fake *FakePolicyValidator) ValidateReturnsOnCall(i int, result1 error) { + fake.validateMutex.Lock() + defer fake.validateMutex.Unlock() + fake.ValidateStub = nil + if fake.validateReturnsOnCall == nil { + fake.validateReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.validateReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakePolicyValidator) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.conflictsMutex.RLock() + defer fake.conflictsMutex.RUnlock() + fake.validateMutex.RLock() + defer fake.validateMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakePolicyValidator) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ validation.PolicyValidator = new(FakePolicyValidator) diff --git a/internal/mode/static/state/validation/validator.go b/internal/mode/static/state/validation/validator.go index 6f6dd12b4f..15566901b4 100644 --- a/internal/mode/static/state/validation/validator.go +++ b/internal/mode/static/state/validation/validator.go @@ -2,6 +2,10 @@ package validation //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate +import ( + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies" +) + // Validators include validators for API resources from the perspective of a data-plane. // It is used for fields that propagate into the data plane configuration. For example, the path in a routing rule. // However, not all such fields are validated: NGF will not validate a field using Validators if it is confident that @@ -9,6 +13,7 @@ package validation type Validators struct { HTTPFieldsValidator HTTPFieldsValidator GenericValidator GenericValidator + PolicyValidator PolicyValidator } // HTTPFieldsValidator validates the HTTP-related fields of Gateway API resources from the perspective of @@ -39,5 +44,16 @@ type GenericValidator interface { ValidateEscapedStringNoVarExpansion(value string) error ValidateServiceName(name string) error ValidateNginxDuration(duration string) error + ValidateNginxSize(size string) error ValidateEndpoint(endpoint string) error } + +// PolicyValidator validates an NGF Policy. +// +//counterfeiter:generate . PolicyValidator +type PolicyValidator interface { + // Validate validates an NGF Policy. + Validate(policy policies.Policy) error + // Conflicts returns true if the two Policies conflict. + Conflicts(a, b policies.Policy) bool +} diff --git a/internal/mode/static/status/prepare_requests.go b/internal/mode/static/status/prepare_requests.go index 459be5f8ec..8b97994379 100644 --- a/internal/mode/static/status/prepare_requests.go +++ b/internal/mode/static/status/prepare_requests.go @@ -13,6 +13,7 @@ import ( ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" frameworkStatus "github.com/nginxinc/nginx-gateway-fabric/internal/framework/status" staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/graph" @@ -297,6 +298,51 @@ func prepareGatewayRequest( } } +func PrepareNGFPolicyRequests( + policies map[graph.PolicyKey]*graph.Policy, + transitionTime metav1.Time, + gatewayCtlrName string, +) []frameworkStatus.UpdateRequest { + reqs := make([]frameworkStatus.UpdateRequest, 0, len(policies)) + + for key, pol := range policies { + ancestorStatuses := make([]v1alpha2.PolicyAncestorStatus, 0, 1) + ancestor := pol.Ancestor + + if ancestor == nil { + continue + } + + allConds := make([]conditions.Condition, 0, len(pol.Conditions)+len(ancestor.Conditions)+1) + + // The order of conditions matters here. + // We add the default condition first, followed by the ancestor conditions, and finally the policy conditions. + // DeduplicateConditions will ensure the last condition wins. + allConds = append(allConds, staticConds.NewPolicyAccepted()) + allConds = append(allConds, ancestor.Conditions...) + allConds = append(allConds, pol.Conditions...) + + conds := conditions.DeduplicateConditions(allConds) + apiConds := conditions.ConvertConditions(conds, pol.Source.GetGeneration(), transitionTime) + + ancestorStatuses = append(ancestorStatuses, v1alpha2.PolicyAncestorStatus{ + AncestorRef: ancestor.Ancestor, + ControllerName: v1alpha2.GatewayController(gatewayCtlrName), + Conditions: apiConds, + }) + + status := v1alpha2.PolicyStatus{Ancestors: ancestorStatuses} + + reqs = append(reqs, frameworkStatus.UpdateRequest{ + NsName: key.NsName, + ResourceType: pol.Source, + Setter: newNGFPolicyStatusSetter(status, gatewayCtlrName), + }) + } + + return reqs +} + // PrepareBackendTLSPolicyRequests prepares status UpdateRequests for the given BackendTLSPolicies. func PrepareBackendTLSPolicyRequests( policies map[types.NamespacedName]*graph.BackendTLSPolicy, @@ -319,6 +365,8 @@ func PrepareBackendTLSPolicyRequests( AncestorRef: v1.ParentReference{ Namespace: (*v1.Namespace)(&pol.Gateway.Namespace), Name: v1alpha2.ObjectName(pol.Gateway.Name), + Group: helpers.GetPointer[v1.Group](v1.GroupName), + Kind: helpers.GetPointer[v1.Kind](kinds.Gateway), }, ControllerName: v1alpha2.GatewayController(gatewayCtlrName), Conditions: apiConds, @@ -355,7 +403,9 @@ func PrepareNginxGatewayStatus( var conds []conditions.Condition if cpUpdateRes.Error != nil { msg := "Failed to update control plane configuration" - conds = []conditions.Condition{staticConds.NewNginxGatewayInvalid(fmt.Sprintf("%s: %v", msg, cpUpdateRes.Error))} + conds = []conditions.Condition{ + staticConds.NewNginxGatewayInvalid(fmt.Sprintf("%s: %v", msg, cpUpdateRes.Error)), + } } else { conds = []conditions.Condition{staticConds.NewNginxGatewayValid()} } diff --git a/internal/mode/static/status/prepare_requests_test.go b/internal/mode/static/status/prepare_requests_test.go index 594e317df3..546b534e08 100644 --- a/internal/mode/static/status/prepare_requests_test.go +++ b/internal/mode/static/status/prepare_requests_test.go @@ -8,6 +8,7 @@ import ( . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -20,6 +21,7 @@ import ( ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" statusFramework "github.com/nginxinc/nginx-gateway-fabric/internal/framework/status" staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/graph" @@ -1058,7 +1060,13 @@ func TestBuildGatewayStatuses(t *testing.T) { updater := statusFramework.NewUpdater(k8sClient, zap.New()) - reqs := PrepareGatewayRequests(test.gateway, test.ignoredGateways, transitionTime, addr, test.nginxReloadRes) + reqs := PrepareGatewayRequests( + test.gateway, + test.ignoredGateways, + transitionTime, + addr, + test.nginxReloadRes, + ) g.Expect(reqs).To(HaveLen(expectedTotalReqs)) @@ -1105,8 +1113,8 @@ func TestBuildBackendTLSPolicyStatuses(t *testing.T) { } } - attachedConds := []conditions.Condition{staticConds.NewBackendTLSPolicyAccepted()} - invalidConds := []conditions.Condition{staticConds.NewBackendTLSPolicyInvalid("invalid backendTLSPolicy")} + attachedConds := []conditions.Condition{staticConds.NewPolicyAccepted()} + invalidConds := []conditions.Condition{staticConds.NewPolicyInvalid("invalid backendTLSPolicy")} validPolicyCfg := policyCfg{ Name: "valid-bt", @@ -1156,6 +1164,8 @@ func TestBuildBackendTLSPolicyStatuses(t *testing.T) { AncestorRef: v1.ParentReference{ Namespace: helpers.GetPointer[v1.Namespace]("test"), Name: "gateway", + Group: helpers.GetPointer[v1.Group](v1.GroupName), + Kind: helpers.GetPointer[v1.Kind](kinds.Gateway), }, ControllerName: gatewayCtlrName, Conditions: []metav1.Condition{ @@ -1165,7 +1175,7 @@ func TestBuildBackendTLSPolicyStatuses(t *testing.T) { ObservedGeneration: 1, LastTransitionTime: transitionTime, Reason: string(v1alpha2.PolicyReasonAccepted), - Message: "BackendTLSPolicy is accepted by the Gateway", + Message: "Policy is accepted", }, }, }, @@ -1186,6 +1196,8 @@ func TestBuildBackendTLSPolicyStatuses(t *testing.T) { AncestorRef: v1.ParentReference{ Namespace: helpers.GetPointer[v1.Namespace]("test"), Name: "gateway", + Group: helpers.GetPointer[v1.Group](v1.GroupName), + Kind: helpers.GetPointer[v1.Kind](kinds.Gateway), }, ControllerName: gatewayCtlrName, Conditions: []metav1.Condition{ @@ -1230,6 +1242,8 @@ func TestBuildBackendTLSPolicyStatuses(t *testing.T) { AncestorRef: v1.ParentReference{ Namespace: helpers.GetPointer[v1.Namespace]("test"), Name: "gateway", + Group: helpers.GetPointer[v1.Group](v1.GroupName), + Kind: helpers.GetPointer[v1.Kind](kinds.Gateway), }, ControllerName: gatewayCtlrName, Conditions: []metav1.Condition{ @@ -1239,7 +1253,7 @@ func TestBuildBackendTLSPolicyStatuses(t *testing.T) { ObservedGeneration: 1, LastTransitionTime: transitionTime, Reason: string(v1alpha2.PolicyReasonAccepted), - Message: "BackendTLSPolicy is accepted by the Gateway", + Message: "Policy is accepted", }, }, }, @@ -1371,3 +1385,244 @@ func TestBuildNginxGatewayStatus(t *testing.T) { }) } } + +func TestBuildNGFPolicyStatuses(t *testing.T) { + const gatewayCtlrName = "controller" + + transitionTime := helpers.PrepareTimeForFakeClient(metav1.Now()) + + type policyCfg struct { + Ancestor *graph.PolicyAncestor + Name string + Conditions []conditions.Condition + } + + // We have to use a real policy here because the test makes the status update using the k8sClient. + // One policy type should suffice here, unless a new policy introduces branching. + getPolicy := func(cfg policyCfg) *graph.Policy { + return &graph.Policy{ + Source: &ngfAPI.ClientSettingsPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: cfg.Name, + Namespace: "test", + Generation: 2, + }, + }, + Conditions: cfg.Conditions, + Ancestor: cfg.Ancestor, + } + } + + invalidConds := []conditions.Condition{staticConds.NewPolicyInvalid("invalid")} + targetRefNotFoundConds := []conditions.Condition{staticConds.NewPolicyTargetNotFound("target not found")} + + validPolicyKey := graph.PolicyKey{ + NsName: types.NamespacedName{Namespace: "test", Name: "valid-pol"}, + GVK: schema.GroupVersionKind{Group: ngfAPI.GroupName, Kind: kinds.ClientSettingsPolicy}, + } + validPolicyCfg := policyCfg{ + Name: validPolicyKey.NsName.Name, + Ancestor: &graph.PolicyAncestor{ + Ancestor: v1.ParentReference{ + Name: "ancestor", + }, + }, + } + + invalidPolicyKey := graph.PolicyKey{ + NsName: types.NamespacedName{Namespace: "test", Name: "invalid-pol"}, + GVK: schema.GroupVersionKind{Group: ngfAPI.GroupName, Kind: kinds.ClientSettingsPolicy}, + } + invalidPolicyCfg := policyCfg{ + Name: invalidPolicyKey.NsName.Name, + Conditions: invalidConds, + Ancestor: &graph.PolicyAncestor{ + Ancestor: v1.ParentReference{ + Name: "ancestor", + }, + }, + } + + targetRefNotFoundPolicyKey := graph.PolicyKey{ + NsName: types.NamespacedName{Namespace: "test", Name: "target-not-found-pol"}, + GVK: schema.GroupVersionKind{Group: ngfAPI.GroupName, Kind: kinds.ClientSettingsPolicy}, + } + targetRefNotFoundPolicyCfg := policyCfg{ + Name: targetRefNotFoundPolicyKey.NsName.Name, + Ancestor: &graph.PolicyAncestor{ + Ancestor: v1.ParentReference{ + Name: "ancestor", + }, + Conditions: targetRefNotFoundConds, + }, + } + + multiInvalidCondsPolicyKey := graph.PolicyKey{ + NsName: types.NamespacedName{Namespace: "test", Name: "multi-invalid-conds-pol"}, + GVK: schema.GroupVersionKind{Group: ngfAPI.GroupName, Kind: kinds.ClientSettingsPolicy}, + } + multiInvalidCondsPolicyCfg := policyCfg{ + Name: multiInvalidCondsPolicyKey.NsName.Name, + Conditions: invalidConds, + Ancestor: &graph.PolicyAncestor{ + Ancestor: v1.ParentReference{ + Name: "ancestor", + }, + Conditions: targetRefNotFoundConds, + }, + } + + nilAncestorPolicyKey := graph.PolicyKey{ + NsName: types.NamespacedName{Namespace: "test", Name: "nil-ancestor-pol"}, + GVK: schema.GroupVersionKind{Group: ngfAPI.GroupName, Kind: kinds.ClientSettingsPolicy}, + } + nilAncestorPolicyCfg := policyCfg{ + Name: nilAncestorPolicyKey.NsName.Name, + Ancestor: nil, + } + + tests := []struct { + policies map[graph.PolicyKey]*graph.Policy + expected map[types.NamespacedName]v1alpha2.PolicyStatus + name string + }{ + { + name: "nil policies", + expected: map[types.NamespacedName]v1alpha2.PolicyStatus{}, + }, + { + name: "mix valid and invalid policies", + policies: map[graph.PolicyKey]*graph.Policy{ + invalidPolicyKey: getPolicy(invalidPolicyCfg), + targetRefNotFoundPolicyKey: getPolicy(targetRefNotFoundPolicyCfg), + validPolicyKey: getPolicy(validPolicyCfg), + }, + expected: map[types.NamespacedName]v1alpha2.PolicyStatus{ + invalidPolicyKey.NsName: { + Ancestors: []v1alpha2.PolicyAncestorStatus{ + { + AncestorRef: v1.ParentReference{ + Name: "ancestor", + }, + ControllerName: gatewayCtlrName, + Conditions: []metav1.Condition{ + { + Type: string(v1alpha2.PolicyConditionAccepted), + Status: metav1.ConditionFalse, + ObservedGeneration: 2, + LastTransitionTime: transitionTime, + Reason: string(v1alpha2.PolicyReasonInvalid), + Message: "invalid", + }, + }, + }, + }, + }, + targetRefNotFoundPolicyKey.NsName: { + Ancestors: []v1alpha2.PolicyAncestorStatus{ + { + AncestorRef: v1.ParentReference{ + Name: "ancestor", + }, + ControllerName: gatewayCtlrName, + Conditions: []metav1.Condition{ + { + Type: string(v1alpha2.PolicyConditionAccepted), + Status: metav1.ConditionFalse, + ObservedGeneration: 2, + LastTransitionTime: transitionTime, + Reason: string(v1alpha2.PolicyReasonTargetNotFound), + Message: "target not found", + }, + }, + }, + }, + }, + validPolicyKey.NsName: { + Ancestors: []v1alpha2.PolicyAncestorStatus{ + { + AncestorRef: v1.ParentReference{ + Name: "ancestor", + }, + ControllerName: gatewayCtlrName, + Conditions: []metav1.Condition{ + { + Type: string(v1alpha2.PolicyConditionAccepted), + Status: metav1.ConditionTrue, + ObservedGeneration: 2, + LastTransitionTime: transitionTime, + Reason: string(v1alpha2.PolicyReasonAccepted), + Message: "Policy is accepted", + }, + }, + }, + }, + }, + }, + }, + { + name: "policy with policy conditions and ancestor conditions; policy conditions win", + policies: map[graph.PolicyKey]*graph.Policy{ + multiInvalidCondsPolicyKey: getPolicy(multiInvalidCondsPolicyCfg), + }, + expected: map[types.NamespacedName]v1alpha2.PolicyStatus{ + multiInvalidCondsPolicyKey.NsName: { + Ancestors: []v1alpha2.PolicyAncestorStatus{ + { + AncestorRef: v1.ParentReference{ + Name: "ancestor", + }, + ControllerName: gatewayCtlrName, + Conditions: []metav1.Condition{ + { + Type: string(v1alpha2.PolicyConditionAccepted), + Status: metav1.ConditionFalse, + ObservedGeneration: 2, + LastTransitionTime: transitionTime, + Reason: string(v1alpha2.PolicyReasonInvalid), + Message: "invalid", + }, + }, + }, + }, + }, + }, + }, + { + name: "Policy with nil ancestor", + policies: map[graph.PolicyKey]*graph.Policy{ + nilAncestorPolicyKey: getPolicy(nilAncestorPolicyCfg), + }, + expected: map[types.NamespacedName]v1alpha2.PolicyStatus{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + k8sClient := createK8sClientFor(&ngfAPI.ClientSettingsPolicy{}) + + for _, pol := range test.policies { + err := k8sClient.Create(context.Background(), pol.Source) + g.Expect(err).ToNot(HaveOccurred()) + } + + updater := statusFramework.NewUpdater(k8sClient, zap.New()) + + reqs := PrepareNGFPolicyRequests(test.policies, transitionTime, gatewayCtlrName) + + g.Expect(reqs).To(HaveLen(len(test.expected))) + + updater.Update(context.Background(), reqs...) + + for nsname, expected := range test.expected { + var pol ngfAPI.ClientSettingsPolicy + + err := k8sClient.Get(context.Background(), nsname, &pol) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(helpers.Diff(expected, pol.Status)).To(BeEmpty()) + } + }) + } +} diff --git a/internal/mode/static/status/status_setters.go b/internal/mode/static/status/status_setters.go index 12cf21cb69..141a1a3411 100644 --- a/internal/mode/static/status/status_setters.go +++ b/internal/mode/static/status/status_setters.go @@ -6,11 +6,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" "sigs.k8s.io/gateway-api/apis/v1alpha2" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "sigs.k8s.io/gateway-api/apis/v1alpha3" ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" frameworkStatus "github.com/nginxinc/nginx-gateway-fabric/internal/framework/status" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies" ) func newNginxGatewayStatusSetter(status ngfAPI.NginxGatewayStatus) frameworkStatus.Setter { @@ -41,7 +43,7 @@ func newGatewayStatusSetter(status gatewayv1.GatewayStatus) frameworkStatus.Sett func gwStatusEqual(prev, cur gatewayv1.GatewayStatus) bool { addressesEqual := slices.EqualFunc(prev.Addresses, cur.Addresses, func(a1, a2 gatewayv1.GatewayStatusAddress) bool { - if !equalPointers[gatewayv1.AddressType](a1.Type, a2.Type) { + if !helpers.EqualPointers[gatewayv1.AddressType](a1.Type, a2.Type) { return false } @@ -74,7 +76,7 @@ func gwStatusEqual(prev, cur gatewayv1.GatewayStatus) bool { return false } - return equalPointers(k1.Group, k2.Group) + return helpers.EqualPointers(k1.Group, k2.Group) }) }) } @@ -164,11 +166,11 @@ func routeParentStatusEqual(p1, p2 gatewayv1.RouteParentStatus) bool { return false } - if !equalPointers(p1.ParentRef.Namespace, p2.ParentRef.Namespace) { + if !helpers.EqualPointers(p1.ParentRef.Namespace, p2.ParentRef.Namespace) { return false } - if !equalPointers(p1.ParentRef.SectionName, p2.ParentRef.SectionName) { + if !helpers.EqualPointers(p1.ParentRef.SectionName, p2.ParentRef.SectionName) { return false } @@ -212,7 +214,7 @@ func newBackendTLSPolicyStatusSetter( ancestors = append(ancestors, status.Ancestors...) status.Ancestors = ancestors - if btpStatusEqual(gatewayCtlrName, btp.Status, status) { + if policyStatusEqual(gatewayCtlrName, btp.Status, status) { return false } @@ -221,8 +223,40 @@ func newBackendTLSPolicyStatusSetter( } } -func btpStatusEqual(gatewayCtlrName string, prev, cur v1alpha2.PolicyStatus) bool { - // Since other controllers may update BackendTLSPolicy status we can't assume anything about the order of the +func newNGFPolicyStatusSetter( + status gatewayv1alpha2.PolicyStatus, + gatewayCtlrName string, +) frameworkStatus.Setter { + return func(object client.Object) (wasSet bool) { + policy := helpers.MustCastObject[policies.Policy](object) + prevStatus := policy.GetPolicyStatus() + + // maxAncestors is the max number of ancestor statuses which is the sum of all new ancestor statuses and all old + // ancestor statuses. + maxAncestors := len(status.Ancestors) + len(prevStatus.Ancestors) + ancestors := make([]gatewayv1alpha2.PolicyAncestorStatus, 0, maxAncestors) + + // keep all the ancestor statuses that belong to other controllers + for _, as := range prevStatus.Ancestors { + if string(as.ControllerName) != gatewayCtlrName { + ancestors = append(ancestors, as) + } + } + + ancestors = append(ancestors, status.Ancestors...) + status.Ancestors = ancestors + + if policyStatusEqual(gatewayCtlrName, prevStatus, status) { + return false + } + + policy.SetPolicyStatus(status) + return true + } +} + +func policyStatusEqual(gatewayCtlrName string, prev, cur gatewayv1alpha2.PolicyStatus) bool { + // Since other controllers may update Policy status we can't assume anything about the order of the // statuses, and we have to ignore statuses written by other controllers when checking for equality. // Therefore, we can't use slices.EqualFunc here because it cares about the order. @@ -233,7 +267,7 @@ func btpStatusEqual(gatewayCtlrName string, prev, cur v1alpha2.PolicyStatus) boo } exists := slices.ContainsFunc(cur.Ancestors, func(curAncestor v1alpha2.PolicyAncestorStatus) bool { - return btpAncestorStatusEqual(prevAncestor, curAncestor) + return ancestorStatusEqual(prevAncestor, curAncestor) }) if !exists { @@ -244,7 +278,7 @@ func btpStatusEqual(gatewayCtlrName string, prev, cur v1alpha2.PolicyStatus) boo // Then, we check if the cur status has any PolicyAncestorStatuses that are no longer present in the prev status. for _, curParent := range cur.Ancestors { exists := slices.ContainsFunc(prev.Ancestors, func(prevAncestor v1alpha2.PolicyAncestorStatus) bool { - return btpAncestorStatusEqual(curParent, prevAncestor) + return ancestorStatusEqual(curParent, prevAncestor) }) if !exists { @@ -255,7 +289,7 @@ func btpStatusEqual(gatewayCtlrName string, prev, cur v1alpha2.PolicyStatus) boo return true } -func btpAncestorStatusEqual(p1, p2 v1alpha2.PolicyAncestorStatus) bool { +func ancestorStatusEqual(p1, p2 v1alpha2.PolicyAncestorStatus) bool { if p1.ControllerName != p2.ControllerName { return false } @@ -264,34 +298,18 @@ func btpAncestorStatusEqual(p1, p2 v1alpha2.PolicyAncestorStatus) bool { return false } - if !equalPointers(p1.AncestorRef.Namespace, p2.AncestorRef.Namespace) { + if !helpers.EqualPointers(p1.AncestorRef.Namespace, p2.AncestorRef.Namespace) { return false } - // we ignore the rest of the AncestorRef fields because we do not set them - - return frameworkStatus.ConditionsEqual(p1.Conditions, p2.Conditions) -} - -// equalPointers returns whether two pointers are equal. -// Pointers are considered equal if one of the following is true: -// - They are both nil. -// - One is nil and the other is empty (e.g. nil string and ""). -// - They are both non-nil, and their values are the same. -func equalPointers[T comparable](p1, p2 *T) bool { - if p1 == nil && p2 == nil { - return true - } - - var p1Val, p2Val T - - if p1 != nil { - p1Val = *p1 + if !helpers.EqualPointers(p1.AncestorRef.Group, p2.AncestorRef.Group) { + return false } - if p2 != nil { - p2Val = *p2 + if !helpers.EqualPointers(p1.AncestorRef.Kind, p2.AncestorRef.Kind) { + return false } + // we ignore the rest of the AncestorRef fields because we do not set them - return p1Val == p2Val + return frameworkStatus.ConditionsEqual(p1.Conditions, p2.Conditions) } diff --git a/internal/mode/static/status/status_setters_test.go b/internal/mode/static/status/status_setters_test.go index d258ec6a68..a7b838342d 100644 --- a/internal/mode/static/status/status_setters_test.go +++ b/internal/mode/static/status/status_setters_test.go @@ -11,6 +11,8 @@ import ( ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/kinds" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/policies/policiesfakes" ) func TestNewNginxGatewayStatusSetter(t *testing.T) { @@ -665,6 +667,153 @@ func TestNewBackendTLSPolicyStatusSetter(t *testing.T) { } } +func TestNewNGFPolicyStatusSetter(t *testing.T) { + const ( + controllerName = "controller" + otherControllerName = "other-controller" + ) + + tests := []struct { + name string + status, newStatus, expStatus v1alpha2.PolicyStatus + expStatusSet bool + }{ + { + name: "Policy has no status", + newStatus: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ + { + ControllerName: controllerName, + Conditions: []metav1.Condition{{Message: "new condition"}}, + }, + }, + }, + expStatus: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ + { + ControllerName: controllerName, + Conditions: []metav1.Condition{{Message: "new condition"}}, + }, + }, + }, + expStatusSet: true, + }, + { + name: "Policy has old status", + newStatus: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ + { + ControllerName: controllerName, + Conditions: []metav1.Condition{{Message: "new condition"}}, + }, + }, + }, + status: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ + { + ControllerName: controllerName, + Conditions: []metav1.Condition{{Message: "old condition"}}, + }, + }, + }, + expStatus: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ + { + ControllerName: controllerName, + Conditions: []metav1.Condition{{Message: "new condition"}}, + }, + }, + }, + expStatusSet: true, + }, + { + name: "Policy has old status and other controller status", + newStatus: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ + { + ControllerName: controllerName, + Conditions: []metav1.Condition{{Message: "new condition"}}, + }, + }, + }, + status: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ + { + ControllerName: controllerName, + Conditions: []metav1.Condition{{Message: "old condition"}}, + }, + { + ControllerName: otherControllerName, + Conditions: []metav1.Condition{{Message: "some condition"}}, + }, + }, + }, + expStatus: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ + { + ControllerName: otherControllerName, + Conditions: []metav1.Condition{{Message: "some condition"}}, + }, + { + ControllerName: controllerName, + Conditions: []metav1.Condition{{Message: "new condition"}}, + }, + }, + }, + expStatusSet: true, + }, + { + name: "Policy has same status", + newStatus: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ + { + ControllerName: controllerName, + Conditions: []metav1.Condition{{Message: "same condition"}}, + }, + }, + }, + status: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ + { + ControllerName: controllerName, + Conditions: []metav1.Condition{{Message: "same condition"}}, + }, + }, + }, + expStatus: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ + { + ControllerName: controllerName, + Conditions: []metav1.Condition{{Message: "same condition"}}, + }, + }, + }, + expStatusSet: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + setter := newNGFPolicyStatusSetter(test.newStatus, controllerName) + obj := &policiesfakes.FakePolicy{ + GetPolicyStatusStub: func() v1alpha2.PolicyStatus { + return test.status + }, + } + + statusSet := setter(obj) + + g.Expect(statusSet).To(Equal(test.expStatusSet)) + + if statusSet { + g.Expect(obj.SetPolicyStatusArgsForCall(0)).To(Equal(test.expStatus)) + } + }) + } +} + func TestGWStatusEqual(t *testing.T) { getDefaultStatus := func() gatewayv1.GatewayStatus { return gatewayv1.GatewayStatus{ @@ -689,7 +838,7 @@ func TestGWStatusEqual(t *testing.T) { SupportedKinds: []gatewayv1.RouteGroupKind{ { Group: helpers.GetPointer[gatewayv1.Group](gatewayv1.GroupName), - Kind: "HTTPRoute", + Kind: kinds.HTTPRoute, }, { Group: helpers.GetPointer[gatewayv1.Group](gatewayv1.GroupName), @@ -708,7 +857,7 @@ func TestGWStatusEqual(t *testing.T) { SupportedKinds: []gatewayv1.RouteGroupKind{ { Group: helpers.GetPointer[gatewayv1.Group](gatewayv1.GroupName), - Kind: "HTTPRoute", + Kind: kinds.HTTPRoute, }, }, AttachedRoutes: 1, @@ -723,7 +872,7 @@ func TestGWStatusEqual(t *testing.T) { SupportedKinds: []gatewayv1.RouteGroupKind{ { Group: helpers.GetPointer[gatewayv1.Group](gatewayv1.GroupName), - Kind: "HTTPRoute", + Kind: kinds.HTTPRoute, }, }, AttachedRoutes: 1, @@ -1091,87 +1240,51 @@ func TestRouteParentStatusEqual(t *testing.T) { } } -func TestEqualPointers(t *testing.T) { - tests := []struct { - p1 *string - p2 *string - name string - expEqual bool - }{ - { - name: "first pointer nil; second has non-empty value", - p1: nil, - p2: helpers.GetPointer("test"), - expEqual: false, - }, - { - name: "second pointer nil; first has non-empty value", - p1: helpers.GetPointer("test"), - p2: nil, - expEqual: false, - }, - { - name: "different values", - p1: helpers.GetPointer("test"), - p2: helpers.GetPointer("different"), - expEqual: false, - }, - { - name: "both pointers nil", - p1: nil, - p2: nil, - expEqual: true, - }, - { - name: "first pointer nil; second empty", - p1: nil, - p2: helpers.GetPointer(""), - expEqual: true, - }, - { - name: "second pointer nil; first empty", - p1: helpers.GetPointer(""), - p2: nil, - expEqual: true, - }, - { - name: "same value", - p1: helpers.GetPointer("test"), - p2: helpers.GetPointer("test"), - expEqual: true, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - g := NewWithT(t) - - val := equalPointers(test.p1, test.p2) - g.Expect(val).To(Equal(test.expEqual)) - }) - } -} - -func TestBtpStatusEqual(t *testing.T) { - getPolicyStatus := func(ancestorName, ancestorNs, ctlrName string) v1alpha2.PolicyStatus { +func TestPolicyStatusEqual(t *testing.T) { + getPolicyStatus := func() v1alpha2.PolicyStatus { return v1alpha2.PolicyStatus{ Ancestors: []v1alpha2.PolicyAncestorStatus{ { AncestorRef: gatewayv1.ParentReference{ - Namespace: helpers.GetPointer[gatewayv1.Namespace]((gatewayv1.Namespace)(ancestorNs)), - Name: v1alpha2.ObjectName(ancestorName), + Namespace: helpers.GetPointer[gatewayv1.Namespace]("ns1"), + Name: "ancestor1", + Group: helpers.GetPointer[gatewayv1.Group](gatewayv1.GroupName), + Kind: helpers.GetPointer[gatewayv1.Kind](kinds.Gateway), }, - ControllerName: v1alpha2.GatewayController(ctlrName), + ControllerName: "ctlr1", Conditions: []metav1.Condition{{Type: "otherType", Status: "otherStatus"}}, }, }, } } - prevMultiple := getPolicyStatus("ancestor1", "ns1", "ctlr1") - prevMultiple.Ancestors = append(prevMultiple.Ancestors, getPolicyStatus("ancestor2", "ns2", "ctlr2").Ancestors...) - currMultiple := getPolicyStatus("ancestor1", "ns1", "ctlr1") - currMultiple.Ancestors = append(currMultiple.Ancestors, getPolicyStatus("ancestor3", "ns3", "ctlr2").Ancestors...) + type modFunc func(s v1alpha2.PolicyStatus) v1alpha2.PolicyStatus + + getModifiedPolicyStatus := func(mod modFunc) v1alpha2.PolicyStatus { + return mod(getPolicyStatus()) + } + + prevMultiple := getPolicyStatus() + prevMultiple.Ancestors = append( + prevMultiple.Ancestors, + getModifiedPolicyStatus(func(s v1alpha2.PolicyStatus) v1alpha2.PolicyStatus { + ns := "ns2" + s.Ancestors[0].AncestorRef.Name = "ancestor2" + s.Ancestors[0].AncestorRef.Namespace = (*gatewayv1.Namespace)(&ns) + s.Ancestors[0].ControllerName = "ctlr2" + return s + }).Ancestors...) + + currMultiple := getPolicyStatus() + currMultiple.Ancestors = append( + currMultiple.Ancestors, + getModifiedPolicyStatus(func(s v1alpha2.PolicyStatus) v1alpha2.PolicyStatus { + ns := "ns3" + s.Ancestors[0].AncestorRef.Name = "ancestor3" + s.Ancestors[0].AncestorRef.Namespace = (*gatewayv1.Namespace)(&ns) + s.Ancestors[0].ControllerName = "ctlr3" + return s + }).Ancestors...) tests := []struct { name string @@ -1182,36 +1295,79 @@ func TestBtpStatusEqual(t *testing.T) { }{ { name: "status equal", - previous: getPolicyStatus("ancestor1", "ns1", "ctlr1"), - current: getPolicyStatus("ancestor1", "ns1", "ctlr1"), + previous: getPolicyStatus(), + current: getPolicyStatus(), controllerName: "ctlr1", expEqual: true, }, { - name: "status not equal, different ancestor name", - previous: getPolicyStatus("ancestor1", "ns1", "ctlr1"), - current: getPolicyStatus("ancestor2", "ns1", "ctlr1"), + name: "status not equal, different ancestor name", + previous: getPolicyStatus(), + current: getModifiedPolicyStatus(func(s v1alpha2.PolicyStatus) v1alpha2.PolicyStatus { + s.Ancestors[0].AncestorRef.Name = "diff" + return s + }), + controllerName: "ctlr1", + expEqual: false, + }, + { + name: "status not equal, different ancestor namespace", + previous: getPolicyStatus(), + current: getModifiedPolicyStatus(func(s v1alpha2.PolicyStatus) v1alpha2.PolicyStatus { + ns := "diff" + s.Ancestors[0].AncestorRef.Namespace = (*gatewayv1.Namespace)(&ns) + return s + }), + controllerName: "ctlr1", + expEqual: false, + }, + { + name: "status not equal, different ancestor kind", + previous: getPolicyStatus(), + current: getModifiedPolicyStatus(func(s v1alpha2.PolicyStatus) v1alpha2.PolicyStatus { + s.Ancestors[0].AncestorRef.Kind = helpers.GetPointer[gatewayv1.Kind]("diff") + return s + }), + controllerName: "ctlr1", + expEqual: false, + }, + { + name: "status not equal, different ancestor group", + previous: getPolicyStatus(), + current: getModifiedPolicyStatus(func(s v1alpha2.PolicyStatus) v1alpha2.PolicyStatus { + s.Ancestors[0].AncestorRef.Group = helpers.GetPointer[gatewayv1.Group]("diff") + return s + }), controllerName: "ctlr1", expEqual: false, }, { - name: "status not equal, different ancestor namespace", - previous: getPolicyStatus("ancestor1", "ns1", "ctlr1"), - current: getPolicyStatus("ancestor1", "ns2", "ctlr1"), + name: "status not equal, different controller name on current", + previous: getPolicyStatus(), + current: getModifiedPolicyStatus(func(s v1alpha2.PolicyStatus) v1alpha2.PolicyStatus { + s.Ancestors[0].ControllerName = "diff" + return s + }), controllerName: "ctlr1", expEqual: false, }, { - name: "status not equal, different controller name on current", - previous: getPolicyStatus("ancestor1", "ns1", "ctlr1"), - current: getPolicyStatus("ancestor1", "ns1", "ctlr2"), + name: "status not equal, different conds", + previous: getPolicyStatus(), + current: getModifiedPolicyStatus(func(s v1alpha2.PolicyStatus) v1alpha2.PolicyStatus { + s.Ancestors[0].Conditions = nil + return s + }), controllerName: "ctlr1", expEqual: false, }, { - name: "status not equal, different controller name on previous", - previous: getPolicyStatus("ancestor1", "ns1", "ctlr2"), - current: getPolicyStatus("ancestor1", "ns1", "ctlr1"), + name: "status not equal, different controller name on previous", + previous: getModifiedPolicyStatus(func(s v1alpha2.PolicyStatus) v1alpha2.PolicyStatus { + s.Ancestors[0].ControllerName = "diff" + return s + }), + current: getPolicyStatus(), controllerName: "ctlr1", expEqual: false, }, @@ -1227,7 +1383,7 @@ func TestBtpStatusEqual(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { g := NewWithT(t) - equal := btpStatusEqual(test.controllerName, test.previous, test.current) + equal := policyStatusEqual(test.controllerName, test.previous, test.current) g.Expect(equal).To(Equal(test.expEqual)) }) }