diff --git a/api/v1alpha1/shared_types.go b/api/v1alpha1/shared_types.go index 7d8128df3c6..cce1eaccd79 100644 --- a/api/v1alpha1/shared_types.go +++ b/api/v1alpha1/shared_types.go @@ -220,6 +220,7 @@ const ( // KubernetesServiceSpec defines the desired state of the Kubernetes service resource. // +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'" type KubernetesServiceSpec struct { // Annotations that should be appended to the service. @@ -250,6 +251,14 @@ type KubernetesServiceSpec struct { // +optional AllocateLoadBalancerNodePorts *bool `json:"allocateLoadBalancerNodePorts,omitempty"` + // LoadBalancerSourceRanges defines a list of allowed IP addresses which will be configured as + // firewall rules on the platform providers load balancer. This is not guaranteed to be working as + // it happens outside of kubernetes and has to be supported and handled by the platform provider. + // This field may only be set for services with type LoadBalancer and will be cleared if the type + // is changed to any other type. + // +optional + LoadBalancerSourceRanges []string `json:"loadBalancerSourceRanges,omitempty"` + // LoadBalancerIP defines the IP Address of the underlying load balancer service. This field // may be ignored if the load balancer provider does not support this feature. // This field has been deprecated in Kubernetes, but it is still used for setting the IP Address in some cloud diff --git a/api/v1alpha1/validation/envoyproxy_validate.go b/api/v1alpha1/validation/envoyproxy_validate.go index bb880891e2c..b885d8fadfc 100644 --- a/api/v1alpha1/validation/envoyproxy_validate.go +++ b/api/v1alpha1/validation/envoyproxy_validate.go @@ -8,6 +8,7 @@ package validation import ( "errors" "fmt" + "net" "net/netip" bootstrapv3 "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v3" @@ -115,6 +116,18 @@ func validateService(spec *egv1a1.EnvoyProxySpec) []error { errs = append(errs, fmt.Errorf("allocateLoadBalancerNodePorts can only be set for %v type", egv1a1.ServiceTypeLoadBalancer)) } } + if serviceType, serviceLoadBalancerSourceRanges := + spec.Provider.Kubernetes.EnvoyService.Type, spec.Provider.Kubernetes.EnvoyService.LoadBalancerSourceRanges; serviceType != nil && serviceLoadBalancerSourceRanges != nil { + if *serviceType != egv1a1.ServiceTypeLoadBalancer { + errs = append(errs, fmt.Errorf("loadBalancerSourceRanges can only be set for %v type", egv1a1.ServiceTypeLoadBalancer)) + } + + for _, serviceLoadBalancerSourceRange := range serviceLoadBalancerSourceRanges { + if ip, _, err := net.ParseCIDR(serviceLoadBalancerSourceRange); err != nil || ip.To4() == nil { + errs = append(errs, fmt.Errorf("loadBalancerSourceRange:%s is an invalid IPv4 subnet", serviceLoadBalancerSourceRange)) + } + } + } if serviceType, serviceLoadBalancerIP := spec.Provider.Kubernetes.EnvoyService.Type, spec.Provider.Kubernetes.EnvoyService.LoadBalancerIP; serviceType != nil && serviceLoadBalancerIP != nil { if *serviceType != egv1a1.ServiceTypeLoadBalancer { errs = append(errs, fmt.Errorf("loadBalancerIP can only be set for %v type", egv1a1.ServiceTypeLoadBalancer)) diff --git a/api/v1alpha1/validation/envoyproxy_validate_test.go b/api/v1alpha1/validation/envoyproxy_validate_test.go index 54342d73579..27ab08f3091 100644 --- a/api/v1alpha1/validation/envoyproxy_validate_test.go +++ b/api/v1alpha1/validation/envoyproxy_validate_test.go @@ -213,6 +213,49 @@ func TestValidateEnvoyProxy(t *testing.T) { }, expected: false, }, + + { + name: "envoy service type 'LoadBalancer' with loadBalancerSourceRanges", + proxy: &egv1a1.EnvoyProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.EnvoyProxySpec{ + Provider: &egv1a1.EnvoyProxyProvider{ + Type: egv1a1.ProviderTypeKubernetes, + Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{ + EnvoyService: &egv1a1.KubernetesServiceSpec{ + Type: egv1a1.GetKubernetesServiceType(egv1a1.ServiceTypeLoadBalancer), + LoadBalancerSourceRanges: []string{"1.1.1.1/32"}, + }, + }, + }, + }, + }, + expected: true, + }, + { + name: "non envoy service type 'LoadBalancer' with loadBalancerSourceRanges", + proxy: &egv1a1.EnvoyProxy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.EnvoyProxySpec{ + Provider: &egv1a1.EnvoyProxyProvider{ + Type: egv1a1.ProviderTypeKubernetes, + Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{ + EnvoyService: &egv1a1.KubernetesServiceSpec{ + Type: egv1a1.GetKubernetesServiceType(egv1a1.ServiceTypeClusterIP), + LoadBalancerSourceRanges: []string{"1.1.1.1/32"}, + }, + }, + }, + }, + }, + expected: false, + }, { name: "envoy service type 'LoadBalancer' with valid loadBalancerIP", proxy: &egv1a1.EnvoyProxy{ diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 0055d7669f1..93cf3857057 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -2570,6 +2570,11 @@ func (in *KubernetesServiceSpec) DeepCopyInto(out *KubernetesServiceSpec) { *out = new(bool) **out = **in } + if in.LoadBalancerSourceRanges != nil { + in, out := &in.LoadBalancerSourceRanges, &out.LoadBalancerSourceRanges + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.LoadBalancerIP != nil { in, out := &in.LoadBalancerIP, &out.LoadBalancerIP *out = new(string) diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml index 76d96eccaea..a41ad5c713d 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml @@ -5761,6 +5761,16 @@ spec: x-kubernetes-validations: - message: loadBalancerIP must be a valid IPv4 address rule: self.matches(r"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$") + loadBalancerSourceRanges: + description: |- + LoadBalancerSourceRanges defines a list of allowed IP addresses which will be configured as + firewall rules on the platform providers load balancer. This is not guaranteed to be working as + it happens outside of kubernetes and has to be supported and handled by the platform provider. + This field may only be set for services with type LoadBalancer and will be cleared if the type + is changed to any other type. + items: + type: string + type: array patch: description: Patch defines how to perform the patch operation to the service @@ -5798,6 +5808,10 @@ spec: LoadBalancer type rule: '!has(self.allocateLoadBalancerNodePorts) || self.type == ''LoadBalancer''' + - message: loadBalancerSourceRanges can only be set for LoadBalancer + type + rule: '!has(self.loadBalancerSourceRanges) || self.type + == ''LoadBalancer''' - message: loadBalancerIP can only be set for LoadBalancer type rule: '!has(self.loadBalancerIP) || self.type == ''LoadBalancer''' diff --git a/internal/infrastructure/kubernetes/resource/resource.go b/internal/infrastructure/kubernetes/resource/resource.go index bec8257071d..bae4245f22a 100644 --- a/internal/infrastructure/kubernetes/resource/resource.go +++ b/internal/infrastructure/kubernetes/resource/resource.go @@ -37,6 +37,9 @@ func ExpectedServiceSpec(service *egv1a1.KubernetesServiceSpec) corev1.ServiceSp if service.AllocateLoadBalancerNodePorts != nil { serviceSpec.AllocateLoadBalancerNodePorts = service.AllocateLoadBalancerNodePorts } + if service.LoadBalancerSourceRanges != nil && len(service.LoadBalancerSourceRanges) > 0 { + serviceSpec.LoadBalancerSourceRanges = service.LoadBalancerSourceRanges + } if service.LoadBalancerIP != nil { serviceSpec.LoadBalancerIP = *service.LoadBalancerIP } diff --git a/internal/infrastructure/kubernetes/resource/resource_test.go b/internal/infrastructure/kubernetes/resource/resource_test.go index 9eb71ce4ced..590a699df51 100644 --- a/internal/infrastructure/kubernetes/resource/resource_test.go +++ b/internal/infrastructure/kubernetes/resource/resource_test.go @@ -77,6 +77,19 @@ func TestExpectedServiceSpec(t *testing.T) { ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyTypeLocal, }, }, + { + name: "LoadBalancerWithLoadBalancerSourceRanges", + args: args{service: &egv1a1.KubernetesServiceSpec{ + Type: egv1a1.GetKubernetesServiceType(egv1a1.ServiceTypeLoadBalancer), + LoadBalancerSourceRanges: []string{"1.1.1.1/32"}, + }}, + want: corev1.ServiceSpec{ + Type: corev1.ServiceTypeLoadBalancer, + LoadBalancerSourceRanges: []string{"1.1.1.1/32"}, + SessionAffinity: corev1.ServiceAffinityNone, + ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyTypeLocal, + }, + }, { name: "LoadBalancerWithLoadBalancerIP", args: args{service: &egv1a1.KubernetesServiceSpec{ diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index a9847ac0645..1d290f667e9 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -1805,6 +1805,7 @@ _Appears in:_ | `type` | _[ServiceType](#servicetype)_ | false | Type determines how the Service is exposed. Defaults to LoadBalancer.
Valid options are ClusterIP, LoadBalancer and NodePort.
"LoadBalancer" means a service will be exposed via an external load balancer (if the cloud provider supports it).
"ClusterIP" means a service will only be accessible inside the cluster, via the cluster IP.
"NodePort" means a service will be exposed on a static Port on all Nodes of the cluster. | | `loadBalancerClass` | _string_ | false | LoadBalancerClass, when specified, allows for choosing the LoadBalancer provider
implementation if more than one are available or is otherwise expected to be specified | | `allocateLoadBalancerNodePorts` | _boolean_ | false | AllocateLoadBalancerNodePorts defines if NodePorts will be automatically allocated for
services with type LoadBalancer. Default is "true". It may be set to "false" if the cluster
load-balancer does not rely on NodePorts. If the caller requests specific NodePorts (by specifying a
value), those requests will be respected, regardless of this field. This field may only be set for
services with type LoadBalancer and will be cleared if the type is changed to any other type. | +| `loadBalancerSourceRanges` | _string array_ | false | LoadBalancerSourceRanges defines a list of allowed IP addresses which will be configured as
firewall rules on the platform providers load balancer. This is not guaranteed to be working as
it happens outside of kubernetes and has to be supported and handled by the platform provider.
This field may only be set for services with type LoadBalancer and will be cleared if the type
is changed to any other type. | | `loadBalancerIP` | _string_ | false | LoadBalancerIP defines the IP Address of the underlying load balancer service. This field
may be ignored if the load balancer provider does not support this feature.
This field has been deprecated in Kubernetes, but it is still used for setting the IP Address in some cloud
providers such as GCP. | | `externalTrafficPolicy` | _[ServiceExternalTrafficPolicy](#serviceexternaltrafficpolicy)_ | false | ExternalTrafficPolicy determines the externalTrafficPolicy for the Envoy Service. Valid options
are Local and Cluster. Default is "Local". "Local" means traffic will only go to pods on the node
receiving the traffic. "Cluster" means connections are loadbalanced to all pods in the cluster. | | `patch` | _[KubernetesPatchSpec](#kubernetespatchspec)_ | false | Patch defines how to perform the patch operation to the service | diff --git a/test/cel-validation/envoyproxy_test.go b/test/cel-validation/envoyproxy_test.go index 3b2785f713d..1d7131369f2 100644 --- a/test/cel-validation/envoyproxy_test.go +++ b/test/cel-validation/envoyproxy_test.go @@ -120,6 +120,56 @@ func TestEnvoyProxyProvider(t *testing.T) { }, wantErrors: []string{"allocateLoadBalancerNodePorts can only be set for LoadBalancer type"}, }, + { + desc: "loadBalancerSourceRanges-pass-case1", + mutate: func(envoy *egv1a1.EnvoyProxy) { + envoy.Spec = egv1a1.EnvoyProxySpec{ + Provider: &egv1a1.EnvoyProxyProvider{ + Type: egv1a1.ProviderTypeKubernetes, + Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{ + EnvoyService: &egv1a1.KubernetesServiceSpec{ + Type: ptr.To(egv1a1.ServiceTypeLoadBalancer), + LoadBalancerSourceRanges: []string{"1.1.1.1"}, + }, + }, + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "loadBalancerSourceRanges-pass-case2", + mutate: func(envoy *egv1a1.EnvoyProxy) { + envoy.Spec = egv1a1.EnvoyProxySpec{ + Provider: &egv1a1.EnvoyProxyProvider{ + Type: egv1a1.ProviderTypeKubernetes, + Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{ + EnvoyService: &egv1a1.KubernetesServiceSpec{ + Type: ptr.To(egv1a1.ServiceTypeClusterIP), + }, + }, + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "loadBalancerSourceRanges-fail", + mutate: func(envoy *egv1a1.EnvoyProxy) { + envoy.Spec = egv1a1.EnvoyProxySpec{ + Provider: &egv1a1.EnvoyProxyProvider{ + Type: egv1a1.ProviderTypeKubernetes, + Kubernetes: &egv1a1.EnvoyProxyKubernetesProvider{ + EnvoyService: &egv1a1.KubernetesServiceSpec{ + Type: ptr.To(egv1a1.ServiceTypeClusterIP), + LoadBalancerSourceRanges: []string{"1.1.1.1"}, + }, + }, + }, + } + }, + wantErrors: []string{"loadBalancerSourceRanges can only be set for LoadBalancer type"}, + }, { desc: "ServiceTypeLoadBalancer-with-valid-IP", mutate: func(envoy *egv1a1.EnvoyProxy) {