mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-05 18:24:07 +00:00
Merge pull request #119937 from RyanAoh/kep-1860-dev
Make Kubernetes aware of the LoadBalancer behaviour
This commit is contained in:
commit
ee265c92fe
4
api/openapi-spec/swagger.json
generated
4
api/openapi-spec/swagger.json
generated
@ -7153,6 +7153,10 @@
|
||||
"description": "IP is set for load-balancer ingress points that are IP based (typically GCE or OpenStack load-balancers)",
|
||||
"type": "string"
|
||||
},
|
||||
"ipMode": {
|
||||
"description": "IPMode specifies how the load-balancer IP behaves, and may only be specified when the ip field is specified. Setting this to \"VIP\" indicates that traffic is delivered to the node with the destination set to the load-balancer's IP and port. Setting this to \"Proxy\" indicates that traffic is delivered to the node or pod with the destination set to the node's IP and node port or the pod's IP and port. Service implementations may use this information to adjust traffic routing.",
|
||||
"type": "string"
|
||||
},
|
||||
"ports": {
|
||||
"description": "Ports is a list of records of service ports If used, every port defined in the service should have an entry in it",
|
||||
"items": {
|
||||
|
@ -3102,6 +3102,10 @@
|
||||
"description": "IP is set for load-balancer ingress points that are IP based (typically GCE or OpenStack load-balancers)",
|
||||
"type": "string"
|
||||
},
|
||||
"ipMode": {
|
||||
"description": "IPMode specifies how the load-balancer IP behaves, and may only be specified when the ip field is specified. Setting this to \"VIP\" indicates that traffic is delivered to the node with the destination set to the load-balancer's IP and port. Setting this to \"Proxy\" indicates that traffic is delivered to the node or pod with the destination set to the node's IP and node port or the pod's IP and port. Service implementations may use this information to adjust traffic routing.",
|
||||
"type": "string"
|
||||
},
|
||||
"ports": {
|
||||
"description": "Ports is a list of records of service ports If used, every port defined in the service should have an entry in it",
|
||||
"items": {
|
||||
|
@ -4074,6 +4074,15 @@ type LoadBalancerIngress struct {
|
||||
// +optional
|
||||
Hostname string
|
||||
|
||||
// IPMode specifies how the load-balancer IP behaves, and may only be specified when the ip field is specified.
|
||||
// Setting this to "VIP" indicates that traffic is delivered to the node with
|
||||
// the destination set to the load-balancer's IP and port.
|
||||
// Setting this to "Proxy" indicates that traffic is delivered to the node or pod with
|
||||
// the destination set to the node's IP and node port or the pod's IP and port.
|
||||
// Service implementations may use this information to adjust traffic routing.
|
||||
// +optional
|
||||
IPMode *LoadBalancerIPMode
|
||||
|
||||
// Ports is a list of records of service ports
|
||||
// If used, every port defined in the service should have an entry in it
|
||||
// +optional
|
||||
@ -6146,3 +6155,15 @@ type PortStatus struct {
|
||||
// +kubebuilder:validation:MaxLength=316
|
||||
Error *string
|
||||
}
|
||||
|
||||
// LoadBalancerIPMode represents the mode of the LoadBalancer ingress IP
|
||||
type LoadBalancerIPMode string
|
||||
|
||||
const (
|
||||
// LoadBalancerIPModeVIP indicates that traffic is delivered to the node with
|
||||
// the destination set to the load-balancer's IP and port.
|
||||
LoadBalancerIPModeVIP LoadBalancerIPMode = "VIP"
|
||||
// LoadBalancerIPModeProxy indicates that traffic is delivered to the node or pod with
|
||||
// the destination set to the node's IP and port or the pod's IP and port.
|
||||
LoadBalancerIPModeProxy LoadBalancerIPMode = "Proxy"
|
||||
)
|
||||
|
@ -142,6 +142,19 @@ func SetDefaults_Service(obj *v1.Service) {
|
||||
obj.Spec.AllocateLoadBalancerNodePorts = pointer.Bool(true)
|
||||
}
|
||||
}
|
||||
|
||||
if obj.Spec.Type == v1.ServiceTypeLoadBalancer {
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.LoadBalancerIPMode) {
|
||||
ipMode := v1.LoadBalancerIPModeVIP
|
||||
|
||||
for i, ing := range obj.Status.LoadBalancer.Ingress {
|
||||
if ing.IP != "" && ing.IPMode == nil {
|
||||
obj.Status.LoadBalancer.Ingress[i].IPMode = &ipMode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
func SetDefaults_Pod(obj *v1.Pod) {
|
||||
// If limits are specified, but requests are not, default requests to limits
|
||||
|
@ -1221,6 +1221,74 @@ func TestSetDefaultServiceSessionAffinityConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDefaultServiceLoadbalancerIPMode(t *testing.T) {
|
||||
modeVIP := v1.LoadBalancerIPModeVIP
|
||||
modeProxy := v1.LoadBalancerIPModeProxy
|
||||
testCases := []struct {
|
||||
name string
|
||||
ipModeEnabled bool
|
||||
svc *v1.Service
|
||||
expectedIPMode []*v1.LoadBalancerIPMode
|
||||
}{
|
||||
{
|
||||
name: "Set IP but not set IPMode with LoadbalancerIPMode disabled",
|
||||
ipModeEnabled: false,
|
||||
svc: &v1.Service{
|
||||
Spec: v1.ServiceSpec{Type: v1.ServiceTypeLoadBalancer},
|
||||
Status: v1.ServiceStatus{
|
||||
LoadBalancer: v1.LoadBalancerStatus{
|
||||
Ingress: []v1.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
},
|
||||
}},
|
||||
expectedIPMode: []*v1.LoadBalancerIPMode{nil},
|
||||
}, {
|
||||
name: "Set IP but bot set IPMode with LoadbalancerIPMode enabled",
|
||||
ipModeEnabled: true,
|
||||
svc: &v1.Service{
|
||||
Spec: v1.ServiceSpec{Type: v1.ServiceTypeLoadBalancer},
|
||||
Status: v1.ServiceStatus{
|
||||
LoadBalancer: v1.LoadBalancerStatus{
|
||||
Ingress: []v1.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
},
|
||||
}},
|
||||
expectedIPMode: []*v1.LoadBalancerIPMode{&modeVIP},
|
||||
}, {
|
||||
name: "Both IP and IPMode are set with LoadbalancerIPMode enabled",
|
||||
ipModeEnabled: true,
|
||||
svc: &v1.Service{
|
||||
Spec: v1.ServiceSpec{Type: v1.ServiceTypeLoadBalancer},
|
||||
Status: v1.ServiceStatus{
|
||||
LoadBalancer: v1.LoadBalancerStatus{
|
||||
Ingress: []v1.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &modeProxy,
|
||||
}},
|
||||
},
|
||||
}},
|
||||
expectedIPMode: []*v1.LoadBalancerIPMode{&modeProxy},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.LoadBalancerIPMode, tc.ipModeEnabled)()
|
||||
obj := roundTrip(t, runtime.Object(tc.svc))
|
||||
svc := obj.(*v1.Service)
|
||||
for i, s := range svc.Status.LoadBalancer.Ingress {
|
||||
got := s.IPMode
|
||||
expected := tc.expectedIPMode[i]
|
||||
if !reflect.DeepEqual(got, expected) {
|
||||
t.Errorf("Expected IPMode %v, got %v", tc.expectedIPMode[i], s.IPMode)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDefaultSecretVolumeSource(t *testing.T) {
|
||||
s := v1.PodSpec{}
|
||||
s.Volumes = []v1.Volume{
|
||||
|
2
pkg/apis/core/v1/zz_generated.conversion.go
generated
2
pkg/apis/core/v1/zz_generated.conversion.go
generated
@ -4478,6 +4478,7 @@ func Convert_core_List_To_v1_List(in *core.List, out *v1.List, s conversion.Scop
|
||||
func autoConvert_v1_LoadBalancerIngress_To_core_LoadBalancerIngress(in *v1.LoadBalancerIngress, out *core.LoadBalancerIngress, s conversion.Scope) error {
|
||||
out.IP = in.IP
|
||||
out.Hostname = in.Hostname
|
||||
out.IPMode = (*core.LoadBalancerIPMode)(unsafe.Pointer(in.IPMode))
|
||||
out.Ports = *(*[]core.PortStatus)(unsafe.Pointer(&in.Ports))
|
||||
return nil
|
||||
}
|
||||
@ -4490,6 +4491,7 @@ func Convert_v1_LoadBalancerIngress_To_core_LoadBalancerIngress(in *v1.LoadBalan
|
||||
func autoConvert_core_LoadBalancerIngress_To_v1_LoadBalancerIngress(in *core.LoadBalancerIngress, out *v1.LoadBalancerIngress, s conversion.Scope) error {
|
||||
out.IP = in.IP
|
||||
out.Hostname = in.Hostname
|
||||
out.IPMode = (*v1.LoadBalancerIPMode)(unsafe.Pointer(in.IPMode))
|
||||
out.Ports = *(*[]v1.PortStatus)(unsafe.Pointer(&in.Ports))
|
||||
return nil
|
||||
}
|
||||
|
@ -7048,6 +7048,10 @@ func ValidatePodLogOptions(opts *core.PodLogOptions) field.ErrorList {
|
||||
return allErrs
|
||||
}
|
||||
|
||||
var (
|
||||
supportedLoadBalancerIPMode = sets.NewString(string(core.LoadBalancerIPModeVIP), string(core.LoadBalancerIPModeProxy))
|
||||
)
|
||||
|
||||
// ValidateLoadBalancerStatus validates required fields on a LoadBalancerStatus
|
||||
func ValidateLoadBalancerStatus(status *core.LoadBalancerStatus, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
@ -7058,6 +7062,17 @@ func ValidateLoadBalancerStatus(status *core.LoadBalancerStatus, fldPath *field.
|
||||
allErrs = append(allErrs, field.Invalid(idxPath.Child("ip"), ingress.IP, "must be a valid IP address"))
|
||||
}
|
||||
}
|
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.LoadBalancerIPMode) && ingress.IPMode == nil {
|
||||
if len(ingress.IP) > 0 {
|
||||
allErrs = append(allErrs, field.Required(idxPath.Child("ipMode"), "must be specified when `ip` is set"))
|
||||
}
|
||||
} else if ingress.IPMode != nil && len(ingress.IP) == 0 {
|
||||
allErrs = append(allErrs, field.Forbidden(idxPath.Child("ipMode"), "may not be specified when `ip` is not set"))
|
||||
} else if ingress.IPMode != nil && !supportedLoadBalancerIPMode.Has(string(*ingress.IPMode)) {
|
||||
allErrs = append(allErrs, field.NotSupported(idxPath.Child("ipMode"), ingress.IPMode, supportedLoadBalancerIPMode.List()))
|
||||
}
|
||||
|
||||
if len(ingress.Hostname) > 0 {
|
||||
for _, msg := range validation.IsDNS1123Subdomain(ingress.Hostname) {
|
||||
allErrs = append(allErrs, field.Invalid(idxPath.Child("hostname"), ingress.Hostname, msg))
|
||||
|
@ -23368,3 +23368,87 @@ func TestValidateDynamicResourceAllocation(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateLoadBalancerStatus(t *testing.T) {
|
||||
ipModeVIP := core.LoadBalancerIPModeVIP
|
||||
ipModeProxy := core.LoadBalancerIPModeProxy
|
||||
ipModeDummy := core.LoadBalancerIPMode("dummy")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
ipModeEnabled bool
|
||||
tweakLBStatus func(s *core.LoadBalancerStatus)
|
||||
numErrs int
|
||||
}{
|
||||
/* LoadBalancerIPMode*/
|
||||
{
|
||||
name: "valid vip ipMode",
|
||||
ipModeEnabled: true,
|
||||
tweakLBStatus: func(s *core.LoadBalancerStatus) {
|
||||
s.Ingress = []core.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeVIP,
|
||||
}}
|
||||
},
|
||||
numErrs: 0,
|
||||
}, {
|
||||
name: "valid proxy ipMode",
|
||||
ipModeEnabled: true,
|
||||
tweakLBStatus: func(s *core.LoadBalancerStatus) {
|
||||
s.Ingress = []core.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeProxy,
|
||||
}}
|
||||
},
|
||||
numErrs: 0,
|
||||
}, {
|
||||
name: "invalid ipMode",
|
||||
ipModeEnabled: true,
|
||||
tweakLBStatus: func(s *core.LoadBalancerStatus) {
|
||||
s.Ingress = []core.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeDummy,
|
||||
}}
|
||||
},
|
||||
numErrs: 1,
|
||||
}, {
|
||||
name: "missing ipMode with LoadbalancerIPMode enabled",
|
||||
ipModeEnabled: true,
|
||||
tweakLBStatus: func(s *core.LoadBalancerStatus) {
|
||||
s.Ingress = []core.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}}
|
||||
},
|
||||
numErrs: 1,
|
||||
}, {
|
||||
name: "missing ipMode with LoadbalancerIPMode disabled",
|
||||
ipModeEnabled: false,
|
||||
tweakLBStatus: func(s *core.LoadBalancerStatus) {
|
||||
s.Ingress = []core.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}}
|
||||
},
|
||||
numErrs: 0,
|
||||
}, {
|
||||
name: "missing ip with ipMode present",
|
||||
ipModeEnabled: true,
|
||||
tweakLBStatus: func(s *core.LoadBalancerStatus) {
|
||||
s.Ingress = []core.LoadBalancerIngress{{
|
||||
IPMode: &ipModeProxy,
|
||||
}}
|
||||
},
|
||||
numErrs: 1,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.LoadBalancerIPMode, tc.ipModeEnabled)()
|
||||
s := core.LoadBalancerStatus{}
|
||||
tc.tweakLBStatus(&s)
|
||||
errs := ValidateLoadBalancerStatus(&s, field.NewPath("status"))
|
||||
if len(errs) != tc.numErrs {
|
||||
t.Errorf("Unexpected error list for case %q(expected:%v got %v) - Errors:\n %v", tc.name, tc.numErrs, len(errs), errs.ToAggregate())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
5
pkg/apis/core/zz_generated.deepcopy.go
generated
5
pkg/apis/core/zz_generated.deepcopy.go
generated
@ -2230,6 +2230,11 @@ func (in *List) DeepCopyObject() runtime.Object {
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *LoadBalancerIngress) DeepCopyInto(out *LoadBalancerIngress) {
|
||||
*out = *in
|
||||
if in.IPMode != nil {
|
||||
in, out := &in.IPMode, &out.IPMode
|
||||
*out = new(LoadBalancerIPMode)
|
||||
**out = **in
|
||||
}
|
||||
if in.Ports != nil {
|
||||
in, out := &in.Ports, &out.Ports
|
||||
*out = make([]PortStatus, len(*in))
|
||||
|
@ -934,6 +934,12 @@ const (
|
||||
//
|
||||
// Enables In-Place Pod Vertical Scaling
|
||||
InPlacePodVerticalScaling featuregate.Feature = "InPlacePodVerticalScaling"
|
||||
|
||||
// owner: @Sh4d1,@RyanAoh
|
||||
// kep: http://kep.k8s.io/1860
|
||||
// alpha: v1.29
|
||||
// LoadBalancerIPMode enables the IPMode field in the LoadBalancerIngress status of a Service
|
||||
LoadBalancerIPMode featuregate.Feature = "LoadBalancerIPMode"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -1185,6 +1191,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
|
||||
|
||||
PodIndexLabel: {Default: true, PreRelease: featuregate.Beta},
|
||||
|
||||
LoadBalancerIPMode: {Default: false, PreRelease: featuregate.Alpha},
|
||||
|
||||
// inherited features from generic apiserver, relisted here to get a conflict if it is changed
|
||||
// unintentionally on either side:
|
||||
|
||||
|
7
pkg/generated/openapi/zz_generated.openapi.go
generated
7
pkg/generated/openapi/zz_generated.openapi.go
generated
@ -21756,6 +21756,13 @@ func schema_k8sio_api_core_v1_LoadBalancerIngress(ref common.ReferenceCallback)
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"ipMode": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "IPMode specifies how the load-balancer IP behaves, and may only be specified when the ip field is specified. Setting this to \"VIP\" indicates that traffic is delivered to the node with the destination set to the load-balancer's IP and port. Setting this to \"Proxy\" indicates that traffic is delivered to the node or pod with the destination set to the node's IP and node port or the pod's IP and port. Service implementations may use this information to adjust traffic routing.",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"ports": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
|
@ -50,7 +50,7 @@ func deleteStaleServiceConntrackEntries(isIPv6 bool, exec utilexec.Interface, sv
|
||||
for _, extIP := range svcInfo.ExternalIPStrings() {
|
||||
conntrackCleanupServiceIPs.Insert(extIP)
|
||||
}
|
||||
for _, lbIP := range svcInfo.LoadBalancerIPStrings() {
|
||||
for _, lbIP := range svcInfo.LoadBalancerVIPStrings() {
|
||||
conntrackCleanupServiceIPs.Insert(lbIP)
|
||||
}
|
||||
nodePort := svcInfo.NodePort()
|
||||
@ -100,7 +100,7 @@ func deleteStaleEndpointConntrackEntries(exec utilexec.Interface, svcPortMap pro
|
||||
klog.ErrorS(err, "Failed to delete endpoint connections for externalIP", "servicePortName", epSvcPair.ServicePortName, "externalIP", extIP)
|
||||
}
|
||||
}
|
||||
for _, lbIP := range svcInfo.LoadBalancerIPStrings() {
|
||||
for _, lbIP := range svcInfo.LoadBalancerVIPStrings() {
|
||||
err := ClearEntriesForNAT(exec, lbIP, endpointIP, v1.ProtocolUDP)
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "Failed to delete endpoint connections for LoadBalancerIP", "servicePortName", epSvcPair.ServicePortName, "loadBalancerIP", lbIP)
|
||||
|
@ -1024,7 +1024,7 @@ func (proxier *Proxier) syncProxyRules() {
|
||||
// create a firewall chain.
|
||||
loadBalancerTrafficChain := externalTrafficChain
|
||||
fwChain := svcInfo.firewallChainName
|
||||
usesFWChain := hasEndpoints && len(svcInfo.LoadBalancerIPStrings()) > 0 && len(svcInfo.LoadBalancerSourceRanges()) > 0
|
||||
usesFWChain := hasEndpoints && len(svcInfo.LoadBalancerVIPStrings()) > 0 && len(svcInfo.LoadBalancerSourceRanges()) > 0
|
||||
if usesFWChain {
|
||||
activeNATChains[fwChain] = true
|
||||
loadBalancerTrafficChain = fwChain
|
||||
@ -1116,7 +1116,7 @@ func (proxier *Proxier) syncProxyRules() {
|
||||
}
|
||||
|
||||
// Capture load-balancer ingress.
|
||||
for _, lbip := range svcInfo.LoadBalancerIPStrings() {
|
||||
for _, lbip := range svcInfo.LoadBalancerVIPStrings() {
|
||||
if hasEndpoints {
|
||||
natRules.Write(
|
||||
"-A", string(kubeServicesChain),
|
||||
@ -1141,7 +1141,7 @@ func (proxier *Proxier) syncProxyRules() {
|
||||
// Either no endpoints at all (REJECT) or no endpoints for
|
||||
// external traffic (DROP anything that didn't get short-circuited
|
||||
// by the EXT chain.)
|
||||
for _, lbip := range svcInfo.LoadBalancerIPStrings() {
|
||||
for _, lbip := range svcInfo.LoadBalancerVIPStrings() {
|
||||
filterRules.Write(
|
||||
"-A", string(kubeExternalServicesChain),
|
||||
"-m", "comment", "--comment", externalTrafficFilterComment,
|
||||
@ -1319,7 +1319,7 @@ func (proxier *Proxier) syncProxyRules() {
|
||||
// will loop back with the source IP set to the VIP. We
|
||||
// need the following rules to allow requests from this node.
|
||||
if allowFromNode {
|
||||
for _, lbip := range svcInfo.LoadBalancerIPStrings() {
|
||||
for _, lbip := range svcInfo.LoadBalancerVIPStrings() {
|
||||
natRules.Write(
|
||||
args,
|
||||
"-s", lbip,
|
||||
|
@ -38,8 +38,11 @@ import (
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
"k8s.io/component-base/metrics/testutil"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
"k8s.io/kubernetes/pkg/proxy"
|
||||
"k8s.io/kubernetes/pkg/proxy/conntrack"
|
||||
"k8s.io/kubernetes/pkg/proxy/metrics"
|
||||
@ -8276,3 +8279,128 @@ func TestNoEndpointsMetric(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBalancerIngressRouteTypeProxy(t *testing.T) {
|
||||
ipModeProxy := v1.LoadBalancerIPModeProxy
|
||||
ipModeVIP := v1.LoadBalancerIPModeVIP
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
ipModeEnabled bool
|
||||
svcIP string
|
||||
svcLBIP string
|
||||
ipMode *v1.LoadBalancerIPMode
|
||||
expectedRule bool
|
||||
}{
|
||||
/* LoadBalancerIPMode disabled */
|
||||
{
|
||||
name: "LoadBalancerIPMode disabled, ipMode Proxy",
|
||||
ipModeEnabled: false,
|
||||
svcIP: "10.20.30.41",
|
||||
svcLBIP: "1.2.3.4",
|
||||
ipMode: &ipModeProxy,
|
||||
expectedRule: true,
|
||||
},
|
||||
{
|
||||
name: "LoadBalancerIPMode disabled, ipMode VIP",
|
||||
ipModeEnabled: false,
|
||||
svcIP: "10.20.30.42",
|
||||
svcLBIP: "1.2.3.5",
|
||||
ipMode: &ipModeVIP,
|
||||
expectedRule: true,
|
||||
},
|
||||
{
|
||||
name: "LoadBalancerIPMode disabled, ipMode nil",
|
||||
ipModeEnabled: false,
|
||||
svcIP: "10.20.30.43",
|
||||
svcLBIP: "1.2.3.6",
|
||||
ipMode: nil,
|
||||
expectedRule: true,
|
||||
},
|
||||
/* LoadBalancerIPMode enabled */
|
||||
{
|
||||
name: "LoadBalancerIPMode enabled, ipMode Proxy",
|
||||
ipModeEnabled: true,
|
||||
svcIP: "10.20.30.41",
|
||||
svcLBIP: "1.2.3.4",
|
||||
ipMode: &ipModeProxy,
|
||||
expectedRule: false,
|
||||
},
|
||||
{
|
||||
name: "LoadBalancerIPMode enabled, ipMode VIP",
|
||||
ipModeEnabled: true,
|
||||
svcIP: "10.20.30.42",
|
||||
svcLBIP: "1.2.3.5",
|
||||
ipMode: &ipModeVIP,
|
||||
expectedRule: true,
|
||||
},
|
||||
{
|
||||
name: "LoadBalancerIPMode enabled, ipMode nil",
|
||||
ipModeEnabled: true,
|
||||
svcIP: "10.20.30.43",
|
||||
svcLBIP: "1.2.3.6",
|
||||
ipMode: nil,
|
||||
expectedRule: true,
|
||||
},
|
||||
}
|
||||
|
||||
svcPort := 80
|
||||
svcNodePort := 3001
|
||||
svcPortName := proxy.ServicePortName{
|
||||
NamespacedName: makeNSN("ns1", "svc1"),
|
||||
Port: "p80",
|
||||
Protocol: v1.ProtocolTCP,
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.LoadBalancerIPMode, testCase.ipModeEnabled)()
|
||||
ipt := iptablestest.NewFake()
|
||||
fp := NewFakeProxier(ipt)
|
||||
makeServiceMap(fp,
|
||||
makeTestService(svcPortName.Namespace, svcPortName.Name, func(svc *v1.Service) {
|
||||
svc.Spec.Type = "LoadBalancer"
|
||||
svc.Spec.ClusterIP = testCase.svcIP
|
||||
svc.Spec.Ports = []v1.ServicePort{{
|
||||
Name: svcPortName.Port,
|
||||
Port: int32(svcPort),
|
||||
Protocol: v1.ProtocolTCP,
|
||||
NodePort: int32(svcNodePort),
|
||||
}}
|
||||
svc.Status.LoadBalancer.Ingress = []v1.LoadBalancerIngress{{
|
||||
IP: testCase.svcLBIP,
|
||||
IPMode: testCase.ipMode,
|
||||
}}
|
||||
}),
|
||||
)
|
||||
|
||||
tcpProtocol := v1.ProtocolTCP
|
||||
populateEndpointSlices(fp,
|
||||
makeTestEndpointSlice("ns1", "svc1", 1, func(eps *discovery.EndpointSlice) {
|
||||
eps.AddressType = discovery.AddressTypeIPv4
|
||||
eps.Endpoints = []discovery.Endpoint{{
|
||||
Addresses: []string{"10.180.0.1"},
|
||||
}}
|
||||
eps.Ports = []discovery.EndpointPort{{
|
||||
Name: pointer.String("p80"),
|
||||
Port: pointer.Int32(80),
|
||||
Protocol: &tcpProtocol,
|
||||
}}
|
||||
}),
|
||||
)
|
||||
|
||||
fp.syncProxyRules()
|
||||
|
||||
c, _ := ipt.Dump.GetChain(utiliptables.TableNAT, kubeServicesChain)
|
||||
ruleExists := false
|
||||
for _, r := range c.Rules {
|
||||
if r.DestinationAddress != nil && r.DestinationAddress.Value == testCase.svcLBIP {
|
||||
ruleExists = true
|
||||
}
|
||||
}
|
||||
if ruleExists != testCase.expectedRule {
|
||||
t.Errorf("unexpected rule for %s", testCase.svcLBIP)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1172,7 +1172,7 @@ func (proxier *Proxier) syncProxyRules() {
|
||||
}
|
||||
|
||||
// Capture load-balancer ingress.
|
||||
for _, ingress := range svcInfo.LoadBalancerIPStrings() {
|
||||
for _, ingress := range svcInfo.LoadBalancerVIPStrings() {
|
||||
// ipset call
|
||||
entry = &utilipset.Entry{
|
||||
IP: ingress,
|
||||
|
@ -36,7 +36,10 @@ import (
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
"k8s.io/component-base/metrics/testutil"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
"k8s.io/kubernetes/pkg/proxy"
|
||||
"k8s.io/kubernetes/pkg/proxy/healthcheck"
|
||||
utilipset "k8s.io/kubernetes/pkg/proxy/ipvs/ipset"
|
||||
@ -5836,3 +5839,123 @@ func TestDismissLocalhostRuleExist(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBalancerIngressRouteTypeProxy(t *testing.T) {
|
||||
ipModeProxy := v1.LoadBalancerIPModeProxy
|
||||
ipModeVIP := v1.LoadBalancerIPModeVIP
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
ipModeEnabled bool
|
||||
svcIP string
|
||||
svcLBIP string
|
||||
ipMode *v1.LoadBalancerIPMode
|
||||
expectedServices int
|
||||
}{
|
||||
/* LoadBalancerIPMode disabled */
|
||||
{
|
||||
name: "LoadBalancerIPMode disabled, ipMode Proxy",
|
||||
ipModeEnabled: false,
|
||||
svcIP: "10.20.30.41",
|
||||
svcLBIP: "1.2.3.4",
|
||||
ipMode: &ipModeProxy,
|
||||
expectedServices: 2,
|
||||
},
|
||||
{
|
||||
name: "LoadBalancerIPMode disabled, ipMode VIP",
|
||||
ipModeEnabled: false,
|
||||
svcIP: "10.20.30.42",
|
||||
svcLBIP: "1.2.3.5",
|
||||
ipMode: &ipModeVIP,
|
||||
expectedServices: 2,
|
||||
},
|
||||
{
|
||||
name: "LoadBalancerIPMode disabled, ipMode nil",
|
||||
ipModeEnabled: false,
|
||||
svcIP: "10.20.30.43",
|
||||
svcLBIP: "1.2.3.6",
|
||||
ipMode: nil,
|
||||
expectedServices: 2,
|
||||
},
|
||||
/* LoadBalancerIPMode enabled */
|
||||
{
|
||||
name: "LoadBalancerIPMode enabled, ipMode Proxy",
|
||||
ipModeEnabled: true,
|
||||
svcIP: "10.20.30.41",
|
||||
svcLBIP: "1.2.3.4",
|
||||
ipMode: &ipModeProxy,
|
||||
expectedServices: 1,
|
||||
},
|
||||
{
|
||||
name: "LoadBalancerIPMode enabled, ipMode VIP",
|
||||
ipModeEnabled: true,
|
||||
svcIP: "10.20.30.42",
|
||||
svcLBIP: "1.2.3.5",
|
||||
ipMode: &ipModeVIP,
|
||||
expectedServices: 2,
|
||||
},
|
||||
{
|
||||
name: "LoadBalancerIPMode enabled, ipMode nil",
|
||||
ipModeEnabled: true,
|
||||
svcIP: "10.20.30.43",
|
||||
svcLBIP: "1.2.3.6",
|
||||
ipMode: nil,
|
||||
expectedServices: 2,
|
||||
},
|
||||
}
|
||||
|
||||
svcPort := 80
|
||||
svcNodePort := 3001
|
||||
svcPortName := proxy.ServicePortName{
|
||||
NamespacedName: makeNSN("ns1", "svc1"),
|
||||
Port: "p80",
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.LoadBalancerIPMode, testCase.ipModeEnabled)()
|
||||
_, fp := buildFakeProxier()
|
||||
makeServiceMap(fp,
|
||||
makeTestService(svcPortName.Namespace, svcPortName.Name, func(svc *v1.Service) {
|
||||
svc.Spec.Type = "LoadBalancer"
|
||||
svc.Spec.ClusterIP = testCase.svcIP
|
||||
svc.Spec.Ports = []v1.ServicePort{{
|
||||
Name: svcPortName.Port,
|
||||
Port: int32(svcPort),
|
||||
Protocol: v1.ProtocolTCP,
|
||||
NodePort: int32(svcNodePort),
|
||||
}}
|
||||
svc.Status.LoadBalancer.Ingress = []v1.LoadBalancerIngress{{
|
||||
IP: testCase.svcLBIP,
|
||||
IPMode: testCase.ipMode,
|
||||
}}
|
||||
}),
|
||||
)
|
||||
|
||||
tcpProtocol := v1.ProtocolTCP
|
||||
makeEndpointSliceMap(fp,
|
||||
makeTestEndpointSlice("ns1", "svc1", 1, func(eps *discovery.EndpointSlice) {
|
||||
eps.AddressType = discovery.AddressTypeIPv4
|
||||
eps.Endpoints = []discovery.Endpoint{{
|
||||
Addresses: []string{"10.180.0.1"},
|
||||
}}
|
||||
eps.Ports = []discovery.EndpointPort{{
|
||||
Name: pointer.String("p80"),
|
||||
Port: pointer.Int32(80),
|
||||
Protocol: &tcpProtocol,
|
||||
}}
|
||||
}),
|
||||
)
|
||||
|
||||
fp.syncProxyRules()
|
||||
|
||||
services, err := fp.ipvs.GetVirtualServers()
|
||||
if err != nil {
|
||||
t.Errorf("Failed to get ipvs services, err: %v", err)
|
||||
}
|
||||
if len(services) != testCase.expectedServices {
|
||||
t.Errorf("Expected %d ipvs services, got %d", testCase.expectedServices, len(services))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ type BaseServicePortInfo struct {
|
||||
port int
|
||||
protocol v1.Protocol
|
||||
nodePort int
|
||||
loadBalancerStatus v1.LoadBalancerStatus
|
||||
loadBalancerVIPs []string
|
||||
sessionAffinityType v1.ServiceAffinity
|
||||
stickyMaxAgeSeconds int
|
||||
externalIPs []string
|
||||
@ -108,13 +108,9 @@ func (bsvcPortInfo *BaseServicePortInfo) ExternalIPStrings() []string {
|
||||
return bsvcPortInfo.externalIPs
|
||||
}
|
||||
|
||||
// LoadBalancerIPStrings is part of ServicePort interface.
|
||||
func (bsvcPortInfo *BaseServicePortInfo) LoadBalancerIPStrings() []string {
|
||||
var ips []string
|
||||
for _, ing := range bsvcPortInfo.loadBalancerStatus.Ingress {
|
||||
ips = append(ips, ing.IP)
|
||||
}
|
||||
return ips
|
||||
// LoadBalancerVIPStrings is part of ServicePort interface.
|
||||
func (bsvcPortInfo *BaseServicePortInfo) LoadBalancerVIPStrings() []string {
|
||||
return bsvcPortInfo.loadBalancerVIPs
|
||||
}
|
||||
|
||||
// ExternalPolicyLocal is part of ServicePort interface.
|
||||
@ -139,7 +135,7 @@ func (bsvcPortInfo *BaseServicePortInfo) HintsAnnotation() string {
|
||||
|
||||
// ExternallyAccessible is part of ServicePort interface.
|
||||
func (bsvcPortInfo *BaseServicePortInfo) ExternallyAccessible() bool {
|
||||
return bsvcPortInfo.nodePort != 0 || len(bsvcPortInfo.loadBalancerStatus.Ingress) != 0 || len(bsvcPortInfo.externalIPs) != 0
|
||||
return bsvcPortInfo.nodePort != 0 || len(bsvcPortInfo.loadBalancerVIPs) != 0 || len(bsvcPortInfo.externalIPs) != 0
|
||||
}
|
||||
|
||||
// UsesClusterEndpoints is part of ServicePort interface.
|
||||
@ -211,25 +207,21 @@ func (sct *ServiceChangeTracker) newBaseServiceInfo(port *v1.ServicePort, servic
|
||||
"ipFamily", sct.ipFamily, "loadBalancerSourceRanges", strings.Join(cidrs, ", "), "service", klog.KObj(service))
|
||||
}
|
||||
|
||||
// Obtain Load Balancer Ingress IPs
|
||||
var ips []string
|
||||
// Obtain Load Balancer Ingress
|
||||
var invalidIPs []string
|
||||
for _, ing := range service.Status.LoadBalancer.Ingress {
|
||||
if ing.IP != "" {
|
||||
ips = append(ips, ing.IP)
|
||||
if ing.IP == "" {
|
||||
continue
|
||||
}
|
||||
if ipFamily := proxyutil.GetIPFamilyFromIP(ing.IP); ipFamily == sct.ipFamily && proxyutil.IsVIPMode(ing) {
|
||||
info.loadBalancerVIPs = append(info.loadBalancerVIPs, ing.IP)
|
||||
} else {
|
||||
invalidIPs = append(invalidIPs, ing.IP)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ips) > 0 {
|
||||
ipFamilyMap = proxyutil.MapIPsByIPFamily(ips)
|
||||
|
||||
if ipList, ok := ipFamilyMap[proxyutil.OtherIPFamily(sct.ipFamily)]; ok && len(ipList) > 0 {
|
||||
klog.V(4).InfoS("Service change tracker ignored the following load balancer ingress IPs for given Service as they don't match the IP Family",
|
||||
"ipFamily", sct.ipFamily, "loadBalancerIngressIps", strings.Join(ipList, ", "), "service", klog.KObj(service))
|
||||
}
|
||||
// Create the LoadBalancerStatus with the filtered IPs
|
||||
for _, ip := range ipFamilyMap[sct.ipFamily] {
|
||||
info.loadBalancerStatus.Ingress = append(info.loadBalancerStatus.Ingress, v1.LoadBalancerIngress{IP: ip})
|
||||
}
|
||||
if len(invalidIPs) > 0 {
|
||||
klog.V(4).InfoS("Service change tracker ignored the following load balancer ingress IPs for given Service as they don't match the IP Family",
|
||||
"ipFamily", sct.ipFamily, "loadBalancerIngressIPs", strings.Join(invalidIPs, ", "), "service", klog.KObj(service))
|
||||
}
|
||||
|
||||
if apiservice.NeedsHealthCheck(service) {
|
||||
|
@ -181,10 +181,10 @@ func TestServiceToServiceMap(t *testing.T) {
|
||||
}),
|
||||
expected: map[ServicePortName]*BaseServicePortInfo{
|
||||
makeServicePortName("ns1", "load-balancer", "port3", v1.ProtocolUDP): makeTestServiceInfo("172.16.55.11", 8675, "UDP", 0, func(bsvcPortInfo *BaseServicePortInfo) {
|
||||
bsvcPortInfo.loadBalancerStatus.Ingress = []v1.LoadBalancerIngress{{IP: "10.1.2.4"}}
|
||||
bsvcPortInfo.loadBalancerVIPs = []string{"10.1.2.4"}
|
||||
}),
|
||||
makeServicePortName("ns1", "load-balancer", "port4", v1.ProtocolUDP): makeTestServiceInfo("172.16.55.11", 8676, "UDP", 0, func(bsvcPortInfo *BaseServicePortInfo) {
|
||||
bsvcPortInfo.loadBalancerStatus.Ingress = []v1.LoadBalancerIngress{{IP: "10.1.2.4"}}
|
||||
bsvcPortInfo.loadBalancerVIPs = []string{"10.1.2.4"}
|
||||
}),
|
||||
},
|
||||
},
|
||||
@ -204,10 +204,10 @@ func TestServiceToServiceMap(t *testing.T) {
|
||||
}),
|
||||
expected: map[ServicePortName]*BaseServicePortInfo{
|
||||
makeServicePortName("ns1", "only-local-load-balancer", "portx", v1.ProtocolUDP): makeTestServiceInfo("172.16.55.12", 8677, "UDP", 345, func(bsvcPortInfo *BaseServicePortInfo) {
|
||||
bsvcPortInfo.loadBalancerStatus.Ingress = []v1.LoadBalancerIngress{{IP: "10.1.2.3"}}
|
||||
bsvcPortInfo.loadBalancerVIPs = []string{"10.1.2.3"}
|
||||
}),
|
||||
makeServicePortName("ns1", "only-local-load-balancer", "porty", v1.ProtocolUDP): makeTestServiceInfo("172.16.55.12", 8678, "UDP", 345, func(bsvcPortInfo *BaseServicePortInfo) {
|
||||
bsvcPortInfo.loadBalancerStatus.Ingress = []v1.LoadBalancerIngress{{IP: "10.1.2.3"}}
|
||||
bsvcPortInfo.loadBalancerVIPs = []string{"10.1.2.3"}
|
||||
}),
|
||||
},
|
||||
},
|
||||
@ -315,7 +315,7 @@ func TestServiceToServiceMap(t *testing.T) {
|
||||
makeServicePortName("test", "validIPv4", "testPort", v1.ProtocolTCP): makeTestServiceInfo(testClusterIPv4, 12345, "TCP", 0, func(bsvcPortInfo *BaseServicePortInfo) {
|
||||
bsvcPortInfo.externalIPs = []string{testExternalIPv4}
|
||||
bsvcPortInfo.loadBalancerSourceRanges = []string{testSourceRangeIPv4}
|
||||
bsvcPortInfo.loadBalancerStatus.Ingress = []v1.LoadBalancerIngress{{IP: testExternalIPv4}}
|
||||
bsvcPortInfo.loadBalancerVIPs = []string{testExternalIPv4}
|
||||
}),
|
||||
},
|
||||
},
|
||||
@ -353,7 +353,7 @@ func TestServiceToServiceMap(t *testing.T) {
|
||||
makeServicePortName("test", "validIPv6", "testPort", v1.ProtocolTCP): makeTestServiceInfo(testClusterIPv6, 12345, "TCP", 0, func(bsvcPortInfo *BaseServicePortInfo) {
|
||||
bsvcPortInfo.externalIPs = []string{testExternalIPv6}
|
||||
bsvcPortInfo.loadBalancerSourceRanges = []string{testSourceRangeIPv6}
|
||||
bsvcPortInfo.loadBalancerStatus.Ingress = []v1.LoadBalancerIngress{{IP: testExternalIPv6}}
|
||||
bsvcPortInfo.loadBalancerVIPs = []string{testExternalIPv6}
|
||||
}),
|
||||
},
|
||||
},
|
||||
@ -391,7 +391,7 @@ func TestServiceToServiceMap(t *testing.T) {
|
||||
makeServicePortName("test", "filterIPv6InIPV4Mode", "testPort", v1.ProtocolTCP): makeTestServiceInfo(testClusterIPv4, 12345, "TCP", 0, func(bsvcPortInfo *BaseServicePortInfo) {
|
||||
bsvcPortInfo.externalIPs = []string{testExternalIPv4}
|
||||
bsvcPortInfo.loadBalancerSourceRanges = []string{testSourceRangeIPv4}
|
||||
bsvcPortInfo.loadBalancerStatus.Ingress = []v1.LoadBalancerIngress{{IP: testExternalIPv4}}
|
||||
bsvcPortInfo.loadBalancerVIPs = []string{testExternalIPv4}
|
||||
}),
|
||||
},
|
||||
},
|
||||
@ -429,7 +429,7 @@ func TestServiceToServiceMap(t *testing.T) {
|
||||
makeServicePortName("test", "filterIPv4InIPV6Mode", "testPort", v1.ProtocolTCP): makeTestServiceInfo(testClusterIPv6, 12345, "TCP", 0, func(bsvcPortInfo *BaseServicePortInfo) {
|
||||
bsvcPortInfo.externalIPs = []string{testExternalIPv6}
|
||||
bsvcPortInfo.loadBalancerSourceRanges = []string{testSourceRangeIPv6}
|
||||
bsvcPortInfo.loadBalancerStatus.Ingress = []v1.LoadBalancerIngress{{IP: testExternalIPv6}}
|
||||
bsvcPortInfo.loadBalancerVIPs = []string{testExternalIPv6}
|
||||
}),
|
||||
},
|
||||
},
|
||||
@ -483,7 +483,7 @@ func TestServiceToServiceMap(t *testing.T) {
|
||||
svcInfo.healthCheckNodePort != expectedInfo.healthCheckNodePort ||
|
||||
!sets.New[string](svcInfo.externalIPs...).Equal(sets.New[string](expectedInfo.externalIPs...)) ||
|
||||
!sets.New[string](svcInfo.loadBalancerSourceRanges...).Equal(sets.New[string](expectedInfo.loadBalancerSourceRanges...)) ||
|
||||
!reflect.DeepEqual(svcInfo.loadBalancerStatus, expectedInfo.loadBalancerStatus) {
|
||||
!reflect.DeepEqual(svcInfo.loadBalancerVIPs, expectedInfo.loadBalancerVIPs) {
|
||||
t.Errorf("[%s] expected new[%v]to be %v, got %v", tc.desc, svcKey, expectedInfo, *svcInfo)
|
||||
}
|
||||
for svcKey, expectedInfo := range tc.expected {
|
||||
@ -494,7 +494,7 @@ func TestServiceToServiceMap(t *testing.T) {
|
||||
svcInfo.healthCheckNodePort != expectedInfo.healthCheckNodePort ||
|
||||
!sets.New[string](svcInfo.externalIPs...).Equal(sets.New[string](expectedInfo.externalIPs...)) ||
|
||||
!sets.New[string](svcInfo.loadBalancerSourceRanges...).Equal(sets.New[string](expectedInfo.loadBalancerSourceRanges...)) ||
|
||||
!reflect.DeepEqual(svcInfo.loadBalancerStatus, expectedInfo.loadBalancerStatus) {
|
||||
!reflect.DeepEqual(svcInfo.loadBalancerVIPs, expectedInfo.loadBalancerVIPs) {
|
||||
t.Errorf("expected new[%v]to be %v, got %v", svcKey, expectedInfo, *svcInfo)
|
||||
}
|
||||
}
|
||||
|
@ -73,8 +73,8 @@ type ServicePort interface {
|
||||
StickyMaxAgeSeconds() int
|
||||
// ExternalIPStrings returns service ExternalIPs as a string array.
|
||||
ExternalIPStrings() []string
|
||||
// LoadBalancerIPStrings returns service LoadBalancerIPs as a string array.
|
||||
LoadBalancerIPStrings() []string
|
||||
// LoadBalancerVIPStrings returns service LoadBalancerIPs which are VIP mode as a string array.
|
||||
LoadBalancerVIPStrings() []string
|
||||
// Protocol returns service protocol.
|
||||
Protocol() v1.Protocol
|
||||
// LoadBalancerSourceRanges returns service LoadBalancerSourceRanges if present empty array if not
|
||||
|
@ -27,9 +27,11 @@ import (
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
utilrand "k8s.io/apimachinery/pkg/util/rand"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/client-go/tools/events"
|
||||
utilsysctl "k8s.io/component-helpers/node/util/sysctl"
|
||||
helper "k8s.io/kubernetes/pkg/apis/core/v1/helper"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
netutils "k8s.io/utils/net"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
@ -182,7 +184,7 @@ func MapIPsByIPFamily(ipStrings []string) map[v1.IPFamily][]string {
|
||||
ipFamilyMap := map[v1.IPFamily][]string{}
|
||||
for _, ip := range ipStrings {
|
||||
// Handle only the valid IPs
|
||||
if ipFamily := getIPFamilyFromIP(ip); ipFamily != "" {
|
||||
if ipFamily := GetIPFamilyFromIP(ip); ipFamily != v1.IPFamilyUnknown {
|
||||
ipFamilyMap[ipFamily] = append(ipFamilyMap[ipFamily], ip)
|
||||
} else {
|
||||
// this function is called in multiple places. All of which
|
||||
@ -204,7 +206,7 @@ func MapCIDRsByIPFamily(cidrStrings []string) map[v1.IPFamily][]string {
|
||||
ipFamilyMap := map[v1.IPFamily][]string{}
|
||||
for _, cidr := range cidrStrings {
|
||||
// Handle only the valid CIDRs
|
||||
if ipFamily := getIPFamilyFromCIDR(cidr); ipFamily != "" {
|
||||
if ipFamily := getIPFamilyFromCIDR(cidr); ipFamily != v1.IPFamilyUnknown {
|
||||
ipFamilyMap[ipFamily] = append(ipFamilyMap[ipFamily], cidr)
|
||||
} else {
|
||||
klog.ErrorS(nil, "Skipping invalid CIDR", "cidr", cidr)
|
||||
@ -213,29 +215,26 @@ func MapCIDRsByIPFamily(cidrStrings []string) map[v1.IPFamily][]string {
|
||||
return ipFamilyMap
|
||||
}
|
||||
|
||||
// Returns the IP family of ipStr, or "" if ipStr can't be parsed as an IP
|
||||
func getIPFamilyFromIP(ipStr string) v1.IPFamily {
|
||||
netIP := netutils.ParseIPSloppy(ipStr)
|
||||
if netIP == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if netutils.IsIPv6(netIP) {
|
||||
return v1.IPv6Protocol
|
||||
}
|
||||
return v1.IPv4Protocol
|
||||
// GetIPFamilyFromIP Returns the IP family of ipStr, or IPFamilyUnknown if ipStr can't be parsed as an IP
|
||||
func GetIPFamilyFromIP(ipStr string) v1.IPFamily {
|
||||
return convertToV1IPFamily(netutils.IPFamilyOfString(ipStr))
|
||||
}
|
||||
|
||||
// Returns the IP family of cidrStr, or "" if cidrStr can't be parsed as a CIDR
|
||||
// Returns the IP family of cidrStr, or IPFamilyUnknown if cidrStr can't be parsed as a CIDR
|
||||
func getIPFamilyFromCIDR(cidrStr string) v1.IPFamily {
|
||||
_, netCIDR, err := netutils.ParseCIDRSloppy(cidrStr)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if netutils.IsIPv6CIDR(netCIDR) {
|
||||
return convertToV1IPFamily(netutils.IPFamilyOfCIDRString(cidrStr))
|
||||
}
|
||||
|
||||
// Convert netutils.IPFamily to v1.IPFamily
|
||||
func convertToV1IPFamily(ipFamily netutils.IPFamily) v1.IPFamily {
|
||||
switch ipFamily {
|
||||
case netutils.IPv4:
|
||||
return v1.IPv4Protocol
|
||||
case netutils.IPv6:
|
||||
return v1.IPv6Protocol
|
||||
}
|
||||
return v1.IPv4Protocol
|
||||
|
||||
return v1.IPFamilyUnknown
|
||||
}
|
||||
|
||||
// OtherIPFamily returns the other ip family
|
||||
@ -331,3 +330,13 @@ func RevertPorts(replacementPortsMap, originalPortsMap map[netutils.LocalPort]ne
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func IsVIPMode(ing v1.LoadBalancerIngress) bool {
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(features.LoadBalancerIPMode) {
|
||||
return true // backwards compat
|
||||
}
|
||||
if ing.IPMode == nil {
|
||||
return true
|
||||
}
|
||||
return *ing.IPMode == v1.LoadBalancerIPModeVIP
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
@ -39,9 +40,12 @@ import (
|
||||
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
epstest "k8s.io/kubernetes/pkg/api/endpoints/testing"
|
||||
svctest "k8s.io/kubernetes/pkg/api/service/testing"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
endpointstore "k8s.io/kubernetes/pkg/registry/core/endpoint/storage"
|
||||
podstore "k8s.io/kubernetes/pkg/registry/core/pod/storage"
|
||||
"k8s.io/kubernetes/pkg/registry/core/service/ipallocator"
|
||||
@ -11681,3 +11685,288 @@ func TestServiceRegistryResourceLocation(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateServiceLoadBalancerStatus(t *testing.T) {
|
||||
storage, statusStorage, server := newStorage(t, []api.IPFamily{api.IPv4Protocol})
|
||||
defer server.Terminate(t)
|
||||
defer storage.Store.DestroyFunc()
|
||||
defer statusStorage.store.DestroyFunc()
|
||||
|
||||
ipModeVIP := api.LoadBalancerIPModeVIP
|
||||
ipModeProxy := api.LoadBalancerIPModeProxy
|
||||
ipModeDummy := api.LoadBalancerIPMode("dummy")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
ipModeEnabled bool
|
||||
statusBeforeUpdate api.ServiceStatus
|
||||
newStatus api.ServiceStatus
|
||||
expectedStatus api.ServiceStatus
|
||||
expectErr bool
|
||||
expectedReasonForError metav1.StatusReason
|
||||
}{
|
||||
/*LoadBalancerIPMode disabled*/
|
||||
{
|
||||
name: "LoadBalancerIPMode disabled, ipMode not used in old, not used in new",
|
||||
ipModeEnabled: false,
|
||||
statusBeforeUpdate: api.ServiceStatus{},
|
||||
newStatus: api.ServiceStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
},
|
||||
},
|
||||
expectedStatus: api.ServiceStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
}, {
|
||||
name: "LoadBalancerIPMode disabled, ipMode used in old and in new",
|
||||
ipModeEnabled: false,
|
||||
statusBeforeUpdate: api.ServiceStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeVIP,
|
||||
}},
|
||||
},
|
||||
},
|
||||
newStatus: api.ServiceStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeProxy,
|
||||
}},
|
||||
},
|
||||
},
|
||||
expectedStatus: api.ServiceStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeProxy,
|
||||
}},
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
}, {
|
||||
name: "LoadBalancerIPMode disabled, ipMode not used in old, used in new",
|
||||
ipModeEnabled: false,
|
||||
statusBeforeUpdate: api.ServiceStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
},
|
||||
},
|
||||
newStatus: api.ServiceStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeProxy,
|
||||
}},
|
||||
},
|
||||
},
|
||||
expectedStatus: api.ServiceStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
}, {
|
||||
name: "LoadBalancerIPMode disabled, ipMode used in old, not used in new",
|
||||
ipModeEnabled: false,
|
||||
statusBeforeUpdate: api.ServiceStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeVIP,
|
||||
}},
|
||||
},
|
||||
},
|
||||
newStatus: api.ServiceStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
},
|
||||
},
|
||||
expectedStatus: api.ServiceStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
/*LoadBalancerIPMode enabled*/
|
||||
{
|
||||
name: "LoadBalancerIPMode enabled, ipMode not used in old, not used in new",
|
||||
ipModeEnabled: true,
|
||||
statusBeforeUpdate: api.ServiceStatus{},
|
||||
newStatus: api.ServiceStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
},
|
||||
},
|
||||
expectedStatus: api.ServiceStatus{},
|
||||
expectErr: true,
|
||||
expectedReasonForError: metav1.StatusReasonInvalid,
|
||||
}, {
|
||||
name: "LoadBalancerIPMode enabled, ipMode used in old and in new",
|
||||
ipModeEnabled: true,
|
||||
statusBeforeUpdate: api.ServiceStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeProxy,
|
||||
}},
|
||||
},
|
||||
},
|
||||
newStatus: api.ServiceStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeVIP,
|
||||
}},
|
||||
},
|
||||
},
|
||||
expectedStatus: api.ServiceStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeVIP,
|
||||
}},
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
}, {
|
||||
name: "LoadBalancerIPMode enabled, ipMode not used in old, used in new",
|
||||
ipModeEnabled: true,
|
||||
statusBeforeUpdate: api.ServiceStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
},
|
||||
},
|
||||
newStatus: api.ServiceStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeProxy,
|
||||
}},
|
||||
},
|
||||
},
|
||||
expectedStatus: api.ServiceStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeProxy,
|
||||
}},
|
||||
},
|
||||
},
|
||||
expectErr: false,
|
||||
}, {
|
||||
name: "LoadBalancerIPMode enabled, ipMode used in old, not used in new",
|
||||
ipModeEnabled: true,
|
||||
statusBeforeUpdate: api.ServiceStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeVIP,
|
||||
}},
|
||||
},
|
||||
},
|
||||
newStatus: api.ServiceStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
},
|
||||
},
|
||||
expectedStatus: api.ServiceStatus{},
|
||||
expectErr: true,
|
||||
expectedReasonForError: metav1.StatusReasonInvalid,
|
||||
}, {
|
||||
name: "LoadBalancerIPMode enabled, ipMode not used in old, invalid value used in new",
|
||||
ipModeEnabled: true,
|
||||
statusBeforeUpdate: api.ServiceStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
},
|
||||
},
|
||||
newStatus: api.ServiceStatus{
|
||||
LoadBalancer: api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeDummy,
|
||||
}},
|
||||
},
|
||||
},
|
||||
expectedStatus: api.ServiceStatus{},
|
||||
expectErr: true,
|
||||
expectedReasonForError: metav1.StatusReasonInvalid,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
svc := svctest.MakeService("foo", svctest.SetTypeLoadBalancer)
|
||||
ctx := genericapirequest.NewDefaultContext()
|
||||
obj, err := storage.Create(ctx, svc, rest.ValidateAllObjectFunc, &metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Errorf("created svc: %s", err)
|
||||
}
|
||||
defer storage.Delete(ctx, svc.Name, rest.ValidateAllObjectFunc, &metav1.DeleteOptions{})
|
||||
|
||||
// prepare status
|
||||
if loadbalancerIPModeInUse(tc.statusBeforeUpdate) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.LoadBalancerIPMode, true)()
|
||||
}
|
||||
oldSvc := obj.(*api.Service).DeepCopy()
|
||||
oldSvc.Status = tc.statusBeforeUpdate
|
||||
obj, _, err = statusStorage.Update(ctx, oldSvc.Name, rest.DefaultUpdatedObjectInfo(oldSvc), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
t.Errorf("updated status: %s", err)
|
||||
}
|
||||
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.LoadBalancerIPMode, tc.ipModeEnabled)()
|
||||
newSvc := obj.(*api.Service).DeepCopy()
|
||||
newSvc.Status = tc.newStatus
|
||||
obj, _, err = statusStorage.Update(ctx, newSvc.Name, rest.DefaultUpdatedObjectInfo(newSvc), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{})
|
||||
if err != nil {
|
||||
if tc.expectErr && tc.expectedReasonForError == errors.ReasonForError(err) {
|
||||
return
|
||||
}
|
||||
t.Errorf("updated status: %s", err)
|
||||
}
|
||||
|
||||
updated := obj.(*api.Service)
|
||||
if !reflect.DeepEqual(tc.expectedStatus, updated.Status) {
|
||||
t.Errorf("%v: unexpected svc status: %v", tc.name, cmp.Diff(tc.expectedStatus, updated.Status))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func loadbalancerIPModeInUse(status api.ServiceStatus) bool {
|
||||
for _, ing := range status.LoadBalancer.Ingress {
|
||||
if ing.IPMode != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -24,10 +24,12 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apiserver/pkg/storage/names"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||
serviceapi "k8s.io/kubernetes/pkg/api/service"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/apis/core/validation"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
|
||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||
)
|
||||
@ -144,6 +146,8 @@ func (serviceStatusStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpat
|
||||
func (serviceStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
||||
newService := obj.(*api.Service)
|
||||
oldService := old.(*api.Service)
|
||||
|
||||
dropServiceStatusDisabledFields(newService, oldService)
|
||||
// status changes are not allowed to update spec
|
||||
newService.Spec = oldService.Spec
|
||||
}
|
||||
@ -158,6 +162,33 @@ func (serviceStatusStrategy) WarningsOnUpdate(ctx context.Context, obj, old runt
|
||||
return nil
|
||||
}
|
||||
|
||||
// dropServiceStatusDisabledFields drops fields that are not used if their associated feature gates
|
||||
// are not enabled. The typical pattern is:
|
||||
//
|
||||
// if !utilfeature.DefaultFeatureGate.Enabled(features.MyFeature) && !myFeatureInUse(oldSvc) {
|
||||
// newSvc.Status.MyFeature = nil
|
||||
// }
|
||||
func dropServiceStatusDisabledFields(newSvc *api.Service, oldSvc *api.Service) {
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(features.LoadBalancerIPMode) && !loadbalancerIPModeInUse(oldSvc) {
|
||||
for i := range newSvc.Status.LoadBalancer.Ingress {
|
||||
newSvc.Status.LoadBalancer.Ingress[i].IPMode = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// returns true when the LoadBalancer Ingress IPMode fields are in use.
|
||||
func loadbalancerIPModeInUse(svc *api.Service) bool {
|
||||
if svc == nil {
|
||||
return false
|
||||
}
|
||||
for _, ing := range svc.Status.LoadBalancer.Ingress {
|
||||
if ing.IPMode != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func sameStringSlice(a []string, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
|
@ -26,8 +26,11 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
_ "k8s.io/kubernetes/pkg/apis/core/install"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
utilpointer "k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
@ -229,6 +232,251 @@ func TestDropDisabledField(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestDropServiceStatusDisabledFields(t *testing.T) {
|
||||
ipModeVIP := api.LoadBalancerIPModeVIP
|
||||
ipModeProxy := api.LoadBalancerIPModeProxy
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
ipModeEnabled bool
|
||||
svc *api.Service
|
||||
oldSvc *api.Service
|
||||
compareSvc *api.Service
|
||||
}{
|
||||
/*LoadBalancerIPMode disabled*/
|
||||
{
|
||||
name: "LoadBalancerIPMode disabled, ipMode not used in old, not used in new",
|
||||
ipModeEnabled: false,
|
||||
svc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
}
|
||||
}),
|
||||
oldSvc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{}
|
||||
}),
|
||||
compareSvc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
}
|
||||
}),
|
||||
}, {
|
||||
name: "LoadBalancerIPMode disabled, ipMode used in old and in new",
|
||||
ipModeEnabled: false,
|
||||
svc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeProxy,
|
||||
}},
|
||||
}
|
||||
}),
|
||||
oldSvc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeVIP,
|
||||
}},
|
||||
}
|
||||
}),
|
||||
compareSvc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeProxy,
|
||||
}},
|
||||
}
|
||||
}),
|
||||
}, {
|
||||
name: "LoadBalancerIPMode disabled, ipMode not used in old, used in new",
|
||||
ipModeEnabled: false,
|
||||
svc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeVIP,
|
||||
}},
|
||||
}
|
||||
}),
|
||||
oldSvc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
}
|
||||
}),
|
||||
compareSvc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
}
|
||||
}),
|
||||
}, {
|
||||
name: "LoadBalancerIPMode disabled, ipMode used in old, not used in new",
|
||||
ipModeEnabled: false,
|
||||
svc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
}
|
||||
}),
|
||||
oldSvc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeProxy,
|
||||
}},
|
||||
}
|
||||
}),
|
||||
compareSvc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
}
|
||||
}),
|
||||
},
|
||||
/*LoadBalancerIPMode enabled*/
|
||||
{
|
||||
name: "LoadBalancerIPMode enabled, ipMode not used in old, not used in new",
|
||||
ipModeEnabled: true,
|
||||
svc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
}
|
||||
}),
|
||||
oldSvc: nil,
|
||||
compareSvc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
}
|
||||
}),
|
||||
}, {
|
||||
name: "LoadBalancerIPMode enabled, ipMode used in old and in new",
|
||||
ipModeEnabled: true,
|
||||
svc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeProxy,
|
||||
}},
|
||||
}
|
||||
}),
|
||||
oldSvc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeVIP,
|
||||
}},
|
||||
}
|
||||
}),
|
||||
compareSvc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeProxy,
|
||||
}},
|
||||
}
|
||||
}),
|
||||
}, {
|
||||
name: "LoadBalancerIPMode enabled, ipMode not used in old, used in new",
|
||||
ipModeEnabled: true,
|
||||
svc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeVIP,
|
||||
}},
|
||||
}
|
||||
}),
|
||||
oldSvc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
}
|
||||
}),
|
||||
compareSvc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeVIP,
|
||||
}},
|
||||
}
|
||||
}),
|
||||
}, {
|
||||
name: "LoadBalancerIPMode enabled, ipMode used in old, not used in new",
|
||||
ipModeEnabled: true,
|
||||
svc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
}
|
||||
}),
|
||||
oldSvc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
IPMode: &ipModeProxy,
|
||||
}},
|
||||
}
|
||||
}),
|
||||
compareSvc: makeValidServiceCustom(func(svc *api.Service) {
|
||||
svc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||
svc.Status.LoadBalancer = api.LoadBalancerStatus{
|
||||
Ingress: []api.LoadBalancerIngress{{
|
||||
IP: "1.2.3.4",
|
||||
}},
|
||||
}
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.LoadBalancerIPMode, tc.ipModeEnabled)()
|
||||
dropServiceStatusDisabledFields(tc.svc, tc.oldSvc)
|
||||
|
||||
if !reflect.DeepEqual(tc.svc, tc.compareSvc) {
|
||||
t.Errorf("%v: unexpected svc spec: %v", tc.name, cmp.Diff(tc.svc, tc.compareSvc))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDropTypeDependentFields(t *testing.T) {
|
||||
// Tweaks used below.
|
||||
setTypeExternalName := func(svc *api.Service) {
|
||||
|
1198
staging/src/k8s.io/api/core/v1/generated.pb.go
generated
1198
staging/src/k8s.io/api/core/v1/generated.pb.go
generated
File diff suppressed because it is too large
Load Diff
@ -2171,6 +2171,15 @@ message LoadBalancerIngress {
|
||||
// +optional
|
||||
optional string hostname = 2;
|
||||
|
||||
// IPMode specifies how the load-balancer IP behaves, and may only be specified when the ip field is specified.
|
||||
// Setting this to "VIP" indicates that traffic is delivered to the node with
|
||||
// the destination set to the load-balancer's IP and port.
|
||||
// Setting this to "Proxy" indicates that traffic is delivered to the node or pod with
|
||||
// the destination set to the node's IP and node port or the pod's IP and port.
|
||||
// Service implementations may use this information to adjust traffic routing.
|
||||
// +optional
|
||||
optional string ipMode = 3;
|
||||
|
||||
// Ports is a list of records of service ports
|
||||
// If used, every port defined in the service should have an entry in it
|
||||
// +listType=atomic
|
||||
|
@ -4692,6 +4692,15 @@ type LoadBalancerIngress struct {
|
||||
// +optional
|
||||
Hostname string `json:"hostname,omitempty" protobuf:"bytes,2,opt,name=hostname"`
|
||||
|
||||
// IPMode specifies how the load-balancer IP behaves, and may only be specified when the ip field is specified.
|
||||
// Setting this to "VIP" indicates that traffic is delivered to the node with
|
||||
// the destination set to the load-balancer's IP and port.
|
||||
// Setting this to "Proxy" indicates that traffic is delivered to the node or pod with
|
||||
// the destination set to the node's IP and node port or the pod's IP and port.
|
||||
// Service implementations may use this information to adjust traffic routing.
|
||||
// +optional
|
||||
IPMode *LoadBalancerIPMode `json:"ipMode,omitempty" protobuf:"bytes,3,opt,name=ipMode"`
|
||||
|
||||
// Ports is a list of records of service ports
|
||||
// If used, every port defined in the service should have an entry in it
|
||||
// +listType=atomic
|
||||
@ -4709,6 +4718,8 @@ const (
|
||||
IPv4Protocol IPFamily = "IPv4"
|
||||
// IPv6Protocol indicates that this IP is IPv6 protocol
|
||||
IPv6Protocol IPFamily = "IPv6"
|
||||
// IPFamilyUnknown indicates that this IP is unknown protocol
|
||||
IPFamilyUnknown IPFamily = ""
|
||||
)
|
||||
|
||||
// IPFamilyPolicy represents the dual-stack-ness requested or required by a Service
|
||||
@ -7054,3 +7065,15 @@ type PortStatus struct {
|
||||
// +kubebuilder:validation:MaxLength=316
|
||||
Error *string `json:"error,omitempty" protobuf:"bytes,3,opt,name=error"`
|
||||
}
|
||||
|
||||
// LoadBalancerIPMode represents the mode of the LoadBalancer ingress IP
|
||||
type LoadBalancerIPMode string
|
||||
|
||||
const (
|
||||
// LoadBalancerIPModeVIP indicates that traffic is delivered to the node with
|
||||
// the destination set to the load-balancer's IP and port.
|
||||
LoadBalancerIPModeVIP LoadBalancerIPMode = "VIP"
|
||||
// LoadBalancerIPModeProxy indicates that traffic is delivered to the node or pod with
|
||||
// the destination set to the node's IP and port or the pod's IP and port.
|
||||
LoadBalancerIPModeProxy LoadBalancerIPMode = "Proxy"
|
||||
)
|
||||
|
@ -988,6 +988,7 @@ var map_LoadBalancerIngress = map[string]string{
|
||||
"": "LoadBalancerIngress represents the status of a load-balancer ingress point: traffic intended for the service should be sent to an ingress point.",
|
||||
"ip": "IP is set for load-balancer ingress points that are IP based (typically GCE or OpenStack load-balancers)",
|
||||
"hostname": "Hostname is set for load-balancer ingress points that are DNS based (typically AWS load-balancers)",
|
||||
"ipMode": "IPMode specifies how the load-balancer IP behaves, and may only be specified when the ip field is specified. Setting this to \"VIP\" indicates that traffic is delivered to the node with the destination set to the load-balancer's IP and port. Setting this to \"Proxy\" indicates that traffic is delivered to the node or pod with the destination set to the node's IP and node port or the pod's IP and port. Service implementations may use this information to adjust traffic routing.",
|
||||
"ports": "Ports is a list of records of service ports If used, every port defined in the service should have an entry in it",
|
||||
}
|
||||
|
||||
|
@ -2228,6 +2228,11 @@ func (in *List) DeepCopyObject() runtime.Object {
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *LoadBalancerIngress) DeepCopyInto(out *LoadBalancerIngress) {
|
||||
*out = *in
|
||||
if in.IPMode != nil {
|
||||
in, out := &in.IPMode, &out.IPMode
|
||||
*out = new(LoadBalancerIPMode)
|
||||
**out = **in
|
||||
}
|
||||
if in.Ports != nil {
|
||||
in, out := &in.Ports, &out.Ports
|
||||
*out = make([]PortStatus, len(*in))
|
||||
|
@ -93,6 +93,7 @@
|
||||
{
|
||||
"ip": "ipValue",
|
||||
"hostname": "hostnameValue",
|
||||
"ipMode": "ipModeValue",
|
||||
"ports": [
|
||||
{
|
||||
"port": 1,
|
||||
|
Binary file not shown.
@ -77,6 +77,7 @@ status:
|
||||
ingress:
|
||||
- hostname: hostnameValue
|
||||
ip: ipValue
|
||||
ipMode: ipModeValue
|
||||
ports:
|
||||
- error: errorValue
|
||||
port: 1
|
||||
|
@ -18,11 +18,16 @@ limitations under the License.
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// LoadBalancerIngressApplyConfiguration represents an declarative configuration of the LoadBalancerIngress type for use
|
||||
// with apply.
|
||||
type LoadBalancerIngressApplyConfiguration struct {
|
||||
IP *string `json:"ip,omitempty"`
|
||||
Hostname *string `json:"hostname,omitempty"`
|
||||
IPMode *v1.LoadBalancerIPMode `json:"ipMode,omitempty"`
|
||||
Ports []PortStatusApplyConfiguration `json:"ports,omitempty"`
|
||||
}
|
||||
|
||||
@ -48,6 +53,14 @@ func (b *LoadBalancerIngressApplyConfiguration) WithHostname(value string) *Load
|
||||
return b
|
||||
}
|
||||
|
||||
// WithIPMode sets the IPMode field in the declarative configuration to the given value
|
||||
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
|
||||
// If called multiple times, the IPMode field is set to the value of the last call.
|
||||
func (b *LoadBalancerIngressApplyConfiguration) WithIPMode(value v1.LoadBalancerIPMode) *LoadBalancerIngressApplyConfiguration {
|
||||
b.IPMode = &value
|
||||
return b
|
||||
}
|
||||
|
||||
// WithPorts adds the given value to the Ports field in the declarative configuration
|
||||
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
|
||||
// If called multiple times, values provided by each call will be appended to the Ports field.
|
||||
|
@ -5567,6 +5567,9 @@ var schemaYAML = typed.YAMLObject(`types:
|
||||
- name: ip
|
||||
type:
|
||||
scalar: string
|
||||
- name: ipMode
|
||||
type:
|
||||
scalar: string
|
||||
- name: ports
|
||||
type:
|
||||
list:
|
||||
|
@ -180,5 +180,8 @@ func ingressEqual(lhs, rhs *v1.LoadBalancerIngress) bool {
|
||||
if lhs.Hostname != rhs.Hostname {
|
||||
return false
|
||||
}
|
||||
if lhs.IPMode != rhs.IPMode {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -28,9 +29,12 @@ import (
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
servicecontroller "k8s.io/cloud-provider/controllers/service"
|
||||
fakecloud "k8s.io/cloud-provider/fake"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
controllersmetrics "k8s.io/component-base/metrics/prometheus/controllers"
|
||||
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
"k8s.io/utils/net"
|
||||
utilpointer "k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
@ -452,3 +456,81 @@ func newServiceController(t *testing.T, client *clientset.Clientset) (*serviceco
|
||||
cloud.ClearCalls() // ignore any cloud calls made in init()
|
||||
return controller, cloud, informerFactory
|
||||
}
|
||||
|
||||
// Test_ServiceLoadBalancerIPMode tests whether the cloud provider has correctly updated the ipMode field.
|
||||
func Test_ServiceLoadBalancerIPMode(t *testing.T) {
|
||||
ipModeVIP := corev1.LoadBalancerIPModeVIP
|
||||
testCases := []struct {
|
||||
ipModeEnabled bool
|
||||
externalIP string
|
||||
expectedIPMode *corev1.LoadBalancerIPMode
|
||||
}{
|
||||
{
|
||||
ipModeEnabled: false,
|
||||
externalIP: "1.2.3.4",
|
||||
expectedIPMode: nil,
|
||||
},
|
||||
{
|
||||
ipModeEnabled: true,
|
||||
externalIP: "1.2.3.5",
|
||||
expectedIPMode: &ipModeVIP,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.LoadBalancerIPMode, tc.ipModeEnabled)()
|
||||
server := kubeapiservertesting.StartTestServerOrDie(t, nil, nil, framework.SharedEtcd())
|
||||
defer server.TearDownFn()
|
||||
|
||||
client, err := clientset.NewForConfig(server.ClientConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating clientset: %v", err)
|
||||
}
|
||||
|
||||
ns := framework.CreateNamespaceOrDie(client, "test-service-update-load-balancer-ip-mode", t)
|
||||
defer framework.DeleteNamespaceOrDie(client, ns, t)
|
||||
|
||||
controller, cloud, informer := newServiceController(t, client)
|
||||
cloud.ExternalIP = net.ParseIPSloppy(tc.externalIP)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
informer.Start(ctx.Done())
|
||||
go controller.Run(ctx, 1, controllersmetrics.NewControllerManagerMetrics("loadbalancer-test"))
|
||||
|
||||
service := &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-update-load-balancer-ip-mode",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Type: corev1.ServiceTypeLoadBalancer,
|
||||
Ports: []corev1.ServicePort{{
|
||||
Port: int32(80),
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
service, err = client.CoreV1().Services(ns.Name).Create(ctx, service, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating test service: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second) // sleep 5 second to wait for the service controller reconcile
|
||||
service, err = client.CoreV1().Services(ns.Name).Get(ctx, service.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting test service: %v", err)
|
||||
}
|
||||
|
||||
if len(service.Status.LoadBalancer.Ingress) == 0 {
|
||||
t.Fatalf("unexpected load balancer status")
|
||||
}
|
||||
|
||||
gotIngress := service.Status.LoadBalancer.Ingress[0]
|
||||
if gotIngress.IP != tc.externalIP || !reflect.DeepEqual(gotIngress.IPMode, tc.expectedIPMode) {
|
||||
t.Errorf("unexpected load balancer ingress, got ingress %v, expected IP %v, expected ipMode %v",
|
||||
gotIngress, tc.externalIP, tc.expectedIPMode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user