mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-24 04:06:03 +00:00
Merge pull request #98277 from XudongLiuHarold/add-loadbalancerclass-field
Add LoadBalancerClass field in service
This commit is contained in:
commit
66cbf0196b
4
api/openapi-spec/swagger.json
generated
4
api/openapi-spec/swagger.json
generated
@ -10243,6 +10243,10 @@
|
||||
"description": "IPFamilyPolicy represents the dual-stack-ness requested or required by this Service, and is gated by the \"IPv6DualStack\" feature gate. If there is no value provided, then this field will be set to SingleStack. Services can be \"SingleStack\" (a single IP family), \"PreferDualStack\" (two IP families on dual-stack configured clusters or a single IP family on single-stack clusters), or \"RequireDualStack\" (two IP families on dual-stack configured clusters, otherwise fail). The ipFamilies and clusterIPs fields depend on the value of this field. This field will be wiped when updating a service to type ExternalName.",
|
||||
"type": "string"
|
||||
},
|
||||
"loadBalancerClass": {
|
||||
"description": "loadBalancerClass is the class of the load balancer implementation this Service belongs to. If specified, the value of this field must be a label-style identifier, with an optional prefix, e.g. \"internal-vip\" or \"example.com/internal-vip\". Unprefixed names are reserved for end-users. This field can only be set when the Service type is 'LoadBalancer'. If not set, the default load balancer implementation is used, today this is typically done through the cloud provider integration, but should apply for any default implementation. If set, it is assumed that a load balancer implementation is watching for Services with a matching class. Any default load balancer implementation (e.g. cloud providers) should ignore Services that set this field. This field can only be set when creating or updating a Service to type 'LoadBalancer'. Once set, it can not be changed. This field will be wiped when a service is updated to a non 'LoadBalancer' type. featureGate=LoadBalancerClass",
|
||||
"type": "string"
|
||||
},
|
||||
"loadBalancerIP": {
|
||||
"description": "Only applies to Service Type: LoadBalancer LoadBalancer will get created with the IP specified in this field. This feature depends on whether the underlying cloud-provider supports specifying the loadBalancerIP when a load balancer is created. This field will be ignored if the cloud-provider does not support the feature.",
|
||||
"type": "string"
|
||||
|
@ -3728,6 +3728,20 @@ type ServiceSpec struct {
|
||||
// This field is alpha-level and is only honored by servers that enable the ServiceLBNodePortControl feature.
|
||||
// +optional
|
||||
AllocateLoadBalancerNodePorts *bool
|
||||
|
||||
// loadBalancerClass is the class of the load balancer implementation this Service belongs to.
|
||||
// If specified, the value of this field must be a label-style identifier, with an optional prefix,
|
||||
// e.g. "internal-vip" or "example.com/internal-vip". Unprefixed names are reserved for end-users.
|
||||
// This field can only be set when the Service type is 'LoadBalancer'. If not set, the default load
|
||||
// balancer implementation is used, today this is typically done through the cloud provider integration,
|
||||
// but should apply for any default implementation. If set, it is assumed that a load balancer
|
||||
// implementation is watching for Services with a matching class. Any default load balancer
|
||||
// implementation (e.g. cloud providers) should ignore Services that set this field.
|
||||
// This field can only be set when creating or updating a Service to type 'LoadBalancer'.
|
||||
// Once set, it can not be changed. This field will be wiped when a service is updated to a non 'LoadBalancer' type.
|
||||
// featureGate=LoadBalancerClass
|
||||
// +optional
|
||||
LoadBalancerClass *string
|
||||
}
|
||||
|
||||
// ServicePort represents the port on which the service is exposed
|
||||
|
2
pkg/apis/core/v1/zz_generated.conversion.go
generated
2
pkg/apis/core/v1/zz_generated.conversion.go
generated
@ -7635,6 +7635,7 @@ func autoConvert_v1_ServiceSpec_To_core_ServiceSpec(in *v1.ServiceSpec, out *cor
|
||||
out.IPFamilies = *(*[]core.IPFamily)(unsafe.Pointer(&in.IPFamilies))
|
||||
out.IPFamilyPolicy = (*core.IPFamilyPolicyType)(unsafe.Pointer(in.IPFamilyPolicy))
|
||||
out.AllocateLoadBalancerNodePorts = (*bool)(unsafe.Pointer(in.AllocateLoadBalancerNodePorts))
|
||||
out.LoadBalancerClass = (*string)(unsafe.Pointer(in.LoadBalancerClass))
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -7662,6 +7663,7 @@ func autoConvert_core_ServiceSpec_To_v1_ServiceSpec(in *core.ServiceSpec, out *v
|
||||
out.PublishNotReadyAddresses = in.PublishNotReadyAddresses
|
||||
out.TopologyKeys = *(*[]string)(unsafe.Pointer(&in.TopologyKeys))
|
||||
out.AllocateLoadBalancerNodePorts = (*bool)(unsafe.Pointer(in.AllocateLoadBalancerNodePorts))
|
||||
out.LoadBalancerClass = (*string)(unsafe.Pointer(in.LoadBalancerClass))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -122,6 +122,15 @@ func ValidateDNS1123Label(value string, fldPath *field.Path) field.ErrorList {
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateQualifiedName validates if name is what Kubernetes calls a "qualified name".
|
||||
func ValidateQualifiedName(value string, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
for _, msg := range validation.IsQualifiedName(value) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, value, msg))
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateDNS1123Subdomain validates that a name is a proper DNS subdomain.
|
||||
func ValidateDNS1123Subdomain(value string, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
@ -4362,6 +4371,9 @@ func ValidateService(service *core.Service) field.ErrorList {
|
||||
}
|
||||
}
|
||||
|
||||
// validate LoadBalancerClass field
|
||||
allErrs = append(allErrs, validateLoadBalancerClassField(nil, service)...)
|
||||
|
||||
// external traffic fields
|
||||
allErrs = append(allErrs, validateServiceExternalTrafficFieldsValue(service)...)
|
||||
return allErrs
|
||||
@ -4475,6 +4487,9 @@ func ValidateServiceUpdate(service, oldService *core.Service) field.ErrorList {
|
||||
upgradeDowngradeIPFamiliesErrs := validateUpgradeDowngradeIPFamilies(oldService, service)
|
||||
allErrs = append(allErrs, upgradeDowngradeIPFamiliesErrs...)
|
||||
|
||||
upgradeDowngradeLoadBalancerClassErrs := validateLoadBalancerClassField(oldService, service)
|
||||
allErrs = append(allErrs, upgradeDowngradeLoadBalancerClassErrs...)
|
||||
|
||||
return append(allErrs, ValidateService(service)...)
|
||||
}
|
||||
|
||||
@ -6346,3 +6361,47 @@ func isHeadlessService(service *core.Service) bool {
|
||||
len(service.Spec.ClusterIPs) == 1 &&
|
||||
service.Spec.ClusterIPs[0] == core.ClusterIPNone
|
||||
}
|
||||
|
||||
// validateLoadBalancerClassField validation for loadBalancerClass
|
||||
func validateLoadBalancerClassField(oldService, service *core.Service) field.ErrorList {
|
||||
allErrs := make(field.ErrorList, 0)
|
||||
if oldService != nil {
|
||||
// validate update op
|
||||
if isTypeLoadBalancer(oldService) && isTypeLoadBalancer(service) {
|
||||
// old and new are both LoadBalancer
|
||||
if !sameLoadBalancerClass(oldService, service) {
|
||||
// can't change loadBalancerClass
|
||||
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "loadBalancerClass"), service.Spec.LoadBalancerClass, "may not change once set"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isTypeLoadBalancer(service) {
|
||||
// check LoadBalancerClass format
|
||||
if service.Spec.LoadBalancerClass != nil {
|
||||
allErrs = append(allErrs, ValidateQualifiedName(*service.Spec.LoadBalancerClass, field.NewPath("spec", "loadBalancerClass"))...)
|
||||
}
|
||||
} else {
|
||||
// check if LoadBalancerClass set for non LoadBalancer type of service
|
||||
if service.Spec.LoadBalancerClass != nil {
|
||||
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "loadBalancerClass"), "may only be used when `type` is 'LoadBalancer'"))
|
||||
}
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// isTypeLoadBalancer tests service type is loadBalancer or not
|
||||
func isTypeLoadBalancer(service *core.Service) bool {
|
||||
return service.Spec.Type == core.ServiceTypeLoadBalancer
|
||||
}
|
||||
|
||||
// sameLoadBalancerClass check two services have the same loadBalancerClass or not
|
||||
func sameLoadBalancerClass(oldService, service *core.Service) bool {
|
||||
if oldService.Spec.LoadBalancerClass == nil && service.Spec.LoadBalancerClass == nil {
|
||||
return true
|
||||
}
|
||||
if oldService.Spec.LoadBalancerClass == nil || service.Spec.LoadBalancerClass == nil {
|
||||
return false
|
||||
}
|
||||
return *oldService.Spec.LoadBalancerClass == *service.Spec.LoadBalancerClass
|
||||
}
|
||||
|
@ -11295,6 +11295,30 @@ func TestValidateServiceCreate(t *testing.T) {
|
||||
},
|
||||
numErrs: 1,
|
||||
},
|
||||
{
|
||||
name: "valid LoadBalancerClass when type is LoadBalancer",
|
||||
tweakSvc: func(s *core.Service) {
|
||||
s.Spec.Type = core.ServiceTypeLoadBalancer
|
||||
s.Spec.LoadBalancerClass = utilpointer.StringPtr("test.com/test-load-balancer-class")
|
||||
},
|
||||
numErrs: 0,
|
||||
},
|
||||
{
|
||||
name: "invalid LoadBalancerClass",
|
||||
tweakSvc: func(s *core.Service) {
|
||||
s.Spec.Type = core.ServiceTypeLoadBalancer
|
||||
s.Spec.LoadBalancerClass = utilpointer.StringPtr("Bad/LoadBalancerClass")
|
||||
},
|
||||
numErrs: 1,
|
||||
},
|
||||
{
|
||||
name: "invalid: set LoadBalancerClass when type is not LoadBalancer",
|
||||
tweakSvc: func(s *core.Service) {
|
||||
s.Spec.Type = core.ServiceTypeClusterIP
|
||||
s.Spec.LoadBalancerClass = utilpointer.StringPtr("test.com/test-load-balancer-class")
|
||||
},
|
||||
numErrs: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@ -13672,6 +13696,143 @@ func TestValidateServiceUpdate(t *testing.T) {
|
||||
},
|
||||
numErrs: 1,
|
||||
},
|
||||
{
|
||||
name: "update LoadBalancer type of service without change LoadBalancerClass",
|
||||
tweakSvc: func(oldSvc, newSvc *core.Service) {
|
||||
oldSvc.Spec.Type = core.ServiceTypeLoadBalancer
|
||||
oldSvc.Spec.LoadBalancerClass = utilpointer.StringPtr("test.com/test-old")
|
||||
|
||||
newSvc.Spec.Type = core.ServiceTypeLoadBalancer
|
||||
newSvc.Spec.LoadBalancerClass = utilpointer.StringPtr("test.com/test-old")
|
||||
},
|
||||
numErrs: 0,
|
||||
},
|
||||
{
|
||||
name: "invalid: change LoadBalancerClass when update service",
|
||||
tweakSvc: func(oldSvc, newSvc *core.Service) {
|
||||
oldSvc.Spec.Type = core.ServiceTypeLoadBalancer
|
||||
oldSvc.Spec.LoadBalancerClass = utilpointer.StringPtr("test.com/test-old")
|
||||
|
||||
newSvc.Spec.Type = core.ServiceTypeLoadBalancer
|
||||
newSvc.Spec.LoadBalancerClass = utilpointer.StringPtr("test.com/test-new")
|
||||
},
|
||||
numErrs: 1,
|
||||
},
|
||||
{
|
||||
name: "invalid: unset LoadBalancerClass when update service",
|
||||
tweakSvc: func(oldSvc, newSvc *core.Service) {
|
||||
oldSvc.Spec.Type = core.ServiceTypeLoadBalancer
|
||||
oldSvc.Spec.LoadBalancerClass = utilpointer.StringPtr("test.com/test-old")
|
||||
|
||||
newSvc.Spec.Type = core.ServiceTypeLoadBalancer
|
||||
newSvc.Spec.LoadBalancerClass = nil
|
||||
},
|
||||
numErrs: 1,
|
||||
},
|
||||
{
|
||||
name: "invalid: set LoadBalancerClass when update service",
|
||||
tweakSvc: func(oldSvc, newSvc *core.Service) {
|
||||
oldSvc.Spec.Type = core.ServiceTypeLoadBalancer
|
||||
oldSvc.Spec.LoadBalancerClass = nil
|
||||
|
||||
newSvc.Spec.Type = core.ServiceTypeLoadBalancer
|
||||
newSvc.Spec.LoadBalancerClass = utilpointer.StringPtr("test.com/test-new")
|
||||
},
|
||||
numErrs: 1,
|
||||
},
|
||||
{
|
||||
name: "update to LoadBalancer type of service with valid LoadBalancerClass",
|
||||
tweakSvc: func(oldSvc, newSvc *core.Service) {
|
||||
oldSvc.Spec.Type = core.ServiceTypeClusterIP
|
||||
|
||||
newSvc.Spec.Type = core.ServiceTypeLoadBalancer
|
||||
newSvc.Spec.LoadBalancerClass = utilpointer.StringPtr("test.com/test-load-balancer-class")
|
||||
},
|
||||
numErrs: 0,
|
||||
},
|
||||
{
|
||||
name: "update to LoadBalancer type of service without LoadBalancerClass",
|
||||
tweakSvc: func(oldSvc, newSvc *core.Service) {
|
||||
oldSvc.Spec.Type = core.ServiceTypeClusterIP
|
||||
|
||||
newSvc.Spec.Type = core.ServiceTypeLoadBalancer
|
||||
newSvc.Spec.LoadBalancerClass = nil
|
||||
},
|
||||
numErrs: 0,
|
||||
},
|
||||
{
|
||||
name: "invalid: set invalid LoadBalancerClass when update service to LoadBalancer",
|
||||
tweakSvc: func(oldSvc, newSvc *core.Service) {
|
||||
oldSvc.Spec.Type = core.ServiceTypeClusterIP
|
||||
|
||||
newSvc.Spec.Type = core.ServiceTypeLoadBalancer
|
||||
newSvc.Spec.LoadBalancerClass = utilpointer.StringPtr("Bad/LoadBalancerclass")
|
||||
},
|
||||
numErrs: 2,
|
||||
},
|
||||
{
|
||||
name: "invalid: set LoadBalancerClass when update service to non LoadBalancer type of service",
|
||||
tweakSvc: func(oldSvc, newSvc *core.Service) {
|
||||
oldSvc.Spec.Type = core.ServiceTypeClusterIP
|
||||
|
||||
newSvc.Spec.Type = core.ServiceTypeClusterIP
|
||||
newSvc.Spec.LoadBalancerClass = utilpointer.StringPtr("test.com/test-load-balancer-class")
|
||||
},
|
||||
numErrs: 2,
|
||||
},
|
||||
{
|
||||
name: "invalid: set LoadBalancerClass when update service to non LoadBalancer type of service",
|
||||
tweakSvc: func(oldSvc, newSvc *core.Service) {
|
||||
oldSvc.Spec.Type = core.ServiceTypeExternalName
|
||||
|
||||
newSvc.Spec.Type = core.ServiceTypeExternalName
|
||||
newSvc.Spec.LoadBalancerClass = utilpointer.StringPtr("test.com/test-load-balancer-class")
|
||||
},
|
||||
numErrs: 3,
|
||||
},
|
||||
{
|
||||
name: "invalid: set LoadBalancerClass when update service to non LoadBalancer type of service",
|
||||
tweakSvc: func(oldSvc, newSvc *core.Service) {
|
||||
oldSvc.Spec.Type = core.ServiceTypeNodePort
|
||||
|
||||
newSvc.Spec.Type = core.ServiceTypeNodePort
|
||||
newSvc.Spec.LoadBalancerClass = utilpointer.StringPtr("test.com/test-load-balancer-class")
|
||||
},
|
||||
numErrs: 2,
|
||||
},
|
||||
{
|
||||
name: "invalid: set LoadBalancerClass when update from LoadBalancer service to non LoadBalancer type of service",
|
||||
tweakSvc: func(oldSvc, newSvc *core.Service) {
|
||||
oldSvc.Spec.Type = core.ServiceTypeLoadBalancer
|
||||
oldSvc.Spec.LoadBalancerClass = utilpointer.StringPtr("test.com/test-load-balancer-class")
|
||||
|
||||
newSvc.Spec.Type = core.ServiceTypeClusterIP
|
||||
newSvc.Spec.LoadBalancerClass = utilpointer.StringPtr("test.com/test-load-balancer-class")
|
||||
},
|
||||
numErrs: 2,
|
||||
},
|
||||
{
|
||||
name: "invalid: set LoadBalancerClass when update from LoadBalancer service to non LoadBalancer type of service",
|
||||
tweakSvc: func(oldSvc, newSvc *core.Service) {
|
||||
oldSvc.Spec.Type = core.ServiceTypeLoadBalancer
|
||||
oldSvc.Spec.LoadBalancerClass = utilpointer.StringPtr("test.com/test-load-balancer-class")
|
||||
|
||||
newSvc.Spec.Type = core.ServiceTypeExternalName
|
||||
newSvc.Spec.LoadBalancerClass = utilpointer.StringPtr("test.com/test-load-balancer-class")
|
||||
},
|
||||
numErrs: 3,
|
||||
},
|
||||
{
|
||||
name: "invalid: set LoadBalancerClass when update from LoadBalancer service to non LoadBalancer type of service",
|
||||
tweakSvc: func(oldSvc, newSvc *core.Service) {
|
||||
oldSvc.Spec.Type = core.ServiceTypeLoadBalancer
|
||||
oldSvc.Spec.LoadBalancerClass = utilpointer.StringPtr("test.com/test-load-balancer-class")
|
||||
|
||||
newSvc.Spec.Type = core.ServiceTypeNodePort
|
||||
newSvc.Spec.LoadBalancerClass = utilpointer.StringPtr("test.com/test-load-balancer-class")
|
||||
},
|
||||
numErrs: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
5
pkg/apis/core/zz_generated.deepcopy.go
generated
5
pkg/apis/core/zz_generated.deepcopy.go
generated
@ -5330,6 +5330,11 @@ func (in *ServiceSpec) DeepCopyInto(out *ServiceSpec) {
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.LoadBalancerClass != nil {
|
||||
in, out := &in.LoadBalancerClass, &out.LoadBalancerClass
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -682,6 +682,11 @@ const (
|
||||
//
|
||||
// Allow specifying NamespaceSelector in PodAffinityTerm.
|
||||
PodAffinityNamespaceSelector featuregate.Feature = "PodAffinityNamespaceSelector"
|
||||
// owner: @andrewsykim @xudongliuharold
|
||||
// alpha: v1.21
|
||||
//
|
||||
// Enable support multiple Service "type: LoadBalancer" implementations in a cluster by specifying LoadBalancerClass
|
||||
ServiceLoadBalancerClass featuregate.Feature = "ServiceLoadBalancerClass"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -785,6 +790,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
|
||||
RunAsGroup: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.22
|
||||
PodDeletionCost: {Default: false, PreRelease: featuregate.Alpha},
|
||||
PodAffinityNamespaceSelector: {Default: false, PreRelease: featuregate.Alpha},
|
||||
ServiceLoadBalancerClass: {Default: false, PreRelease: featuregate.Alpha},
|
||||
|
||||
// inherited features from generic apiserver, relisted here to get a conflict if it is changed
|
||||
// unintentionally on either side:
|
||||
|
@ -173,6 +173,13 @@ func dropServiceDisabledFields(newSvc *api.Service, oldSvc *api.Service) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Drop LoadBalancerClass if LoadBalancerClass is not enabled
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(features.ServiceLoadBalancerClass) {
|
||||
if !loadBalancerClassInUse(oldSvc) {
|
||||
newSvc.Spec.LoadBalancerClass = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// returns true if svc.Spec.AllocateLoadBalancerNodePorts field is in use
|
||||
@ -225,6 +232,14 @@ func loadBalancerPortsInUse(svc *api.Service) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// returns true if svc.Spec.LoadBalancerClass field is in use
|
||||
func loadBalancerClassInUse(svc *api.Service) bool {
|
||||
if svc == nil {
|
||||
return false
|
||||
}
|
||||
return svc.Spec.LoadBalancerClass != nil
|
||||
}
|
||||
|
||||
type serviceStatusStrategy struct {
|
||||
Strategy
|
||||
}
|
||||
@ -390,6 +405,12 @@ func dropTypeDependentFields(newSvc *api.Service, oldSvc *api.Service) {
|
||||
}
|
||||
}
|
||||
|
||||
// If a user is switching to a type that doesn't need LoadBalancerClass AND they did not change
|
||||
// this field, it is safe to drop it.
|
||||
if canSetLoadBalancerClass(oldSvc) && !canSetLoadBalancerClass(newSvc) && sameLoadBalancerClass(oldSvc, newSvc) {
|
||||
newSvc.Spec.LoadBalancerClass = nil
|
||||
}
|
||||
|
||||
// NOTE: there are other fields like `selector` which we could wipe.
|
||||
// Historically we did not wipe them and they are not allocated from
|
||||
// finite pools, so we are (currently) choosing to leave them alone.
|
||||
@ -464,6 +485,20 @@ func sameHCNodePort(oldSvc, newSvc *api.Service) bool {
|
||||
return oldSvc.Spec.HealthCheckNodePort == newSvc.Spec.HealthCheckNodePort
|
||||
}
|
||||
|
||||
func canSetLoadBalancerClass(svc *api.Service) bool {
|
||||
return svc.Spec.Type == api.ServiceTypeLoadBalancer
|
||||
}
|
||||
|
||||
func sameLoadBalancerClass(oldSvc, newSvc *api.Service) bool {
|
||||
if (oldSvc.Spec.LoadBalancerClass == nil) != (newSvc.Spec.LoadBalancerClass == nil) {
|
||||
return false
|
||||
}
|
||||
if oldSvc.Spec.LoadBalancerClass == nil {
|
||||
return true // both are nil
|
||||
}
|
||||
return *oldSvc.Spec.LoadBalancerClass == *newSvc.Spec.LoadBalancerClass
|
||||
}
|
||||
|
||||
// this func allows user to downgrade a service by just changing
|
||||
// IPFamilyPolicy to SingleStack
|
||||
func trimFieldsForDualStackDowngrade(newService, oldService *api.Service) {
|
||||
|
@ -249,18 +249,27 @@ func makeServiceWithPorts(ports []api.PortStatus) *api.Service {
|
||||
}
|
||||
}
|
||||
|
||||
func makeServiceWithLoadBalancerClass(loadBalancerClass *string) *api.Service {
|
||||
return &api.Service{
|
||||
Spec: api.ServiceSpec{
|
||||
LoadBalancerClass: loadBalancerClass,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestDropDisabledField(t *testing.T) {
|
||||
requireDualStack := api.IPFamilyPolicyRequireDualStack
|
||||
preferDualStack := api.IPFamilyPolicyPreferDualStack
|
||||
singleStack := api.IPFamilyPolicySingleStack
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
enableDualStack bool
|
||||
enableMixedProtocol bool
|
||||
svc *api.Service
|
||||
oldSvc *api.Service
|
||||
compareSvc *api.Service
|
||||
name string
|
||||
enableDualStack bool
|
||||
enableMixedProtocol bool
|
||||
enableLoadBalancerClass bool
|
||||
svc *api.Service
|
||||
oldSvc *api.Service
|
||||
compareSvc *api.Service
|
||||
}{
|
||||
{
|
||||
name: "not dual stack, field not used",
|
||||
@ -434,12 +443,70 @@ func TestDropDisabledField(t *testing.T) {
|
||||
oldSvc: makeServiceWithPorts([]api.PortStatus{}),
|
||||
compareSvc: makeServiceWithPorts(nil),
|
||||
},
|
||||
/* svc.Spec.LoadBalancerClass */
|
||||
{
|
||||
name: "loadBalancerClass not enabled, field not used in old, not used in new",
|
||||
enableLoadBalancerClass: false,
|
||||
svc: makeServiceWithLoadBalancerClass(nil),
|
||||
oldSvc: makeServiceWithLoadBalancerClass(nil),
|
||||
compareSvc: makeServiceWithLoadBalancerClass(nil),
|
||||
},
|
||||
{
|
||||
name: "loadBalancerClass not enabled, field used in old and in new",
|
||||
enableLoadBalancerClass: false,
|
||||
svc: makeServiceWithLoadBalancerClass(utilpointer.StringPtr("test.com/test")),
|
||||
oldSvc: makeServiceWithLoadBalancerClass(utilpointer.StringPtr("test.com/test")),
|
||||
compareSvc: makeServiceWithLoadBalancerClass(utilpointer.StringPtr("test.com/test")),
|
||||
},
|
||||
{
|
||||
name: "loadBalancerClass not enabled, field not used in old, used in new",
|
||||
enableLoadBalancerClass: false,
|
||||
svc: makeServiceWithLoadBalancerClass(utilpointer.StringPtr("test.com/test")),
|
||||
oldSvc: makeServiceWithLoadBalancerClass(nil),
|
||||
compareSvc: makeServiceWithLoadBalancerClass(nil),
|
||||
},
|
||||
{
|
||||
name: "loadBalancerClass not enabled, field used in old, not used in new",
|
||||
enableLoadBalancerClass: false,
|
||||
svc: makeServiceWithLoadBalancerClass(utilpointer.StringPtr("test.com/test")),
|
||||
oldSvc: makeServiceWithLoadBalancerClass(utilpointer.StringPtr("test.com/test")),
|
||||
compareSvc: makeServiceWithLoadBalancerClass(utilpointer.StringPtr("test.com/test")),
|
||||
},
|
||||
{
|
||||
name: "loadBalancerClass enabled, field not used in old, not used in new",
|
||||
enableLoadBalancerClass: true,
|
||||
svc: makeServiceWithLoadBalancerClass(nil),
|
||||
oldSvc: makeServiceWithLoadBalancerClass(nil),
|
||||
compareSvc: makeServiceWithLoadBalancerClass(nil),
|
||||
},
|
||||
{
|
||||
name: "loadBalancerClass enabled, field used in old and in new",
|
||||
enableLoadBalancerClass: true,
|
||||
svc: makeServiceWithLoadBalancerClass(utilpointer.StringPtr("test.com/test")),
|
||||
oldSvc: makeServiceWithLoadBalancerClass(utilpointer.StringPtr("test.com/test")),
|
||||
compareSvc: makeServiceWithLoadBalancerClass(utilpointer.StringPtr("test.com/test")),
|
||||
},
|
||||
{
|
||||
name: "loadBalancerClass enabled, field not used in old, used in new",
|
||||
enableLoadBalancerClass: true,
|
||||
svc: makeServiceWithLoadBalancerClass(utilpointer.StringPtr("test.com/test")),
|
||||
oldSvc: makeServiceWithLoadBalancerClass(nil),
|
||||
compareSvc: makeServiceWithLoadBalancerClass(utilpointer.StringPtr("test.com/test")),
|
||||
},
|
||||
{
|
||||
name: "loadBalancerClass enabled, field used in old, not used in new",
|
||||
enableLoadBalancerClass: true,
|
||||
svc: makeServiceWithLoadBalancerClass(nil),
|
||||
oldSvc: makeServiceWithLoadBalancerClass(utilpointer.StringPtr("test.com/test")),
|
||||
compareSvc: makeServiceWithLoadBalancerClass(nil),
|
||||
},
|
||||
/* add more tests for other dropped fields as needed */
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
func() {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.IPv6DualStack, tc.enableDualStack)()
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.MixedProtocolLBService, tc.enableMixedProtocol)()
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ServiceLoadBalancerClass, tc.enableLoadBalancerClass)()
|
||||
old := tc.oldSvc.DeepCopy()
|
||||
|
||||
// to test against user using IPFamily not set on cluster
|
||||
@ -685,6 +752,15 @@ func TestDropTypeDependentFields(t *testing.T) {
|
||||
clearAllocateLoadBalancerNodePorts := func(svc *api.Service) {
|
||||
svc.Spec.AllocateLoadBalancerNodePorts = nil
|
||||
}
|
||||
setLoadBalancerClass := func(svc *api.Service) {
|
||||
svc.Spec.LoadBalancerClass = utilpointer.StringPtr("test-load-balancer-class")
|
||||
}
|
||||
clearLoadBalancerClass := func(svc *api.Service) {
|
||||
svc.Spec.LoadBalancerClass = nil
|
||||
}
|
||||
changeLoadBalancerClass := func(svc *api.Service) {
|
||||
svc.Spec.LoadBalancerClass = utilpointer.StringPtr("test-load-balancer-class-changed")
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@ -812,6 +888,36 @@ func TestDropTypeDependentFields(t *testing.T) {
|
||||
svc: makeValidServiceCustom(setTypeLoadBalancer, setAllocateLoadBalancerNodePortsFalse),
|
||||
patch: patches(setTypeNodePort, setAllocateLoadBalancerNodePortsTrue),
|
||||
expect: makeValidServiceCustom(setTypeNodePort, setAllocateLoadBalancerNodePortsTrue),
|
||||
}, { // loadBalancerClass cases
|
||||
name: "clear loadBalancerClass when set Service type LoadBalancer -> non LoadBalancer",
|
||||
svc: makeValidServiceCustom(setTypeLoadBalancer, setLoadBalancerClass),
|
||||
patch: setTypeClusterIP,
|
||||
expect: makeValidServiceCustom(setTypeClusterIP, clearLoadBalancerClass),
|
||||
}, {
|
||||
name: "update loadBalancerClass load balancer class name",
|
||||
svc: makeValidServiceCustom(setTypeLoadBalancer, setLoadBalancerClass),
|
||||
patch: changeLoadBalancerClass,
|
||||
expect: makeValidServiceCustom(setTypeLoadBalancer, changeLoadBalancerClass),
|
||||
}, {
|
||||
name: "clear load balancer class name",
|
||||
svc: makeValidServiceCustom(setTypeLoadBalancer, setLoadBalancerClass),
|
||||
patch: clearLoadBalancerClass,
|
||||
expect: makeValidServiceCustom(setTypeLoadBalancer, clearLoadBalancerClass),
|
||||
}, {
|
||||
name: "change service type and load balancer class",
|
||||
svc: makeValidServiceCustom(setTypeLoadBalancer, setLoadBalancerClass),
|
||||
patch: patches(setTypeClusterIP, changeLoadBalancerClass),
|
||||
expect: makeValidServiceCustom(setTypeClusterIP, changeLoadBalancerClass),
|
||||
}, {
|
||||
name: "change service type to load balancer and set load balancer class",
|
||||
svc: makeValidServiceCustom(setTypeClusterIP),
|
||||
patch: patches(setTypeLoadBalancer, setLoadBalancerClass),
|
||||
expect: makeValidServiceCustom(setTypeLoadBalancer, setLoadBalancerClass),
|
||||
}, {
|
||||
name: "don't clear load balancer class for Type=LoadBalancer",
|
||||
svc: makeValidServiceCustom(setTypeLoadBalancer, setLoadBalancerClass),
|
||||
patch: nil,
|
||||
expect: makeValidServiceCustom(setTypeLoadBalancer, setLoadBalancerClass),
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@ -846,6 +952,9 @@ func TestDropTypeDependentFields(t *testing.T) {
|
||||
if !reflect.DeepEqual(result.Spec.AllocateLoadBalancerNodePorts, tc.expect.Spec.AllocateLoadBalancerNodePorts) {
|
||||
t.Errorf("failed %q: expected AllocateLoadBalancerNodePorts %v, got %v", tc.name, tc.expect.Spec.AllocateLoadBalancerNodePorts, result.Spec.AllocateLoadBalancerNodePorts)
|
||||
}
|
||||
if !reflect.DeepEqual(result.Spec.LoadBalancerClass, tc.expect.Spec.LoadBalancerClass) {
|
||||
t.Errorf("failed %q: expected LoadBalancerClass %v, got %v", tc.name, tc.expect.Spec.LoadBalancerClass, result.Spec.LoadBalancerClass)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
1804
staging/src/k8s.io/api/core/v1/generated.pb.go
generated
1804
staging/src/k8s.io/api/core/v1/generated.pb.go
generated
File diff suppressed because it is too large
Load Diff
@ -5034,6 +5034,20 @@ message ServiceSpec {
|
||||
// This field is alpha-level and is only honored by servers that enable the ServiceLBNodePortControl feature.
|
||||
// +optional
|
||||
optional bool allocateLoadBalancerNodePorts = 20;
|
||||
|
||||
// loadBalancerClass is the class of the load balancer implementation this Service belongs to.
|
||||
// If specified, the value of this field must be a label-style identifier, with an optional prefix,
|
||||
// e.g. "internal-vip" or "example.com/internal-vip". Unprefixed names are reserved for end-users.
|
||||
// This field can only be set when the Service type is 'LoadBalancer'. If not set, the default load
|
||||
// balancer implementation is used, today this is typically done through the cloud provider integration,
|
||||
// but should apply for any default implementation. If set, it is assumed that a load balancer
|
||||
// implementation is watching for Services with a matching class. Any default load balancer
|
||||
// implementation (e.g. cloud providers) should ignore Services that set this field.
|
||||
// This field can only be set when creating or updating a Service to type 'LoadBalancer'.
|
||||
// Once set, it can not be changed. This field will be wiped when a service is updated to a non 'LoadBalancer' type.
|
||||
// featureGate=LoadBalancerClass
|
||||
// +optional
|
||||
optional string loadBalancerClass = 21;
|
||||
}
|
||||
|
||||
// ServiceStatus represents the current status of a service.
|
||||
|
@ -4261,6 +4261,20 @@ type ServiceSpec struct {
|
||||
// This field is alpha-level and is only honored by servers that enable the ServiceLBNodePortControl feature.
|
||||
// +optional
|
||||
AllocateLoadBalancerNodePorts *bool `json:"allocateLoadBalancerNodePorts,omitempty" protobuf:"bytes,20,opt,name=allocateLoadBalancerNodePorts"`
|
||||
|
||||
// loadBalancerClass is the class of the load balancer implementation this Service belongs to.
|
||||
// If specified, the value of this field must be a label-style identifier, with an optional prefix,
|
||||
// e.g. "internal-vip" or "example.com/internal-vip". Unprefixed names are reserved for end-users.
|
||||
// This field can only be set when the Service type is 'LoadBalancer'. If not set, the default load
|
||||
// balancer implementation is used, today this is typically done through the cloud provider integration,
|
||||
// but should apply for any default implementation. If set, it is assumed that a load balancer
|
||||
// implementation is watching for Services with a matching class. Any default load balancer
|
||||
// implementation (e.g. cloud providers) should ignore Services that set this field.
|
||||
// This field can only be set when creating or updating a Service to type 'LoadBalancer'.
|
||||
// Once set, it can not be changed. This field will be wiped when a service is updated to a non 'LoadBalancer' type.
|
||||
// featureGate=LoadBalancerClass
|
||||
// +optional
|
||||
LoadBalancerClass *string `json:"loadBalancerClass,omitempty" protobuf:"bytes,21,opt,name=loadBalancerClass"`
|
||||
}
|
||||
|
||||
// ServicePort contains information on service's port.
|
||||
|
@ -2257,6 +2257,7 @@ var map_ServiceSpec = map[string]string{
|
||||
"ipFamilies": "IPFamilies is a list of IP families (e.g. IPv4, IPv6) assigned to this service, and is gated by the \"IPv6DualStack\" feature gate. This field is usually assigned automatically based on cluster configuration and the ipFamilyPolicy field. If this field is specified manually, the requested family is available in the cluster, and ipFamilyPolicy allows it, it will be used; otherwise creation of the service will fail. This field is conditionally mutable: it allows for adding or removing a secondary IP family, but it does not allow changing the primary IP family of the Service. Valid values are \"IPv4\" and \"IPv6\". This field only applies to Services of types ClusterIP, NodePort, and LoadBalancer, and does apply to \"headless\" services. This field will be wiped when updating a Service to type ExternalName.\n\nThis field may hold a maximum of two entries (dual-stack families, in either order). These families must correspond to the values of the clusterIPs field, if specified. Both clusterIPs and ipFamilies are governed by the ipFamilyPolicy field.",
|
||||
"ipFamilyPolicy": "IPFamilyPolicy represents the dual-stack-ness requested or required by this Service, and is gated by the \"IPv6DualStack\" feature gate. If there is no value provided, then this field will be set to SingleStack. Services can be \"SingleStack\" (a single IP family), \"PreferDualStack\" (two IP families on dual-stack configured clusters or a single IP family on single-stack clusters), or \"RequireDualStack\" (two IP families on dual-stack configured clusters, otherwise fail). The ipFamilies and clusterIPs fields depend on the value of this field. This field will be wiped when updating a service to type ExternalName.",
|
||||
"allocateLoadBalancerNodePorts": "allocateLoadBalancerNodePorts defines if NodePorts will be automatically allocated for services with type LoadBalancer. Default is \"true\". It may be set to \"false\" if the cluster load-balancer does not rely on NodePorts. allocateLoadBalancerNodePorts may only be set for services with type LoadBalancer and will be cleared if the type is changed to any other type. This field is alpha-level and is only honored by servers that enable the ServiceLBNodePortControl feature.",
|
||||
"loadBalancerClass": "loadBalancerClass is the class of the load balancer implementation this Service belongs to. If specified, the value of this field must be a label-style identifier, with an optional prefix, e.g. \"internal-vip\" or \"example.com/internal-vip\". Unprefixed names are reserved for end-users. This field can only be set when the Service type is 'LoadBalancer'. If not set, the default load balancer implementation is used, today this is typically done through the cloud provider integration, but should apply for any default implementation. If set, it is assumed that a load balancer implementation is watching for Services with a matching class. Any default load balancer implementation (e.g. cloud providers) should ignore Services that set this field. This field can only be set when creating or updating a Service to type 'LoadBalancer'. Once set, it can not be changed. This field will be wiped when a service is updated to a non 'LoadBalancer' type. featureGate=LoadBalancerClass",
|
||||
}
|
||||
|
||||
func (ServiceSpec) SwaggerDoc() map[string]string {
|
||||
|
@ -5345,6 +5345,11 @@ func (in *ServiceSpec) DeepCopyInto(out *ServiceSpec) {
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.LoadBalancerClass != nil {
|
||||
in, out := &in.LoadBalancerClass, &out.LoadBalancerClass
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -83,19 +83,20 @@
|
||||
"斬³;Ơ歿:狞夌碕ʂɭîcP$Iņɖ"
|
||||
],
|
||||
"ipFamilyPolicy": "9ȫŚ",
|
||||
"allocateLoadBalancerNodePorts": true
|
||||
"allocateLoadBalancerNodePorts": true,
|
||||
"loadBalancerClass": "31"
|
||||
},
|
||||
"status": {
|
||||
"loadBalancer": {
|
||||
"ingress": [
|
||||
{
|
||||
"ip": "31",
|
||||
"hostname": "32",
|
||||
"ip": "32",
|
||||
"hostname": "33",
|
||||
"ports": [
|
||||
{
|
||||
"port": -907310967,
|
||||
"protocol": "喂ƈ斎AO6",
|
||||
"error": "33"
|
||||
"error": "34"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -103,12 +104,12 @@
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"type": "34",
|
||||
"type": "35",
|
||||
"status": "C",
|
||||
"observedGeneration": -2492120148461555858,
|
||||
"lastTransitionTime": "2392-12-09T15:37:55Z",
|
||||
"reason": "35",
|
||||
"message": "36"
|
||||
"reason": "36",
|
||||
"message": "37"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Binary file not shown.
@ -42,6 +42,7 @@ spec:
|
||||
ipFamilies:
|
||||
- 斬³;Ơ歿:狞夌碕ʂɭîcP$Iņɖ
|
||||
ipFamilyPolicy: 9ȫŚ
|
||||
loadBalancerClass: "31"
|
||||
loadBalancerIP: "27"
|
||||
loadBalancerSourceRanges:
|
||||
- "28"
|
||||
@ -65,16 +66,16 @@ spec:
|
||||
status:
|
||||
conditions:
|
||||
- lastTransitionTime: "2392-12-09T15:37:55Z"
|
||||
message: "36"
|
||||
message: "37"
|
||||
observedGeneration: -2492120148461555858
|
||||
reason: "35"
|
||||
reason: "36"
|
||||
status: C
|
||||
type: "34"
|
||||
type: "35"
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- hostname: "32"
|
||||
ip: "31"
|
||||
- hostname: "33"
|
||||
ip: "32"
|
||||
ports:
|
||||
- error: "33"
|
||||
- error: "34"
|
||||
port: -907310967
|
||||
protocol: 喂ƈ斎AO6
|
||||
|
@ -43,6 +43,7 @@ type ServiceSpecApplyConfiguration struct {
|
||||
IPFamilies []corev1.IPFamily `json:"ipFamilies,omitempty"`
|
||||
IPFamilyPolicy *corev1.IPFamilyPolicyType `json:"ipFamilyPolicy,omitempty"`
|
||||
AllocateLoadBalancerNodePorts *bool `json:"allocateLoadBalancerNodePorts,omitempty"`
|
||||
LoadBalancerClass *string `json:"loadBalancerClass,omitempty"`
|
||||
}
|
||||
|
||||
// ServiceSpecApplyConfiguration constructs an declarative configuration of the ServiceSpec type for use with
|
||||
@ -215,3 +216,11 @@ func (b *ServiceSpecApplyConfiguration) WithAllocateLoadBalancerNodePorts(value
|
||||
b.AllocateLoadBalancerNodePorts = &value
|
||||
return b
|
||||
}
|
||||
|
||||
// WithLoadBalancerClass sets the LoadBalancerClass 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 LoadBalancerClass field is set to the value of the last call.
|
||||
func (b *ServiceSpecApplyConfiguration) WithLoadBalancerClass(value string) *ServiceSpecApplyConfiguration {
|
||||
b.LoadBalancerClass = &value
|
||||
return b
|
||||
}
|
||||
|
@ -820,7 +820,8 @@ func (s *Controller) lockedUpdateLoadBalancerHosts(service *v1.Service, hosts []
|
||||
}
|
||||
|
||||
func wantsLoadBalancer(service *v1.Service) bool {
|
||||
return service.Spec.Type == v1.ServiceTypeLoadBalancer
|
||||
// if LoadBalancerClass is set, the user does not want the default cloud-provider Load Balancer
|
||||
return service.Spec.Type == v1.ServiceTypeLoadBalancer && service.Spec.LoadBalancerClass == nil
|
||||
}
|
||||
|
||||
func loadBalancerIPsAreEqual(oldService, newService *v1.Service) bool {
|
||||
|
@ -43,6 +43,7 @@ import (
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
fakecloud "k8s.io/cloud-provider/fake"
|
||||
servicehelper "k8s.io/cloud-provider/service/helpers"
|
||||
utilpointer "k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
const region = "us-central"
|
||||
@ -223,6 +224,44 @@ func TestSyncLoadBalancerIfNeeded(t *testing.T) {
|
||||
expectPatchStatus: true,
|
||||
expectPatchFinalizer: true,
|
||||
},
|
||||
{
|
||||
desc: "service specifies loadBalancerClass",
|
||||
service: &v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "with-external-balancer",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Type: v1.ServiceTypeLoadBalancer,
|
||||
LoadBalancerClass: utilpointer.StringPtr("custom-loadbalancer"),
|
||||
},
|
||||
},
|
||||
expectOp: deleteLoadBalancer,
|
||||
expectCreateAttempt: false,
|
||||
expectPatchStatus: false,
|
||||
expectPatchFinalizer: false,
|
||||
},
|
||||
{
|
||||
desc: "service doesn't specify loadBalancerClass",
|
||||
service: &v1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "with-external-balancer",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.ServiceSpec{
|
||||
Ports: []v1.ServicePort{{
|
||||
Port: 80,
|
||||
Protocol: v1.ProtocolSCTP,
|
||||
}},
|
||||
Type: v1.ServiceTypeLoadBalancer,
|
||||
LoadBalancerClass: nil,
|
||||
},
|
||||
},
|
||||
expectOp: ensureLoadBalancer,
|
||||
expectCreateAttempt: true,
|
||||
expectPatchStatus: true,
|
||||
expectPatchFinalizer: true,
|
||||
},
|
||||
// Finalizer test cases below.
|
||||
{
|
||||
desc: "service with finalizer that no longer wants LB",
|
||||
|
Loading…
Reference in New Issue
Block a user