diff --git a/api/v1alpha1/policy_helpers.go b/api/v1alpha1/policy_helpers.go index 054ca72f71..8c4c9762a9 100644 --- a/api/v1alpha1/policy_helpers.go +++ b/api/v1alpha1/policy_helpers.go @@ -6,6 +6,7 @@ package v1alpha1 import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" ) @@ -37,7 +38,14 @@ type TargetSelector struct { Kind gwapiv1.Kind `json:"kind"` // MatchLabels are the set of label selectors for identifying the targeted resource - MatchLabels map[string]string `json:"matchLabels"` + // +optional + MatchLabels map[string]string `json:"matchLabels,omitempty"` + + // MatchExpressions is a list of label selector requirements. The requirements are ANDed. + // + // +optional + // +listType=atomic + MatchExpressions []metav1.LabelSelectorRequirement `json:"matchExpressions,omitempty"` } func (p PolicyTargetReferences) GetTargetRefs() []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index f26bbf4579..1090e34f9d 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -5673,6 +5673,13 @@ func (in *TargetSelector) DeepCopyInto(out *TargetSelector) { (*out)[key] = val } } + if in.MatchExpressions != nil { + in, out := &in.MatchExpressions, &out.MatchExpressions + *out = make([]metav1.LabelSelectorRequirement, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetSelector. diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml index 6808e7992e..187df4482b 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backendtrafficpolicies.yaml @@ -1508,6 +1508,39 @@ spec: minLength: 1 pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ type: string + matchExpressions: + description: MatchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string @@ -1516,7 +1549,6 @@ spec: type: object required: - kind - - matchLabels type: object x-kubernetes-validations: - message: group must be gateway.networking.k8s.io diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml index ebeba3ae17..8d19d3c01d 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_clienttrafficpolicies.yaml @@ -608,6 +608,39 @@ spec: minLength: 1 pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ type: string + matchExpressions: + description: MatchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string @@ -616,7 +649,6 @@ spec: type: object required: - kind - - matchLabels type: object x-kubernetes-validations: - message: group must be gateway.networking.k8s.io diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml index 6036db8264..abeb7e43b2 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyextensionpolicies.yaml @@ -1201,6 +1201,39 @@ spec: minLength: 1 pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ type: string + matchExpressions: + description: MatchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string @@ -1209,7 +1242,6 @@ spec: type: object required: - kind - - matchLabels type: object x-kubernetes-validations: - message: group must be gateway.networking.k8s.io diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml index bc228f30f6..56e1583ba8 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml @@ -4447,6 +4447,39 @@ spec: minLength: 1 pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ type: string + matchExpressions: + description: MatchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string @@ -4455,7 +4488,6 @@ spec: type: object required: - kind - - matchLabels type: object x-kubernetes-validations: - message: group must be gateway.networking.k8s.io diff --git a/internal/gatewayapi/helpers.go b/internal/gatewayapi/helpers.go index 416ce98089..0ef3dd4d85 100644 --- a/internal/gatewayapi/helpers.go +++ b/internal/gatewayapi/helpers.go @@ -533,10 +533,22 @@ type targetRefWithTimestamp struct { CreationTimestamp metav1.Time } +func selectorFromTargetSelector(selector egv1a1.TargetSelector) labels.Selector { + l, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{ + MatchLabels: selector.MatchLabels, + MatchExpressions: selector.MatchExpressions, + }) + if err != nil { + // TODO - how do we we bubble this up + return labels.Nothing() + } + return l +} + func getPolicyTargetRefs[T client.Object](policy egv1a1.PolicyTargetReferences, potentialTargets []T) []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName { dedup := sets.New[targetRefWithTimestamp]() for _, currSelector := range policy.TargetSelectors { - labelSelector := labels.SelectorFromSet(currSelector.MatchLabels) + labelSelector := selectorFromTargetSelector(currSelector) for _, obj := range potentialTargets { gvk := obj.GetObjectKind().GroupVersionKind() if gvk.Kind != string(currSelector.Kind) || diff --git a/internal/gatewayapi/helpers_test.go b/internal/gatewayapi/helpers_test.go index 6403279a5a..22bcafd2c9 100644 --- a/internal/gatewayapi/helpers_test.go +++ b/internal/gatewayapi/helpers_test.go @@ -16,6 +16,7 @@ import ( "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" @@ -475,6 +476,106 @@ func TestGetPolicyTargetRefs(t *testing.T) { }, results: []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{}, }, + { + name: "match expression", + policy: egv1a1.PolicyTargetReferences{ + TargetSelectors: []egv1a1.TargetSelector{ + { + Kind: "Gateway", + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "environment", + Operator: "In", + Values: []string{"prod", "staging"}, + }, + }, + }, + }, + }, + targets: []*unstructured.Unstructured{ + { + Object: map[string]any{ + "apiVersion": "gateway.networking.k8s.io/v1", + "kind": "Gateway", + "metadata": map[string]any{ + "name": "first", + "namespace": "default", + "labels": map[string]any{ + "environment": "prod", + }, + }, + }, + }, + { + Object: map[string]any{ + "apiVersion": "gateway.networking.k8s.io/v1", + "kind": "Gateway", + "metadata": map[string]any{ + "name": "second", + "namespace": "default", + "labels": map[string]any{ + "environment": "dev", + }, + }, + }, + }, + }, + results: []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{ + { + LocalPolicyTargetReference: gwapiv1a2.LocalPolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Name: "first", + }, + }, + }, + }, + { + name: "match expression - bad expression matches nothing", + policy: egv1a1.PolicyTargetReferences{ + TargetSelectors: []egv1a1.TargetSelector{ + { + Kind: "Gateway", + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "environment", + Operator: "Foo", + Values: []string{"prod", "staging"}, + }, + }, + }, + }, + }, + targets: []*unstructured.Unstructured{ + { + Object: map[string]any{ + "apiVersion": "gateway.networking.k8s.io/v1", + "kind": "Gateway", + "metadata": map[string]any{ + "name": "first", + "namespace": "default", + "labels": map[string]any{ + "environment": "prod", + }, + }, + }, + }, + { + Object: map[string]any{ + "apiVersion": "gateway.networking.k8s.io/v1", + "kind": "Gateway", + "metadata": map[string]any{ + "name": "second", + "namespace": "default", + "labels": map[string]any{ + "environment": "dev", + }, + }, + }, + }, + }, + results: []gwapiv1a2.LocalPolicyTargetReferenceWithSectionName{}, + }, } for _, tc := range testCases { diff --git a/release-notes/current.yaml b/release-notes/current.yaml index 892ad239dd..8d17ced8e2 100644 --- a/release-notes/current.yaml +++ b/release-notes/current.yaml @@ -11,6 +11,7 @@ security updates: | new features: | Added support for configuring maxUnavailable in KubernetesPodDisruptionBudgetSpec Added support for percentage-based request mirroring + Allow matchExpressions in TargetSelector Add defaulter for gateway-api resources loading from file to be able to set default values. bug fixes: | diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 49a929a4f6..cd3df8d0a9 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -4297,7 +4297,8 @@ _Appears in:_ | --- | --- | --- | --- | --- | | `group` | _[Group](#group)_ | true | gateway.networking.k8s.io | Group is the group that this selector targets. Defaults to gateway.networking.k8s.io | | `kind` | _[Kind](#kind)_ | true | | Kind is the resource kind that this selector targets. | -| `matchLabels` | _object (keys:string, values:string)_ | true | | MatchLabels are the set of label selectors for identifying the targeted resource | +| `matchLabels` | _object (keys:string, values:string)_ | false | | MatchLabels are the set of label selectors for identifying the targeted resource | +| `matchExpressions` | _[LabelSelectorRequirement](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#labelselectorrequirement-v1-meta) array_ | false | | MatchExpressions is a list of label selector requirements. The requirements are ANDed. | #### Timeout diff --git a/site/content/zh/latest/api/extension_types.md b/site/content/zh/latest/api/extension_types.md index 49a929a4f6..cd3df8d0a9 100644 --- a/site/content/zh/latest/api/extension_types.md +++ b/site/content/zh/latest/api/extension_types.md @@ -4297,7 +4297,8 @@ _Appears in:_ | --- | --- | --- | --- | --- | | `group` | _[Group](#group)_ | true | gateway.networking.k8s.io | Group is the group that this selector targets. Defaults to gateway.networking.k8s.io | | `kind` | _[Kind](#kind)_ | true | | Kind is the resource kind that this selector targets. | -| `matchLabels` | _object (keys:string, values:string)_ | true | | MatchLabels are the set of label selectors for identifying the targeted resource | +| `matchLabels` | _object (keys:string, values:string)_ | false | | MatchLabels are the set of label selectors for identifying the targeted resource | +| `matchExpressions` | _[LabelSelectorRequirement](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#labelselectorrequirement-v1-meta) array_ | false | | MatchExpressions is a list of label selector requirements. The requirements are ANDed. | #### Timeout diff --git a/test/e2e/testdata/backend-traffic-policy-match-expression.yaml b/test/e2e/testdata/backend-traffic-policy-match-expression.yaml new file mode 100644 index 0000000000..21820c0e02 --- /dev/null +++ b/test/e2e/testdata/backend-traffic-policy-match-expression.yaml @@ -0,0 +1,43 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: backend-traffic-match-expression-normal + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + hostnames: ["normal.example.com"] + rules: + - backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: backend-traffic-match-expression-injected + namespace: gateway-conformance-infra + labels: + inject: me +spec: + parentRefs: + - name: same-namespace + hostnames: ["injected.example.com"] + rules: + - backendRefs: + - name: infra-backend-v1 + port: 8080 +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: backend-traffic-match-expression + namespace: gateway-conformance-infra +spec: + targetSelectors: + - kind: HTTPRoute + matchExpressions: [{key: inject, operator: Exists}] + faultInjection: + abort: + httpStatus: 418 diff --git a/test/e2e/tests/backend_traffic_policy_match_expression.go b/test/e2e/tests/backend_traffic_policy_match_expression.go new file mode 100644 index 0000000000..1f35dc2045 --- /dev/null +++ b/test/e2e/tests/backend_traffic_policy_match_expression.go @@ -0,0 +1,98 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +//go:build e2e + +package tests + +import ( + "testing" + + "k8s.io/apimachinery/pkg/types" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" + gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + "sigs.k8s.io/gateway-api/conformance/utils/http" + "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" + "sigs.k8s.io/gateway-api/conformance/utils/suite" + + "github.com/envoyproxy/gateway/internal/gatewayapi" + "github.com/envoyproxy/gateway/internal/gatewayapi/resource" +) + +func init() { + ConformanceTests = append(ConformanceTests, BackendTrafficPolicyMatchExpressionTest) +} + +var BackendTrafficPolicyMatchExpressionTest = suite.ConformanceTest{ + ShortName: "BackendTrafficPolicyMatchExpression", + Description: "Use match expression to select one HTTPRoute", + Manifests: []string{"testdata/backend-traffic-policy-match-expression.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + t.Run("BackendTrafficPolicyMatchExpression", func(t *testing.T) { + ns := "gateway-conformance-infra" + + routeNormal := types.NamespacedName{Name: "backend-traffic-match-expression-normal", Namespace: ns} + routeInjected := types.NamespacedName{Name: "backend-traffic-match-expression-injected", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, + suite.Client, + suite.TimeoutConfig, + suite.ControllerName, + kubernetes.NewGatewayRef(gwNN), + routeNormal, + routeInjected, + ) + + BackendTrafficPolicyMustBeAccepted(t, + suite.Client, + types.NamespacedName{Name: "backend-traffic-match-expression", Namespace: ns}, + suite.ControllerName, + gwapiv1a2.ParentReference{ + Group: gatewayapi.GroupPtr(gwapiv1.GroupName), + Kind: gatewayapi.KindPtr(resource.KindGateway), + Namespace: gatewayapi.NamespacePtr(gwNN.Namespace), + Name: gwapiv1.ObjectName(gwNN.Name), + }, + ) + + normalResponse := http.ExpectedResponse{ + Namespace: ns, + Request: http.Request{ + Host: "normal.example.com", + Path: "/", + }, + Response: http.Response{ + StatusCode: 200, + }, + } + + http.MakeRequestAndExpectEventuallyConsistentResponse(t, + suite.RoundTripper, + suite.TimeoutConfig, + gwAddr, + normalResponse, + ) + + injectedResponse := http.ExpectedResponse{ + Namespace: ns, + Request: http.Request{ + Host: "injected.example.com", + Path: "/", + }, + Response: http.Response{ + StatusCode: 418, + }, + } + + http.MakeRequestAndExpectEventuallyConsistentResponse(t, + suite.RoundTripper, + suite.TimeoutConfig, + gwAddr, + injectedResponse, + ) + }) + }, +}