Merge pull request #119937 from RyanAoh/kep-1860-dev

Make Kubernetes aware of the LoadBalancer behaviour
This commit is contained in:
Kubernetes Prow Robot 2023-08-17 14:00:28 -07:00 committed by GitHub
commit ee265c92fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1878 additions and 640 deletions

View File

@ -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": {

View File

@ -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": {

View File

@ -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"
)

View File

@ -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

View File

@ -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{

View File

@ -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
}

View File

@ -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))

View File

@ -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())
}
})
}
}

View File

@ -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))

View File

@ -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:

View File

@ -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{

View File

@ -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)

View File

@ -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,

View File

@ -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)
}
})
}
}

View File

@ -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,

View File

@ -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))
}
})
}
}

View File

@ -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) {

View File

@ -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)
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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) {

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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"
)

View File

@ -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",
}

View File

@ -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))

View File

@ -93,6 +93,7 @@
{
"ip": "ipValue",
"hostname": "hostnameValue",
"ipMode": "ipModeValue",
"ports": [
{
"port": 1,

View File

@ -77,6 +77,7 @@ status:
ingress:
- hostname: hostnameValue
ip: ipValue
ipMode: ipModeValue
ports:
- error: errorValue
port: 1

View File

@ -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.

View File

@ -5567,6 +5567,9 @@ var schemaYAML = typed.YAMLObject(`types:
- name: ip
type:
scalar: string
- name: ipMode
type:
scalar: string
- name: ports
type:
list:

View File

@ -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
}

View File

@ -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)
}
})
}
}