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}
|
||||
*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) {
|
||||
types := []core.ServiceExternalTrafficPolicyType{core.ServiceExternalTrafficPolicyTypeCluster, core.ServiceExternalTrafficPolicyTypeLocal}
|
||||
*p = types[c.Rand.Intn(len(types))]
|
||||
|
@ -3330,6 +3330,17 @@ type LoadBalancerIngress struct {
|
||||
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
|
||||
type ServiceSpec struct {
|
||||
// Type determines how the Service is exposed. Defaults to ClusterIP. Valid
|
||||
@ -3430,6 +3441,16 @@ type ServiceSpec struct {
|
||||
// of peer discovery.
|
||||
// +optional
|
||||
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 {
|
||||
|
@ -24,6 +24,10 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/kubernetes/pkg/util/parsers"
|
||||
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 {
|
||||
@ -128,6 +132,33 @@ func SetDefaults_Service(obj *v1.Service) {
|
||||
obj.Spec.ExternalTrafficPolicy == "" {
|
||||
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) {
|
||||
// If limits are specified, but requests are not, default requests to limits
|
||||
|
@ -35,6 +35,10 @@ import (
|
||||
|
||||
// enforce that all types are installed
|
||||
_ "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.
|
||||
@ -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) {
|
||||
testCases := map[string]v1.Service{
|
||||
"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 supportedServiceType = sets.NewString(string(core.ServiceTypeClusterIP), string(core.ServiceTypeNodePort),
|
||||
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.
|
||||
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
|
||||
}
|
||||
|
||||
@ -4154,12 +4169,19 @@ func ValidateServiceExternalTrafficFieldsCombination(service *core.Service) fiel
|
||||
func ValidateServiceUpdate(service, oldService *core.Service) field.ErrorList {
|
||||
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)
|
||||
if service.Spec.Type != core.ServiceTypeExternalName {
|
||||
if oldService.Spec.Type != core.ServiceTypeExternalName && oldService.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)...)
|
||||
|
@ -9134,6 +9134,7 @@ func TestValidatePodStatusUpdate(t *testing.T) {
|
||||
}
|
||||
|
||||
func makeValidService() core.Service {
|
||||
serviceIPFamily := core.IPv4Protocol
|
||||
return core.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "valid",
|
||||
@ -9147,6 +9148,7 @@ func makeValidService() core.Service {
|
||||
SessionAffinity: "None",
|
||||
Type: core.ServiceTypeClusterIP,
|
||||
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,
|
||||
},
|
||||
{
|
||||
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 {
|
||||
@ -11922,6 +11947,80 @@ func TestValidateServiceUpdate(t *testing.T) {
|
||||
},
|
||||
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 {
|
||||
|
@ -26,6 +26,9 @@ import (
|
||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/apis/core/validation"
|
||||
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
)
|
||||
|
||||
// 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
|
||||
// }
|
||||
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 {
|
||||
|
@ -23,11 +23,16 @@ import (
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
_ "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) {
|
||||
@ -128,6 +133,7 @@ func TestCheckGeneratedNameError(t *testing.T) {
|
||||
}
|
||||
|
||||
func makeValidService() api.Service {
|
||||
defaultServiceIPFamily := api.IPv4Protocol
|
||||
return api.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "valid",
|
||||
@ -141,6 +147,7 @@ func makeValidService() api.Service {
|
||||
SessionAffinity: "None",
|
||||
Type: api.ServiceTypeClusterIP,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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/api/core/v1
|
||||
- k8s.io/utils/pointer
|
||||
- k8s.io/utils/net
|
||||
- k8s.io/klog
|
||||
|
||||
# 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"`
|
||||
}
|
||||
|
||||
// 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.
|
||||
type ServiceSpec struct {
|
||||
// The list of ports that are exposed by this service.
|
||||
@ -3909,6 +3920,16 @@ type ServiceSpec struct {
|
||||
// sessionAffinityConfig contains the configurations of session affinity.
|
||||
// +optional
|
||||
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.
|
||||
|
@ -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, "Type:\t%s\n", service.Spec.Type)
|
||||
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 {
|
||||
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) {
|
||||
defaultServiceIPFamily := corev1.IPv4Protocol
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
service *corev1.Service
|
||||
@ -364,7 +366,8 @@ func TestDescribeService(t *testing.T) {
|
||||
Namespace: "foo",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Type: corev1.ServiceTypeLoadBalancer,
|
||||
Type: corev1.ServiceTypeLoadBalancer,
|
||||
IPFamily: &defaultServiceIPFamily,
|
||||
Ports: []corev1.ServicePort{{
|
||||
Name: "port-tcp",
|
||||
Port: 8080,
|
||||
@ -402,7 +405,8 @@ func TestDescribeService(t *testing.T) {
|
||||
Namespace: "foo",
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Type: corev1.ServiceTypeLoadBalancer,
|
||||
Type: corev1.ServiceTypeLoadBalancer,
|
||||
IPFamily: &defaultServiceIPFamily,
|
||||
Ports: []corev1.ServicePort{{
|
||||
Name: "port-tcp",
|
||||
Port: 8080,
|
||||
@ -432,6 +436,46 @@ func TestDescribeService(t *testing.T) {
|
||||
"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 {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
|
Loading…
Reference in New Issue
Block a user