From 5e8ccda71cc676e070cd0f5d7b6990b8ebc24c1f Mon Sep 17 00:00:00 2001 From: "Khaled Henidak(Kal)" Date: Fri, 23 Aug 2019 17:25:59 +0000 Subject: [PATCH] phase 2: api types + defaulting + validation + disabled fields handling --- pkg/apis/core/fuzzer/fuzzer.go | 5 + pkg/apis/core/types.go | 21 +++ pkg/apis/core/v1/defaults.go | 31 ++++ pkg/apis/core/v1/defaults_test.go | 138 ++++++++++++++++++ pkg/apis/core/validation/validation.go | 26 +++- pkg/apis/core/validation/validation_test.go | 99 +++++++++++++ pkg/registry/core/service/strategy.go | 18 +++ pkg/registry/core/service/strategy_test.go | 74 ++++++++++ staging/publishing/import-restrictions.yaml | 1 + staging/src/k8s.io/api/core/v1/types.go | 21 +++ .../pkg/describe/versioned/describe.go | 5 + .../pkg/describe/versioned/describe_test.go | 48 +++++- 12 files changed, 483 insertions(+), 4 deletions(-) diff --git a/pkg/apis/core/fuzzer/fuzzer.go b/pkg/apis/core/fuzzer/fuzzer.go index 23eb3cf61dd..44517298295 100644 --- a/pkg/apis/core/fuzzer/fuzzer.go +++ b/pkg/apis/core/fuzzer/fuzzer.go @@ -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))] diff --git a/pkg/apis/core/types.go b/pkg/apis/core/types.go index b02c5749eba..86025fc09fc 100644 --- a/pkg/apis/core/types.go +++ b/pkg/apis/core/types.go @@ -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 { diff --git a/pkg/apis/core/v1/defaults.go b/pkg/apis/core/v1/defaults.go index 0ec32ebf225..bc56c783076 100644 --- a/pkg/apis/core/v1/defaults.go +++ b/pkg/apis/core/v1/defaults.go @@ -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 diff --git a/pkg/apis/core/v1/defaults_test.go b/pkg/apis/core/v1/defaults_test.go index 4fd112a136a..00e468f2d6b 100644 --- a/pkg/apis/core/v1/defaults_test.go +++ b/pkg/apis/core/v1/defaults_test.go @@ -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": { diff --git a/pkg/apis/core/validation/validation.go b/pkg/apis/core/validation/validation.go index e135bb277b7..f609b0980b4 100644 --- a/pkg/apis/core/validation/validation.go +++ b/pkg/apis/core/validation/validation.go @@ -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)...) diff --git a/pkg/apis/core/validation/validation_test.go b/pkg/apis/core/validation/validation_test.go index 55d43df50b6..fd10c767e4d 100644 --- a/pkg/apis/core/validation/validation_test.go +++ b/pkg/apis/core/validation/validation_test.go @@ -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 { diff --git a/pkg/registry/core/service/strategy.go b/pkg/registry/core/service/strategy.go index 1955944604a..7327a4da21b 100644 --- a/pkg/registry/core/service/strategy.go +++ b/pkg/registry/core/service/strategy.go @@ -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 { diff --git a/pkg/registry/core/service/strategy_test.go b/pkg/registry/core/service/strategy_test.go index 1c5ac1d4308..cd9e08a7c53 100644 --- a/pkg/registry/core/service/strategy_test.go +++ b/pkg/registry/core/service/strategy_test.go @@ -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)) + } + }() + } + +} diff --git a/staging/publishing/import-restrictions.yaml b/staging/publishing/import-restrictions.yaml index f66b33f292c..d033a68c983 100644 --- a/staging/publishing/import-restrictions.yaml +++ b/staging/publishing/import-restrictions.yaml @@ -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. diff --git a/staging/src/k8s.io/api/core/v1/types.go b/staging/src/k8s.io/api/core/v1/types.go index b2e721e8895..9bf48ecf15c 100644 --- a/staging/src/k8s.io/api/core/v1/types.go +++ b/staging/src/k8s.io/api/core/v1/types.go @@ -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. diff --git a/staging/src/k8s.io/kubectl/pkg/describe/versioned/describe.go b/staging/src/k8s.io/kubectl/pkg/describe/versioned/describe.go index a5c78c0da00..7ce165e2347 100644 --- a/staging/src/k8s.io/kubectl/pkg/describe/versioned/describe.go +++ b/staging/src/k8s.io/kubectl/pkg/describe/versioned/describe.go @@ -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, ",")) } diff --git a/staging/src/k8s.io/kubectl/pkg/describe/versioned/describe_test.go b/staging/src/k8s.io/kubectl/pkg/describe/versioned/describe_test.go index 1fe3b944f0a..66e719aba2a 100644 --- a/staging/src/k8s.io/kubectl/pkg/describe/versioned/describe_test.go +++ b/staging/src/k8s.io/kubectl/pkg/describe/versioned/describe_test.go @@ -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) {