mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-10 04:27:54 +00:00
Add IP mode field
This commit is contained in:
parent
16534deedf
commit
e6863757f4
@ -4018,6 +4018,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
|
||||
@ -6090,3 +6099,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{
|
||||
|
@ -6997,6 +6997,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{}
|
||||
@ -7007,6 +7011,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))
|
||||
|
@ -22997,3 +22997,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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -885,6 +885,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() {
|
||||
@ -1124,6 +1130,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:
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -4632,6 +4632,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
|
||||
@ -6994,3 +7003,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"
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user