Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions api/v1alpha1/shared_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ const (
// +kubebuilder:validation:XValidation:message="allocateLoadBalancerNodePorts can only be set for LoadBalancer type",rule="!has(self.allocateLoadBalancerNodePorts) || self.type == 'LoadBalancer'"
// +kubebuilder:validation:XValidation:message="loadBalancerSourceRanges can only be set for LoadBalancer type",rule="!has(self.loadBalancerSourceRanges) || self.type == 'LoadBalancer'"
// +kubebuilder:validation:XValidation:message="loadBalancerIP can only be set for LoadBalancer type",rule="!has(self.loadBalancerIP) || self.type == 'LoadBalancer'"
// +kubebuilder:validation:XValidation:message="healthCheckNodePort can only be set for LoadBalancer type with Local externalTrafficPolicy",rule="!has(self.healthCheckNodePort) || (self.type == 'LoadBalancer' && (!has(self.externalTrafficPolicy) || self.externalTrafficPolicy == 'Local'))"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mirror healthCheckNodePort checks in non-CEL validation

This adds a CEL rule for healthCheckNodePort, but the non-CEL validator (api/v1alpha1/validation/envoyproxy_validate.go in validateService) was not updated even though it is still used by internal/gatewayapi/translator.go and egctl paths. In those paths, an EnvoyProxy can pass validation and still produce a Service spec with healthCheckNodePort in unsupported combinations (for example after config merges), which Kubernetes then rejects during reconciliation. Please add equivalent checks in validateService so all validation paths enforce the same constraints.

Useful? React with 👍 / 👎.

type KubernetesServiceSpec struct {
// Annotations that should be appended to the service.
// By default, no annotations are appended.
Expand Down Expand Up @@ -379,6 +380,19 @@ type KubernetesServiceSpec struct {
// +optional
ExternalTrafficPolicy *ServiceExternalTrafficPolicy `json:"externalTrafficPolicy,omitempty"`

// HealthCheckNodePort specifies the healthcheck nodePort for the Envoy Service.
// This only applies when Type is set to LoadBalancer and ExternalTrafficPolicy is set to Local.
// If a value is specified, is in-range, and is not in use, it will be used. If not specified,
// a value will be automatically allocated by the Kubernetes API server. External systems
// (e.g. load-balancers) can use this port to determine if a given node holds endpoints for
// this service or not. Specifying the value allows network policies to allowlist a known port
// rather than a randomly allocated one.
//
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=65535
// +optional
HealthCheckNodePort *int32 `json:"healthCheckNodePort,omitempty"`

// Patch defines how to perform the patch operation to the service
//
// +optional
Expand Down
8 changes: 8 additions & 0 deletions api/v1alpha1/validation/envoyproxy_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,14 @@ func validateService(spec *egv1a1.EnvoyProxySpec) []error {
errs = append(errs, fmt.Errorf("loadBalancerIP:%s is an invalid IP address", *serviceLoadBalancerIP))
}
}
if serviceType, serviceHealthCheckNodePort := spec.Provider.Kubernetes.EnvoyService.Type, spec.Provider.Kubernetes.EnvoyService.HealthCheckNodePort; serviceType != nil && serviceHealthCheckNodePort != nil {
if *serviceType != egv1a1.ServiceTypeLoadBalancer {
errs = append(errs, fmt.Errorf("healthCheckNodePort can only be set for %v type", egv1a1.ServiceTypeLoadBalancer))
}
if etp := spec.Provider.Kubernetes.EnvoyService.ExternalTrafficPolicy; etp != nil && *etp != egv1a1.ServiceExternalTrafficPolicyLocal {
errs = append(errs, fmt.Errorf("healthCheckNodePort can only be set when externalTrafficPolicy is %v", egv1a1.ServiceExternalTrafficPolicyLocal))
}
}
if patch := spec.Provider.Kubernetes.EnvoyService.Patch; patch != nil {
if patch.Value.Raw == nil {
errs = append(errs, fmt.Errorf("envoy service patch object cannot be empty"))
Expand Down
64 changes: 64 additions & 0 deletions api/v1alpha1/validation/envoyproxy_validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,70 @@ func TestValidateEnvoyProxy(t *testing.T) {
},
expected: false,
},
{
name: "envoy service type 'LoadBalancer' with healthCheckNodePort",
proxy: &egv1a1.EnvoyProxy{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test",
Name: "test",
},
Spec: egv1a1.EnvoyProxySpec{
Provider: &egv1a1.EnvoyProxyProvider{
Type: egv1a1.EnvoyProxyProviderTypeKubernetes,
Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{
EnvoyService: &egv1a1.KubernetesServiceSpec{
Type: egv1a1.GetKubernetesServiceType(egv1a1.ServiceTypeLoadBalancer),
HealthCheckNodePort: new(int32(30123)),
},
},
},
},
},
expected: true,
},
{
name: "non envoy service type 'LoadBalancer' with healthCheckNodePort",
proxy: &egv1a1.EnvoyProxy{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test",
Name: "test",
},
Spec: egv1a1.EnvoyProxySpec{
Provider: &egv1a1.EnvoyProxyProvider{
Type: egv1a1.EnvoyProxyProviderTypeKubernetes,
Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{
EnvoyService: &egv1a1.KubernetesServiceSpec{
Type: egv1a1.GetKubernetesServiceType(egv1a1.ServiceTypeClusterIP),
HealthCheckNodePort: new(int32(30123)),
},
},
},
},
},
expected: false,
},
{
name: "envoy service type 'LoadBalancer' with Cluster externalTrafficPolicy and healthCheckNodePort",
proxy: &egv1a1.EnvoyProxy{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test",
Name: "test",
},
Spec: egv1a1.EnvoyProxySpec{
Provider: &egv1a1.EnvoyProxyProvider{
Type: egv1a1.EnvoyProxyProviderTypeKubernetes,
Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{
EnvoyService: &egv1a1.KubernetesServiceSpec{
Type: egv1a1.GetKubernetesServiceType(egv1a1.ServiceTypeLoadBalancer),
ExternalTrafficPolicy: egv1a1.GetKubernetesServiceExternalTrafficPolicy(egv1a1.ServiceExternalTrafficPolicyCluster),
HealthCheckNodePort: new(int32(30123)),
},
},
},
},
},
expected: false,
},
{
name: "should invalid when accesslog enabled using Text format, but `text` field being empty",
proxy: &egv1a1.EnvoyProxy{
Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -11124,6 +11124,19 @@ spec:
- Local
- Cluster
type: string
healthCheckNodePort:
description: |-
HealthCheckNodePort specifies the healthcheck nodePort for the Envoy Service.
This only applies when Type is set to LoadBalancer and ExternalTrafficPolicy is set to Local.
If a value is specified, is in-range, and is not in use, it will be used. If not specified,
a value will be automatically allocated by the Kubernetes API server. External systems
(e.g. load-balancers) can use this port to determine if a given node holds endpoints for
this service or not. Specifying the value allows network policies to allowlist a known port
rather than a randomly allocated one.
format: int32
maximum: 65535
minimum: 1
type: integer
labels:
additionalProperties:
type: string
Expand Down Expand Up @@ -11204,6 +11217,11 @@ spec:
- message: loadBalancerIP can only be set for LoadBalancer
type
rule: '!has(self.loadBalancerIP) || self.type == ''LoadBalancer'''
- message: healthCheckNodePort can only be set for LoadBalancer
type with Local externalTrafficPolicy
rule: '!has(self.healthCheckNodePort) || (self.type == ''LoadBalancer''
&& (!has(self.externalTrafficPolicy) || self.externalTrafficPolicy
== ''Local''))'
envoyServiceAccount:
description: EnvoyServiceAccount defines the desired state
of the Envoy service account resource.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11123,6 +11123,19 @@ spec:
- Local
- Cluster
type: string
healthCheckNodePort:
description: |-
HealthCheckNodePort specifies the healthcheck nodePort for the Envoy Service.
This only applies when Type is set to LoadBalancer and ExternalTrafficPolicy is set to Local.
If a value is specified, is in-range, and is not in use, it will be used. If not specified,
a value will be automatically allocated by the Kubernetes API server. External systems
(e.g. load-balancers) can use this port to determine if a given node holds endpoints for
this service or not. Specifying the value allows network policies to allowlist a known port
rather than a randomly allocated one.
format: int32
maximum: 65535
minimum: 1
type: integer
labels:
additionalProperties:
type: string
Expand Down Expand Up @@ -11203,6 +11216,11 @@ spec:
- message: loadBalancerIP can only be set for LoadBalancer
type
rule: '!has(self.loadBalancerIP) || self.type == ''LoadBalancer'''
- message: healthCheckNodePort can only be set for LoadBalancer
type with Local externalTrafficPolicy
rule: '!has(self.healthCheckNodePort) || (self.type == ''LoadBalancer''
&& (!has(self.externalTrafficPolicy) || self.externalTrafficPolicy
== ''Local''))'
envoyServiceAccount:
description: EnvoyServiceAccount defines the desired state
of the Envoy service account resource.
Expand Down
3 changes: 3 additions & 0 deletions internal/infrastructure/kubernetes/resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ func ExpectedServiceSpec(service *egv1a1.KubernetesServiceSpec) corev1.ServiceSp
if service.LoadBalancerIP != nil {
serviceSpec.LoadBalancerIP = *service.LoadBalancerIP
}
if service.HealthCheckNodePort != nil {
serviceSpec.HealthCheckNodePort = *service.HealthCheckNodePort
}
serviceSpec.ExternalTrafficPolicy = corev1.ServiceExternalTrafficPolicy(*service.ExternalTrafficPolicy)
case egv1a1.ServiceTypeNodePort:
serviceSpec.ExternalTrafficPolicy = corev1.ServiceExternalTrafficPolicy(*service.ExternalTrafficPolicy)
Expand Down
13 changes: 13 additions & 0 deletions internal/infrastructure/kubernetes/resource/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,19 @@ func TestExpectedServiceSpec(t *testing.T) {
ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyTypeLocal,
},
},
{
name: "LoadBalancerWithHealthCheckNodePort",
args: args{service: &egv1a1.KubernetesServiceSpec{
Type: egv1a1.GetKubernetesServiceType(egv1a1.ServiceTypeLoadBalancer),
HealthCheckNodePort: new(int32(30123)),
}},
want: corev1.ServiceSpec{
Type: corev1.ServiceTypeLoadBalancer,
HealthCheckNodePort: 30123,
SessionAffinity: corev1.ServiceAffinityNone,
ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyTypeLocal,
},
},
{
name: "ClusterIP",
args: args{service: &egv1a1.KubernetesServiceSpec{
Expand Down
1 change: 1 addition & 0 deletions release-notes/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ new features: |
Added support for OpenTelemetry sampler configuration for tracing.
Added support for default EnvoyProxy settings on EnvoyGatewaySpec that can be overridden by GatewayClass or Gateway-level EnvoyProxy configurations. A new MergeType field allows choosing between Replace (default), StrategicMerge, or JSONMerge strategies for combining configurations.
Added support for sending Envoy Gateway route metadata to external authorization backends via `SecurityPolicy.spec.extAuth.includeRouteMetadata`.
Added support for specifying `healthCheckNodePort` on the Envoy Service, for use when `type: LoadBalancer` and `externalTrafficPolicy: Local`.

bug fixes: |
Fixed local rate limit rules with identical sourceCIDR client selectors producing conflicting descriptors.
Expand Down
1 change: 1 addition & 0 deletions site/content/en/latest/api/extension_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -3667,6 +3667,7 @@ _Appears in:_
| `loadBalancerSourceRanges` | _string array_ | false | | LoadBalancerSourceRanges defines a list of allowed IP addresses which will be configured as<br />firewall rules on the platform providers load balancer. This is not guaranteed to be working as<br />it happens outside of kubernetes and has to be supported and handled by the platform provider.<br />This field may only be set for services with type LoadBalancer and will be cleared if the type<br />is changed to any other type. |
| `loadBalancerIP` | _string_ | false | | LoadBalancerIP defines the IP Address of the underlying load balancer service. This field<br />may be ignored if the load balancer provider does not support this feature.<br />This field has been deprecated in Kubernetes, but it is still used for setting the IP Address in some cloud<br />providers such as GCP. |
| `externalTrafficPolicy` | _[ServiceExternalTrafficPolicy](#serviceexternaltrafficpolicy)_ | false | Local | ExternalTrafficPolicy determines the externalTrafficPolicy for the Envoy Service. Valid options<br />are Local and Cluster. Default is "Local". "Local" means traffic will only go to pods on the node<br />receiving the traffic. "Cluster" means connections are loadbalanced to all pods in the cluster. |
| `healthCheckNodePort` | _integer_ | false | | HealthCheckNodePort specifies the healthcheck nodePort for the Envoy Service.<br />This only applies when Type is set to LoadBalancer and ExternalTrafficPolicy is set to Local.<br />If a value is specified, is in-range, and is not in use, it will be used. If not specified,<br />a value will be automatically allocated by the Kubernetes API server. External systems<br />(e.g. load-balancers) can use this port to determine if a given node holds endpoints for<br />this service or not. Specifying the value allows network policies to allowlist a known port<br />rather than a randomly allocated one. |
| `patch` | _[KubernetesPatchSpec](#kubernetespatchspec)_ | false | | Patch defines how to perform the patch operation to the service |
| `name` | _string_ | false | | Name of the service.<br />When unset, this defaults to an autogenerated name. |

Expand Down
71 changes: 71 additions & 0 deletions test/cel-validation/envoyproxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/utils/ptr"
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"

egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
Expand Down Expand Up @@ -171,6 +172,76 @@ func TestEnvoyProxyProvider(t *testing.T) {
},
wantErrors: []string{"loadBalancerSourceRanges can only be set for LoadBalancer type"},
},
{
desc: "healthCheckNodePort-pass-with-default-trafficPolicy",
mutate: func(envoy *egv1a1.EnvoyProxy) {
envoy.Spec = egv1a1.EnvoyProxySpec{
Provider: &egv1a1.EnvoyProxyProvider{
Type: egv1a1.EnvoyProxyProviderTypeKubernetes,
Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{
EnvoyService: &egv1a1.KubernetesServiceSpec{
Type: new(egv1a1.ServiceTypeLoadBalancer),
HealthCheckNodePort: ptr.To[int32](30123),
},
},
},
}
},
wantErrors: []string{},
},
{
desc: "healthCheckNodePort-pass-with-explicit-Local",
mutate: func(envoy *egv1a1.EnvoyProxy) {
envoy.Spec = egv1a1.EnvoyProxySpec{
Provider: &egv1a1.EnvoyProxyProvider{
Type: egv1a1.EnvoyProxyProviderTypeKubernetes,
Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{
EnvoyService: &egv1a1.KubernetesServiceSpec{
Type: new(egv1a1.ServiceTypeLoadBalancer),
ExternalTrafficPolicy: ptr.To(egv1a1.ServiceExternalTrafficPolicyLocal),
HealthCheckNodePort: ptr.To[int32](30123),
},
},
},
}
},
wantErrors: []string{},
},
{
desc: "healthCheckNodePort-fail-non-LoadBalancer",
mutate: func(envoy *egv1a1.EnvoyProxy) {
envoy.Spec = egv1a1.EnvoyProxySpec{
Provider: &egv1a1.EnvoyProxyProvider{
Type: egv1a1.EnvoyProxyProviderTypeKubernetes,
Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{
EnvoyService: &egv1a1.KubernetesServiceSpec{
Type: new(egv1a1.ServiceTypeClusterIP),
HealthCheckNodePort: ptr.To[int32](30123),
},
},
},
}
},
wantErrors: []string{"healthCheckNodePort can only be set for LoadBalancer type with Local externalTrafficPolicy"},
},
{
desc: "healthCheckNodePort-fail-Cluster-trafficPolicy",
mutate: func(envoy *egv1a1.EnvoyProxy) {
envoy.Spec = egv1a1.EnvoyProxySpec{
Provider: &egv1a1.EnvoyProxyProvider{
Type: egv1a1.EnvoyProxyProviderTypeKubernetes,
Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{
EnvoyService: &egv1a1.KubernetesServiceSpec{
Type: new(egv1a1.ServiceTypeLoadBalancer),
ExternalTrafficPolicy: ptr.To(egv1a1.ServiceExternalTrafficPolicyCluster),
HealthCheckNodePort: ptr.To[int32](30123),
},
},
},
}
},
wantErrors: []string{"healthCheckNodePort can only be set for LoadBalancer type with Local externalTrafficPolicy"},
},
{
desc: "ServiceTypeLoadBalancer-with-valid-IP",
mutate: func(envoy *egv1a1.EnvoyProxy) {
Expand Down
18 changes: 18 additions & 0 deletions test/helm/gateway-crds-helm/all.out.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42007,6 +42007,19 @@ spec:
- Local
- Cluster
type: string
healthCheckNodePort:
description: |-
HealthCheckNodePort specifies the healthcheck nodePort for the Envoy Service.
This only applies when Type is set to LoadBalancer and ExternalTrafficPolicy is set to Local.
If a value is specified, is in-range, and is not in use, it will be used. If not specified,
a value will be automatically allocated by the Kubernetes API server. External systems
(e.g. load-balancers) can use this port to determine if a given node holds endpoints for
this service or not. Specifying the value allows network policies to allowlist a known port
rather than a randomly allocated one.
format: int32
maximum: 65535
minimum: 1
type: integer
labels:
additionalProperties:
type: string
Expand Down Expand Up @@ -42087,6 +42100,11 @@ spec:
- message: loadBalancerIP can only be set for LoadBalancer
type
rule: '!has(self.loadBalancerIP) || self.type == ''LoadBalancer'''
- message: healthCheckNodePort can only be set for LoadBalancer
type with Local externalTrafficPolicy
rule: '!has(self.healthCheckNodePort) || (self.type == ''LoadBalancer''
&& (!has(self.externalTrafficPolicy) || self.externalTrafficPolicy
== ''Local''))'
envoyServiceAccount:
description: EnvoyServiceAccount defines the desired state
of the Envoy service account resource.
Expand Down
Loading