mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-09 12:07:47 +00:00
phase 2: api types + defaulting + validation + disabled fields handling
This commit is contained in:
parent
d7ecc85239
commit
5e8ccda71c
@ -279,6 +279,11 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
|
|||||||
types := []core.ServiceType{core.ServiceTypeClusterIP, core.ServiceTypeNodePort, core.ServiceTypeLoadBalancer}
|
types := []core.ServiceType{core.ServiceTypeClusterIP, core.ServiceTypeNodePort, core.ServiceTypeLoadBalancer}
|
||||||
*p = types[c.Rand.Intn(len(types))]
|
*p = types[c.Rand.Intn(len(types))]
|
||||||
},
|
},
|
||||||
|
func(p *core.IPFamily, c fuzz.Continue) {
|
||||||
|
types := []core.IPFamily{core.IPv4Protocol, core.IPv6Protocol}
|
||||||
|
selected := types[c.Rand.Intn(len(types))]
|
||||||
|
*p = selected
|
||||||
|
},
|
||||||
func(p *core.ServiceExternalTrafficPolicyType, c fuzz.Continue) {
|
func(p *core.ServiceExternalTrafficPolicyType, c fuzz.Continue) {
|
||||||
types := []core.ServiceExternalTrafficPolicyType{core.ServiceExternalTrafficPolicyTypeCluster, core.ServiceExternalTrafficPolicyTypeLocal}
|
types := []core.ServiceExternalTrafficPolicyType{core.ServiceExternalTrafficPolicyTypeCluster, core.ServiceExternalTrafficPolicyTypeLocal}
|
||||||
*p = types[c.Rand.Intn(len(types))]
|
*p = types[c.Rand.Intn(len(types))]
|
||||||
|
@ -3330,6 +3330,17 @@ type LoadBalancerIngress struct {
|
|||||||
Hostname string
|
Hostname string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IPFamily represents the IP Family (IPv4 or IPv6). This type is used
|
||||||
|
// to express the family of an IP expressed by a type (i.e. service.Spec.IPFamily)
|
||||||
|
type IPFamily string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// IPv4Protocol indicates that this IP is IPv4 protocol
|
||||||
|
IPv4Protocol IPFamily = "IPv4"
|
||||||
|
// IPv6Protocol indicates that this IP is IPv6 protocol
|
||||||
|
IPv6Protocol IPFamily = "IPv6"
|
||||||
|
)
|
||||||
|
|
||||||
// ServiceSpec describes the attributes that a user creates on a service
|
// ServiceSpec describes the attributes that a user creates on a service
|
||||||
type ServiceSpec struct {
|
type ServiceSpec struct {
|
||||||
// Type determines how the Service is exposed. Defaults to ClusterIP. Valid
|
// Type determines how the Service is exposed. Defaults to ClusterIP. Valid
|
||||||
@ -3430,6 +3441,16 @@ type ServiceSpec struct {
|
|||||||
// of peer discovery.
|
// of peer discovery.
|
||||||
// +optional
|
// +optional
|
||||||
PublishNotReadyAddresses bool
|
PublishNotReadyAddresses bool
|
||||||
|
|
||||||
|
// ipFamily specifies whether this Service has a preference for a particular IP family (e.g. IPv4 vs.
|
||||||
|
// IPv6). If a specific IP family is requested, the clusterIP field will be allocated from that family, if it is
|
||||||
|
// available in the cluster. If no IP family is requested, the cluster's primary IP family will be used.
|
||||||
|
// Other IP fields (loadBalancerIP, loadBalancerSourceRanges, externalIPs) and controllers which
|
||||||
|
// allocate external load-balancers should use the same IP family. Endpoints for this Service will be of
|
||||||
|
// this family. This field is immutable after creation. Assigning a ServiceIPFamily not available in the
|
||||||
|
// cluster (e.g. IPv6 in IPv4 only cluster) is an error condition and will fail during clusterIP assignment.
|
||||||
|
// +optional
|
||||||
|
IPFamily *IPFamily
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServicePort struct {
|
type ServicePort struct {
|
||||||
|
@ -24,6 +24,10 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/util/intstr"
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
"k8s.io/kubernetes/pkg/util/parsers"
|
"k8s.io/kubernetes/pkg/util/parsers"
|
||||||
utilpointer "k8s.io/utils/pointer"
|
utilpointer "k8s.io/utils/pointer"
|
||||||
|
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
"k8s.io/kubernetes/pkg/features"
|
||||||
|
utilnet "k8s.io/utils/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
func addDefaultingFuncs(scheme *runtime.Scheme) error {
|
func addDefaultingFuncs(scheme *runtime.Scheme) error {
|
||||||
@ -128,6 +132,33 @@ func SetDefaults_Service(obj *v1.Service) {
|
|||||||
obj.Spec.ExternalTrafficPolicy == "" {
|
obj.Spec.ExternalTrafficPolicy == "" {
|
||||||
obj.Spec.ExternalTrafficPolicy = v1.ServiceExternalTrafficPolicyTypeCluster
|
obj.Spec.ExternalTrafficPolicy = v1.ServiceExternalTrafficPolicyTypeCluster
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if dualstack feature gate is on then we need to default
|
||||||
|
// Spec.IPFamily correctly. This is to cover the case
|
||||||
|
// when an existing cluster have been converted to dualstack
|
||||||
|
// i.e. it already contain services with Spec.IPFamily==nil
|
||||||
|
if utilfeature.DefaultFeatureGate.Enabled(features.IPv6DualStack) &&
|
||||||
|
obj.Spec.Type != v1.ServiceTypeExternalName &&
|
||||||
|
obj.Spec.ClusterIP != "" && /*has an ip already set*/
|
||||||
|
obj.Spec.ClusterIP != "None" && /* not converting from ExternalName to other */
|
||||||
|
obj.Spec.IPFamily == nil /* family was not previously set */ {
|
||||||
|
|
||||||
|
// there is a change that the ClusterIP (set by user) is unparsable.
|
||||||
|
// in this case, the family will be set mistakenly to ipv4 (because
|
||||||
|
// the util function does not parse errors *sigh*). The error
|
||||||
|
// will be caught in validation which asserts the validity of the
|
||||||
|
// IP and the service object will not be persisted with the wrong IP
|
||||||
|
// family
|
||||||
|
|
||||||
|
ipv6 := v1.IPv6Protocol
|
||||||
|
ipv4 := v1.IPv4Protocol
|
||||||
|
if utilnet.IsIPv6String(obj.Spec.ClusterIP) {
|
||||||
|
obj.Spec.IPFamily = &ipv6
|
||||||
|
} else {
|
||||||
|
obj.Spec.IPFamily = &ipv4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
func SetDefaults_Pod(obj *v1.Pod) {
|
func SetDefaults_Pod(obj *v1.Pod) {
|
||||||
// If limits are specified, but requests are not, default requests to limits
|
// If limits are specified, but requests are not, default requests to limits
|
||||||
|
@ -35,6 +35,10 @@ import (
|
|||||||
|
|
||||||
// enforce that all types are installed
|
// enforce that all types are installed
|
||||||
_ "k8s.io/kubernetes/pkg/api/testapi"
|
_ "k8s.io/kubernetes/pkg/api/testapi"
|
||||||
|
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||||
|
"k8s.io/kubernetes/pkg/features"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestWorkloadDefaults detects changes to defaults within PodTemplateSpec.
|
// TestWorkloadDefaults detects changes to defaults within PodTemplateSpec.
|
||||||
@ -976,6 +980,140 @@ func TestSetDefaultService(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSetDefaultServiceIPFamily(t *testing.T) {
|
||||||
|
svc := v1.Service{
|
||||||
|
Spec: v1.ServiceSpec{
|
||||||
|
SessionAffinity: v1.ServiceAffinityNone,
|
||||||
|
Type: v1.ServiceTypeClusterIP,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
inSvcTweak func(s v1.Service) v1.Service
|
||||||
|
outSvcTweak func(s v1.Service) v1.Service
|
||||||
|
enableDualStack bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "dualstack off. ipfamily not set",
|
||||||
|
inSvcTweak: func(s v1.Service) v1.Service { return s },
|
||||||
|
outSvcTweak: func(s v1.Service) v1.Service { return s },
|
||||||
|
enableDualStack: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dualstack on. ipfamily not set, service is *not* ClusterIP-able",
|
||||||
|
inSvcTweak: func(s v1.Service) v1.Service {
|
||||||
|
s.Spec.Type = v1.ServiceTypeExternalName
|
||||||
|
return s
|
||||||
|
},
|
||||||
|
outSvcTweak: func(s v1.Service) v1.Service { return s },
|
||||||
|
enableDualStack: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dualstack off. ipfamily set",
|
||||||
|
inSvcTweak: func(s v1.Service) v1.Service {
|
||||||
|
ipv4Service := v1.IPv4Protocol
|
||||||
|
s.Spec.IPFamily = &ipv4Service
|
||||||
|
return s
|
||||||
|
},
|
||||||
|
outSvcTweak: func(s v1.Service) v1.Service {
|
||||||
|
ipv4Service := v1.IPv4Protocol
|
||||||
|
s.Spec.IPFamily = &ipv4Service
|
||||||
|
return s
|
||||||
|
},
|
||||||
|
enableDualStack: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dualstack off. ipfamily not set. clusterip set",
|
||||||
|
inSvcTweak: func(s v1.Service) v1.Service {
|
||||||
|
s.Spec.ClusterIP = "1.1.1.1"
|
||||||
|
return s
|
||||||
|
},
|
||||||
|
outSvcTweak: func(s v1.Service) v1.Service {
|
||||||
|
return s
|
||||||
|
},
|
||||||
|
enableDualStack: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dualstack on. ipfamily not set (clusterIP is v4)",
|
||||||
|
inSvcTweak: func(s v1.Service) v1.Service {
|
||||||
|
s.Spec.ClusterIP = "1.1.1.1"
|
||||||
|
return s
|
||||||
|
},
|
||||||
|
outSvcTweak: func(s v1.Service) v1.Service {
|
||||||
|
ipv4Service := v1.IPv4Protocol
|
||||||
|
s.Spec.IPFamily = &ipv4Service
|
||||||
|
return s
|
||||||
|
},
|
||||||
|
enableDualStack: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dualstack on. ipfamily not set (clusterIP is v6)",
|
||||||
|
inSvcTweak: func(s v1.Service) v1.Service {
|
||||||
|
s.Spec.ClusterIP = "fdd7:7713:8917:77ed:ffff:ffff:ffff:ffff"
|
||||||
|
return s
|
||||||
|
},
|
||||||
|
outSvcTweak: func(s v1.Service) v1.Service {
|
||||||
|
ipv6Service := v1.IPv6Protocol
|
||||||
|
s.Spec.IPFamily = &ipv6Service
|
||||||
|
return s
|
||||||
|
},
|
||||||
|
enableDualStack: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dualstack on. ipfamily set (clusterIP is v4)",
|
||||||
|
inSvcTweak: func(s v1.Service) v1.Service {
|
||||||
|
ipv4Service := v1.IPv4Protocol
|
||||||
|
s.Spec.IPFamily = &ipv4Service
|
||||||
|
s.Spec.ClusterIP = "1.1.1.1"
|
||||||
|
return s
|
||||||
|
},
|
||||||
|
outSvcTweak: func(s v1.Service) v1.Service {
|
||||||
|
ipv4Service := v1.IPv4Protocol
|
||||||
|
s.Spec.IPFamily = &ipv4Service
|
||||||
|
return s
|
||||||
|
},
|
||||||
|
enableDualStack: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dualstack on. ipfamily set (clusterIP is v6)",
|
||||||
|
inSvcTweak: func(s v1.Service) v1.Service {
|
||||||
|
ipv6Service := v1.IPv6Protocol
|
||||||
|
s.Spec.IPFamily = &ipv6Service
|
||||||
|
s.Spec.ClusterIP = "fdd7:7713:8917:77ed:ffff:ffff:ffff:ffff"
|
||||||
|
return s
|
||||||
|
},
|
||||||
|
outSvcTweak: func(s v1.Service) v1.Service {
|
||||||
|
ipv6Service := v1.IPv6Protocol
|
||||||
|
s.Spec.IPFamily = &ipv6Service
|
||||||
|
return s
|
||||||
|
},
|
||||||
|
enableDualStack: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.IPv6DualStack, tc.enableDualStack)()
|
||||||
|
tweakedIn := tc.inSvcTweak(svc)
|
||||||
|
expectedSvc := tc.outSvcTweak(svc)
|
||||||
|
defaulted := roundTrip(t, runtime.Object(&tweakedIn))
|
||||||
|
|
||||||
|
defaultedSvc := defaulted.(*v1.Service)
|
||||||
|
if expectedSvc.Spec.IPFamily != nil {
|
||||||
|
if defaultedSvc.Spec.IPFamily == nil {
|
||||||
|
t.Fatalf("defaulted service ipfamily is nil while expected is not")
|
||||||
|
}
|
||||||
|
if *(expectedSvc.Spec.IPFamily) != *(defaultedSvc.Spec.IPFamily) {
|
||||||
|
t.Fatalf("defaulted service ipfamily %v does not match expected %v", defaultedSvc.Spec.IPFamily, expectedSvc.Spec.IPFamily)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if expectedSvc.Spec.IPFamily == nil && defaultedSvc.Spec.IPFamily != nil {
|
||||||
|
t.Fatalf("defaulted service ipfamily is not nil, while expected service ipfamily is")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSetDefaultServiceSessionAffinityConfig(t *testing.T) {
|
func TestSetDefaultServiceSessionAffinityConfig(t *testing.T) {
|
||||||
testCases := map[string]v1.Service{
|
testCases := map[string]v1.Service{
|
||||||
"SessionAffinityConfig is empty": {
|
"SessionAffinityConfig is empty": {
|
||||||
|
@ -3893,6 +3893,7 @@ func ValidatePodTemplateUpdate(newPod, oldPod *core.PodTemplate) field.ErrorList
|
|||||||
var supportedSessionAffinityType = sets.NewString(string(core.ServiceAffinityClientIP), string(core.ServiceAffinityNone))
|
var supportedSessionAffinityType = sets.NewString(string(core.ServiceAffinityClientIP), string(core.ServiceAffinityNone))
|
||||||
var supportedServiceType = sets.NewString(string(core.ServiceTypeClusterIP), string(core.ServiceTypeNodePort),
|
var supportedServiceType = sets.NewString(string(core.ServiceTypeClusterIP), string(core.ServiceTypeNodePort),
|
||||||
string(core.ServiceTypeLoadBalancer), string(core.ServiceTypeExternalName))
|
string(core.ServiceTypeLoadBalancer), string(core.ServiceTypeExternalName))
|
||||||
|
var supportedServiceIPFamily = sets.NewString(string(core.IPv4Protocol), string(core.IPv6Protocol))
|
||||||
|
|
||||||
// ValidateService tests if required fields/annotations of a Service are valid.
|
// ValidateService tests if required fields/annotations of a Service are valid.
|
||||||
func ValidateService(service *core.Service) field.ErrorList {
|
func ValidateService(service *core.Service) field.ErrorList {
|
||||||
@ -4064,8 +4065,22 @@ func ValidateService(service *core.Service) field.ErrorList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
allErrs = append(allErrs, validateServiceExternalTrafficFieldsValue(service)...)
|
//if an ipfamily provided then it has to be one of the supported values
|
||||||
|
// note:
|
||||||
|
// - we don't validate service.Spec.IPFamily is supported by the cluster
|
||||||
|
// - we don't validate service.Spec.ClusterIP is within a range supported by the cluster
|
||||||
|
// both of these validations are done by the ipallocator
|
||||||
|
|
||||||
|
// if the gate is on this field is required (and defaulted by REST if not provided by user)
|
||||||
|
if utilfeature.DefaultFeatureGate.Enabled(features.IPv6DualStack) && service.Spec.IPFamily == nil {
|
||||||
|
allErrs = append(allErrs, field.Required(specPath.Child("ipFamily"), ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.Spec.IPFamily != nil && !supportedServiceIPFamily.Has(string(*service.Spec.IPFamily)) {
|
||||||
|
allErrs = append(allErrs, field.NotSupported(specPath.Child("ipFamily"), service.Spec.IPFamily, supportedServiceIPFamily.List()))
|
||||||
|
}
|
||||||
|
|
||||||
|
allErrs = append(allErrs, validateServiceExternalTrafficFieldsValue(service)...)
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4154,12 +4169,19 @@ func ValidateServiceExternalTrafficFieldsCombination(service *core.Service) fiel
|
|||||||
func ValidateServiceUpdate(service, oldService *core.Service) field.ErrorList {
|
func ValidateServiceUpdate(service, oldService *core.Service) field.ErrorList {
|
||||||
allErrs := ValidateObjectMetaUpdate(&service.ObjectMeta, &oldService.ObjectMeta, field.NewPath("metadata"))
|
allErrs := ValidateObjectMetaUpdate(&service.ObjectMeta, &oldService.ObjectMeta, field.NewPath("metadata"))
|
||||||
|
|
||||||
// ClusterIP should be immutable for services using it (every type other than ExternalName)
|
// ClusterIP and IPFamily should be immutable for services using it (every type other than ExternalName)
|
||||||
// which do not have ClusterIP assigned yet (empty string value)
|
// which do not have ClusterIP assigned yet (empty string value)
|
||||||
if service.Spec.Type != core.ServiceTypeExternalName {
|
if service.Spec.Type != core.ServiceTypeExternalName {
|
||||||
if oldService.Spec.Type != core.ServiceTypeExternalName && oldService.Spec.ClusterIP != "" {
|
if oldService.Spec.Type != core.ServiceTypeExternalName && oldService.Spec.ClusterIP != "" {
|
||||||
allErrs = append(allErrs, ValidateImmutableField(service.Spec.ClusterIP, oldService.Spec.ClusterIP, field.NewPath("spec", "clusterIP"))...)
|
allErrs = append(allErrs, ValidateImmutableField(service.Spec.ClusterIP, oldService.Spec.ClusterIP, field.NewPath("spec", "clusterIP"))...)
|
||||||
}
|
}
|
||||||
|
// notes:
|
||||||
|
// we drop the IPFamily field when the Dualstack gate is off.
|
||||||
|
// once the gate is on, we start assigning default ipfamily according to cluster settings. in other words
|
||||||
|
// though the field is immutable, we allow (onetime) change from nil==> to value
|
||||||
|
if oldService.Spec.IPFamily != nil {
|
||||||
|
allErrs = append(allErrs, ValidateImmutableField(service.Spec.IPFamily, oldService.Spec.IPFamily, field.NewPath("spec", "ipFamily"))...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
allErrs = append(allErrs, ValidateService(service)...)
|
allErrs = append(allErrs, ValidateService(service)...)
|
||||||
|
@ -9134,6 +9134,7 @@ func TestValidatePodStatusUpdate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func makeValidService() core.Service {
|
func makeValidService() core.Service {
|
||||||
|
serviceIPFamily := core.IPv4Protocol
|
||||||
return core.Service{
|
return core.Service{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "valid",
|
Name: "valid",
|
||||||
@ -9147,6 +9148,7 @@ func makeValidService() core.Service {
|
|||||||
SessionAffinity: "None",
|
SessionAffinity: "None",
|
||||||
Type: core.ServiceTypeClusterIP,
|
Type: core.ServiceTypeClusterIP,
|
||||||
Ports: []core.ServicePort{{Name: "p", Protocol: "TCP", Port: 8675, TargetPort: intstr.FromInt(8675)}},
|
Ports: []core.ServicePort{{Name: "p", Protocol: "TCP", Port: 8675, TargetPort: intstr.FromInt(8675)}},
|
||||||
|
IPFamily: &serviceIPFamily,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -10072,6 +10074,29 @@ func TestValidateService(t *testing.T) {
|
|||||||
},
|
},
|
||||||
numErrs: 1,
|
numErrs: 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "valid, nil service IPFamily",
|
||||||
|
tweakSvc: func(s *core.Service) {
|
||||||
|
s.Spec.IPFamily = nil
|
||||||
|
},
|
||||||
|
numErrs: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid, service with valid IPFamily",
|
||||||
|
tweakSvc: func(s *core.Service) {
|
||||||
|
ipv4Service := core.IPv4Protocol
|
||||||
|
s.Spec.IPFamily = &ipv4Service
|
||||||
|
},
|
||||||
|
numErrs: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid, service with invalid IPFamily",
|
||||||
|
tweakSvc: func(s *core.Service) {
|
||||||
|
invalidServiceIPFamily := core.IPFamily("not-a-valid-ip-family")
|
||||||
|
s.Spec.IPFamily = &invalidServiceIPFamily
|
||||||
|
},
|
||||||
|
numErrs: 1,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
@ -11922,6 +11947,80 @@ func TestValidateServiceUpdate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
numErrs: 1,
|
numErrs: 1,
|
||||||
},
|
},
|
||||||
|
/* Service IP Family */
|
||||||
|
{
|
||||||
|
name: "same ServiceIPFamily",
|
||||||
|
tweakSvc: func(oldSvc, newSvc *core.Service) {
|
||||||
|
ipv4Service := core.IPv4Protocol
|
||||||
|
oldSvc.Spec.Type = core.ServiceTypeClusterIP
|
||||||
|
oldSvc.Spec.IPFamily = &ipv4Service
|
||||||
|
|
||||||
|
newSvc.Spec.Type = core.ServiceTypeClusterIP
|
||||||
|
newSvc.Spec.IPFamily = &ipv4Service
|
||||||
|
},
|
||||||
|
numErrs: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ExternalName while changing Service IPFamily",
|
||||||
|
tweakSvc: func(oldSvc, newSvc *core.Service) {
|
||||||
|
ipv4Service := core.IPv4Protocol
|
||||||
|
oldSvc.Spec.ExternalName = "somename"
|
||||||
|
oldSvc.Spec.Type = core.ServiceTypeExternalName
|
||||||
|
oldSvc.Spec.IPFamily = &ipv4Service
|
||||||
|
|
||||||
|
ipv6Service := core.IPv6Protocol
|
||||||
|
newSvc.Spec.ExternalName = "somename"
|
||||||
|
newSvc.Spec.Type = core.ServiceTypeExternalName
|
||||||
|
newSvc.Spec.IPFamily = &ipv6Service
|
||||||
|
},
|
||||||
|
numErrs: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "setting ipfamily from nil to v4",
|
||||||
|
tweakSvc: func(oldSvc, newSvc *core.Service) {
|
||||||
|
oldSvc.Spec.IPFamily = nil
|
||||||
|
|
||||||
|
ipv4Service := core.IPv4Protocol
|
||||||
|
newSvc.Spec.ExternalName = "somename"
|
||||||
|
newSvc.Spec.IPFamily = &ipv4Service
|
||||||
|
},
|
||||||
|
numErrs: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "setting ipfamily from nil to v6",
|
||||||
|
tweakSvc: func(oldSvc, newSvc *core.Service) {
|
||||||
|
oldSvc.Spec.IPFamily = nil
|
||||||
|
|
||||||
|
ipv6Service := core.IPv6Protocol
|
||||||
|
newSvc.Spec.ExternalName = "somename"
|
||||||
|
newSvc.Spec.IPFamily = &ipv6Service
|
||||||
|
},
|
||||||
|
numErrs: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remove ipfamily",
|
||||||
|
tweakSvc: func(oldSvc, newSvc *core.Service) {
|
||||||
|
ipv6Service := core.IPv6Protocol
|
||||||
|
oldSvc.Spec.IPFamily = &ipv6Service
|
||||||
|
|
||||||
|
newSvc.Spec.IPFamily = nil
|
||||||
|
},
|
||||||
|
numErrs: 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "change ServiceIPFamily",
|
||||||
|
tweakSvc: func(oldSvc, newSvc *core.Service) {
|
||||||
|
ipv4Service := core.IPv4Protocol
|
||||||
|
oldSvc.Spec.Type = core.ServiceTypeClusterIP
|
||||||
|
oldSvc.Spec.IPFamily = &ipv4Service
|
||||||
|
|
||||||
|
ipv6Service := core.IPv6Protocol
|
||||||
|
newSvc.Spec.Type = core.ServiceTypeClusterIP
|
||||||
|
newSvc.Spec.IPFamily = &ipv6Service
|
||||||
|
},
|
||||||
|
numErrs: 1,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
|
@ -26,6 +26,9 @@ import (
|
|||||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||||
api "k8s.io/kubernetes/pkg/apis/core"
|
api "k8s.io/kubernetes/pkg/apis/core"
|
||||||
"k8s.io/kubernetes/pkg/apis/core/validation"
|
"k8s.io/kubernetes/pkg/apis/core/validation"
|
||||||
|
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
"k8s.io/kubernetes/pkg/features"
|
||||||
)
|
)
|
||||||
|
|
||||||
// svcStrategy implements behavior for Services
|
// svcStrategy implements behavior for Services
|
||||||
@ -114,6 +117,21 @@ func (svcStrategy) Export(ctx context.Context, obj runtime.Object, exact bool) e
|
|||||||
// newSvc.Spec.MyFeature = nil
|
// newSvc.Spec.MyFeature = nil
|
||||||
// }
|
// }
|
||||||
func dropServiceDisabledFields(newSvc *api.Service, oldSvc *api.Service) {
|
func dropServiceDisabledFields(newSvc *api.Service, oldSvc *api.Service) {
|
||||||
|
// Drop IPFamily if DualStack is not enabled
|
||||||
|
if !utilfeature.DefaultFeatureGate.Enabled(features.IPv6DualStack) && !serviceIPFamilyInUse(oldSvc) {
|
||||||
|
newSvc.Spec.IPFamily = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns true if svc.Spec.ServiceIPFamily field is in use
|
||||||
|
func serviceIPFamilyInUse(svc *api.Service) bool {
|
||||||
|
if svc == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if svc.Spec.IPFamily != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
type serviceStatusStrategy struct {
|
type serviceStatusStrategy struct {
|
||||||
|
@ -23,11 +23,16 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/api/errors"
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/diff"
|
||||||
"k8s.io/apimachinery/pkg/util/intstr"
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
api "k8s.io/kubernetes/pkg/apis/core"
|
api "k8s.io/kubernetes/pkg/apis/core"
|
||||||
_ "k8s.io/kubernetes/pkg/apis/core/install"
|
_ "k8s.io/kubernetes/pkg/apis/core/install"
|
||||||
|
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||||
|
"k8s.io/kubernetes/pkg/features"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestExportService(t *testing.T) {
|
func TestExportService(t *testing.T) {
|
||||||
@ -128,6 +133,7 @@ func TestCheckGeneratedNameError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func makeValidService() api.Service {
|
func makeValidService() api.Service {
|
||||||
|
defaultServiceIPFamily := api.IPv4Protocol
|
||||||
return api.Service{
|
return api.Service{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: "valid",
|
Name: "valid",
|
||||||
@ -141,6 +147,7 @@ func makeValidService() api.Service {
|
|||||||
SessionAffinity: "None",
|
SessionAffinity: "None",
|
||||||
Type: api.ServiceTypeClusterIP,
|
Type: api.ServiceTypeClusterIP,
|
||||||
Ports: []api.ServicePort{{Name: "p", Protocol: "TCP", Port: 8675, TargetPort: intstr.FromInt(8675)}},
|
Ports: []api.ServicePort{{Name: "p", Protocol: "TCP", Port: 8675, TargetPort: intstr.FromInt(8675)}},
|
||||||
|
IPFamily: &defaultServiceIPFamily,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -241,3 +248,70 @@ func TestServiceStatusStrategy(t *testing.T) {
|
|||||||
t.Errorf("Unexpected error %v", errs)
|
t.Errorf("Unexpected error %v", errs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeServiceWithIPFamily(IPFamily *api.IPFamily) *api.Service {
|
||||||
|
return &api.Service{
|
||||||
|
Spec: api.ServiceSpec{
|
||||||
|
IPFamily: IPFamily,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestDropDisabledField(t *testing.T) {
|
||||||
|
ipv4Service := api.IPv4Protocol
|
||||||
|
ipv6Service := api.IPv6Protocol
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
enableDualStack bool
|
||||||
|
svc *api.Service
|
||||||
|
oldSvc *api.Service
|
||||||
|
compareSvc *api.Service
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "not dual stack, field not used",
|
||||||
|
enableDualStack: false,
|
||||||
|
svc: makeServiceWithIPFamily(nil),
|
||||||
|
oldSvc: nil,
|
||||||
|
compareSvc: makeServiceWithIPFamily(nil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not dual stack, field used in new, not in old",
|
||||||
|
enableDualStack: false,
|
||||||
|
svc: makeServiceWithIPFamily(&ipv4Service),
|
||||||
|
oldSvc: nil,
|
||||||
|
compareSvc: makeServiceWithIPFamily(nil),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not dual stack, field used in old and new",
|
||||||
|
enableDualStack: false,
|
||||||
|
svc: makeServiceWithIPFamily(&ipv4Service),
|
||||||
|
oldSvc: makeServiceWithIPFamily(&ipv4Service),
|
||||||
|
compareSvc: makeServiceWithIPFamily(&ipv4Service),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dualstack, field used",
|
||||||
|
enableDualStack: true,
|
||||||
|
svc: makeServiceWithIPFamily(&ipv6Service),
|
||||||
|
oldSvc: nil,
|
||||||
|
compareSvc: makeServiceWithIPFamily(&ipv6Service),
|
||||||
|
},
|
||||||
|
|
||||||
|
/* add more tests for other dropped fields as needed */
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
func() {
|
||||||
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.IPv6DualStack, tc.enableDualStack)()
|
||||||
|
old := tc.oldSvc.DeepCopy()
|
||||||
|
dropServiceDisabledFields(tc.svc, tc.oldSvc)
|
||||||
|
|
||||||
|
// old node should never be changed
|
||||||
|
if !reflect.DeepEqual(tc.oldSvc, old) {
|
||||||
|
t.Errorf("%v: old svc changed: %v", tc.name, diff.ObjectReflectDiff(tc.oldSvc, old))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(tc.svc, tc.compareSvc) {
|
||||||
|
t.Errorf("%v: unexpected svc spec: %v", tc.name, diff.ObjectReflectDiff(tc.svc, tc.compareSvc))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
- k8s.io/kubernetes/pkg/util
|
- k8s.io/kubernetes/pkg/util
|
||||||
- k8s.io/api/core/v1
|
- k8s.io/api/core/v1
|
||||||
- k8s.io/utils/pointer
|
- k8s.io/utils/pointer
|
||||||
|
- k8s.io/utils/net
|
||||||
- k8s.io/klog
|
- k8s.io/klog
|
||||||
|
|
||||||
# the following are temporary and should go away. Think twice (or more) before adding anything here.
|
# the following are temporary and should go away. Think twice (or more) before adding anything here.
|
||||||
|
@ -3794,6 +3794,17 @@ type LoadBalancerIngress struct {
|
|||||||
Hostname string `json:"hostname,omitempty" protobuf:"bytes,2,opt,name=hostname"`
|
Hostname string `json:"hostname,omitempty" protobuf:"bytes,2,opt,name=hostname"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IPFamily represents the IP Family (IPv4 or IPv6). This type is used
|
||||||
|
// to express the family of an IP expressed by a type (i.e. service.Spec.IPFamily)
|
||||||
|
type IPFamily string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// IPv4Protocol indicates that this IP is IPv4 protocol
|
||||||
|
IPv4Protocol IPFamily = "IPv4"
|
||||||
|
// IPv6Protocol indicates that this IP is IPv6 protocol
|
||||||
|
IPv6Protocol IPFamily = "IPv6"
|
||||||
|
)
|
||||||
|
|
||||||
// ServiceSpec describes the attributes that a user creates on a service.
|
// ServiceSpec describes the attributes that a user creates on a service.
|
||||||
type ServiceSpec struct {
|
type ServiceSpec struct {
|
||||||
// The list of ports that are exposed by this service.
|
// The list of ports that are exposed by this service.
|
||||||
@ -3909,6 +3920,16 @@ type ServiceSpec struct {
|
|||||||
// sessionAffinityConfig contains the configurations of session affinity.
|
// sessionAffinityConfig contains the configurations of session affinity.
|
||||||
// +optional
|
// +optional
|
||||||
SessionAffinityConfig *SessionAffinityConfig `json:"sessionAffinityConfig,omitempty" protobuf:"bytes,14,opt,name=sessionAffinityConfig"`
|
SessionAffinityConfig *SessionAffinityConfig `json:"sessionAffinityConfig,omitempty" protobuf:"bytes,14,opt,name=sessionAffinityConfig"`
|
||||||
|
|
||||||
|
// ipFamily specifies whether this Service has a preference for a particular IP family (e.g. IPv4 vs.
|
||||||
|
// IPv6). If a specific IP family is requested, the clusterIP field will be allocated from that family, if it is
|
||||||
|
// available in the cluster. If no IP family is requested, the cluster's primary IP family will be used.
|
||||||
|
// Other IP fields (loadBalancerIP, loadBalancerSourceRanges, externalIPs) and controllers which
|
||||||
|
// allocate external load-balancers should use the same IP family. Endpoints for this Service will be of
|
||||||
|
// this family. This field is immutable after creation. Assigning a ServiceIPFamily not available in the
|
||||||
|
// cluster (e.g. IPv6 in IPv4 only cluster) is an error condition and will fail during clusterIP assignment.
|
||||||
|
// +optional
|
||||||
|
IPFamily *IPFamily `json:"ipFamily,omitempty" protobuf:"bytes,15,opt,name=ipFamily,Configcasttype=IPFamily"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServicePort contains information on service's port.
|
// ServicePort contains information on service's port.
|
||||||
|
@ -2478,6 +2478,11 @@ func describeService(service *corev1.Service, endpoints *corev1.Endpoints, event
|
|||||||
w.Write(LEVEL_0, "Selector:\t%s\n", labels.FormatLabels(service.Spec.Selector))
|
w.Write(LEVEL_0, "Selector:\t%s\n", labels.FormatLabels(service.Spec.Selector))
|
||||||
w.Write(LEVEL_0, "Type:\t%s\n", service.Spec.Type)
|
w.Write(LEVEL_0, "Type:\t%s\n", service.Spec.Type)
|
||||||
w.Write(LEVEL_0, "IP:\t%s\n", service.Spec.ClusterIP)
|
w.Write(LEVEL_0, "IP:\t%s\n", service.Spec.ClusterIP)
|
||||||
|
|
||||||
|
if service.Spec.IPFamily != nil {
|
||||||
|
w.Write(LEVEL_0, "IPFamily:\t%s\n", *(service.Spec.IPFamily))
|
||||||
|
}
|
||||||
|
|
||||||
if len(service.Spec.ExternalIPs) > 0 {
|
if len(service.Spec.ExternalIPs) > 0 {
|
||||||
w.Write(LEVEL_0, "External IPs:\t%v\n", strings.Join(service.Spec.ExternalIPs, ","))
|
w.Write(LEVEL_0, "External IPs:\t%v\n", strings.Join(service.Spec.ExternalIPs, ","))
|
||||||
}
|
}
|
||||||
|
@ -351,6 +351,8 @@ func getResourceList(cpu, memory string) corev1.ResourceList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDescribeService(t *testing.T) {
|
func TestDescribeService(t *testing.T) {
|
||||||
|
defaultServiceIPFamily := corev1.IPv4Protocol
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
service *corev1.Service
|
service *corev1.Service
|
||||||
@ -364,7 +366,8 @@ func TestDescribeService(t *testing.T) {
|
|||||||
Namespace: "foo",
|
Namespace: "foo",
|
||||||
},
|
},
|
||||||
Spec: corev1.ServiceSpec{
|
Spec: corev1.ServiceSpec{
|
||||||
Type: corev1.ServiceTypeLoadBalancer,
|
Type: corev1.ServiceTypeLoadBalancer,
|
||||||
|
IPFamily: &defaultServiceIPFamily,
|
||||||
Ports: []corev1.ServicePort{{
|
Ports: []corev1.ServicePort{{
|
||||||
Name: "port-tcp",
|
Name: "port-tcp",
|
||||||
Port: 8080,
|
Port: 8080,
|
||||||
@ -402,7 +405,8 @@ func TestDescribeService(t *testing.T) {
|
|||||||
Namespace: "foo",
|
Namespace: "foo",
|
||||||
},
|
},
|
||||||
Spec: corev1.ServiceSpec{
|
Spec: corev1.ServiceSpec{
|
||||||
Type: corev1.ServiceTypeLoadBalancer,
|
Type: corev1.ServiceTypeLoadBalancer,
|
||||||
|
IPFamily: &defaultServiceIPFamily,
|
||||||
Ports: []corev1.ServicePort{{
|
Ports: []corev1.ServicePort{{
|
||||||
Name: "port-tcp",
|
Name: "port-tcp",
|
||||||
Port: 8080,
|
Port: 8080,
|
||||||
@ -432,6 +436,46 @@ func TestDescribeService(t *testing.T) {
|
|||||||
"HealthCheck NodePort", "32222",
|
"HealthCheck NodePort", "32222",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "test-ServiceIPFamily",
|
||||||
|
service: &corev1.Service{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "bar",
|
||||||
|
Namespace: "foo",
|
||||||
|
},
|
||||||
|
Spec: corev1.ServiceSpec{
|
||||||
|
Type: corev1.ServiceTypeLoadBalancer,
|
||||||
|
IPFamily: &defaultServiceIPFamily,
|
||||||
|
Ports: []corev1.ServicePort{{
|
||||||
|
Name: "port-tcp",
|
||||||
|
Port: 8080,
|
||||||
|
Protocol: corev1.ProtocolTCP,
|
||||||
|
TargetPort: intstr.FromString("targetPort"),
|
||||||
|
NodePort: 31111,
|
||||||
|
}},
|
||||||
|
Selector: map[string]string{"blah": "heh"},
|
||||||
|
ClusterIP: "1.2.3.4",
|
||||||
|
LoadBalancerIP: "5.6.7.8",
|
||||||
|
SessionAffinity: "None",
|
||||||
|
ExternalTrafficPolicy: "Local",
|
||||||
|
HealthCheckNodePort: 32222,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expect: []string{
|
||||||
|
"Name", "bar",
|
||||||
|
"Namespace", "foo",
|
||||||
|
"Selector", "blah=heh",
|
||||||
|
"Type", "LoadBalancer",
|
||||||
|
"IP", "1.2.3.4",
|
||||||
|
"IPFamily", "IPv4",
|
||||||
|
"Port", "port-tcp", "8080/TCP",
|
||||||
|
"TargetPort", "targetPort/TCP",
|
||||||
|
"NodePort", "port-tcp", "31111/TCP",
|
||||||
|
"Session Affinity", "None",
|
||||||
|
"External Traffic Policy", "Local",
|
||||||
|
"HealthCheck NodePort", "32222",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
Loading…
Reference in New Issue
Block a user