mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-01-06 07:57:35 +00:00
dual stack services (#91824)
* api: structure change * api: defaulting, conversion, and validation * [FIX] validation: auto remove second ip/family when service changes to SingleStack * [FIX] api: defaulting, conversion, and validation * api-server: clusterIPs alloc, printers, storage and strategy * [FIX] clusterIPs default on read * alloc: auto remove second ip/family when service changes to SingleStack * api-server: repair loop handling for clusterIPs * api-server: force kubernetes default service into single stack * api-server: tie dualstack feature flag with endpoint feature flag * controller-manager: feature flag, endpoint, and endpointSlice controllers handling multi family service * [FIX] controller-manager: feature flag, endpoint, and endpointSlicecontrollers handling multi family service * kube-proxy: feature-flag, utils, proxier, and meta proxier * [FIX] kubeproxy: call both proxier at the same time * kubenet: remove forced pod IP sorting * kubectl: modify describe to include ClusterIPs, IPFamilies, and IPFamilyPolicy * e2e: fix tests that depends on IPFamily field AND add dual stack tests * e2e: fix expected error message for ClusterIP immutability * add integration tests for dualstack the third phase of dual stack is a very complex change in the API, basically it introduces Dual Stack services. Main changes are: - It pluralizes the Service IPFamily field to IPFamilies, and removes the singular field. - It introduces a new field IPFamilyPolicyType that can take 3 values to express the "dual-stack(mad)ness" of the cluster: SingleStack, PreferDualStack and RequireDualStack - It pluralizes ClusterIP to ClusterIPs. The goal is to add coverage to the services API operations, taking into account the 6 different modes a cluster can have: - single stack: IP4 or IPv6 (as of today) - dual stack: IPv4 only, IPv6 only, IPv4 - IPv6, IPv6 - IPv4 * [FIX] add integration tests for dualstack * generated data * generated files Co-authored-by: Antonio Ojea <aojea@redhat.com>
This commit is contained in:
committed by
GitHub
parent
d0e06cf3e0
commit
6675eba3ef
@@ -29,15 +29,16 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||
corev1 "k8s.io/kubernetes/pkg/apis/core/v1"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
utilpointer "k8s.io/utils/pointer"
|
||||
|
||||
// ensure types are installed
|
||||
_ "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"
|
||||
)
|
||||
|
||||
// TestWorkloadDefaults detects changes to defaults within PodTemplateSpec.
|
||||
@@ -1023,140 +1024,6 @@ 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": {
|
||||
@@ -1935,3 +1802,253 @@ func TestSetDefaultEnableServiceLinks(t *testing.T) {
|
||||
t.Errorf("Expected enableServiceLinks value: %+v\ngot: %+v\n", v1.DefaultEnableServiceLinks, *output.Spec.EnableServiceLinks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDefaultIPFamilies(t *testing.T) {
|
||||
preferDualStack := v1.IPFamilyPolicyPreferDualStack
|
||||
requireDualStack := v1.IPFamilyPolicyRequireDualStack
|
||||
testCases := []struct {
|
||||
name string
|
||||
expectedIPFamiliesWithGate []v1.IPFamily
|
||||
svc v1.Service
|
||||
}{
|
||||
{
|
||||
name: "must not set for ExternalName",
|
||||
expectedIPFamiliesWithGate: nil,
|
||||
|
||||
svc: v1.Service{
|
||||
Spec: v1.ServiceSpec{
|
||||
Type: v1.ServiceTypeExternalName,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "must not set for single stack",
|
||||
expectedIPFamiliesWithGate: nil,
|
||||
|
||||
svc: v1.Service{
|
||||
Spec: v1.ServiceSpec{
|
||||
Type: v1.ServiceTypeClusterIP,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "must not set for single stack, even when a family is set",
|
||||
expectedIPFamiliesWithGate: []v1.IPFamily{v1.IPv6Protocol},
|
||||
|
||||
svc: v1.Service{
|
||||
Spec: v1.ServiceSpec{
|
||||
Type: v1.ServiceTypeClusterIP,
|
||||
IPFamilies: []v1.IPFamily{v1.IPv6Protocol},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "must not set for preferDualStack",
|
||||
expectedIPFamiliesWithGate: []v1.IPFamily{v1.IPv6Protocol},
|
||||
|
||||
svc: v1.Service{
|
||||
Spec: v1.ServiceSpec{
|
||||
Type: v1.ServiceTypeClusterIP,
|
||||
IPFamilyPolicy: &preferDualStack,
|
||||
IPFamilies: []v1.IPFamily{v1.IPv6Protocol},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "must set for requireDualStack (6,4)",
|
||||
expectedIPFamiliesWithGate: []v1.IPFamily{v1.IPv6Protocol, v1.IPv4Protocol},
|
||||
|
||||
svc: v1.Service{
|
||||
Spec: v1.ServiceSpec{
|
||||
Type: v1.ServiceTypeClusterIP,
|
||||
IPFamilyPolicy: &requireDualStack,
|
||||
IPFamilies: []v1.IPFamily{v1.IPv6Protocol},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "must set for requireDualStack (4,6)",
|
||||
expectedIPFamiliesWithGate: []v1.IPFamily{v1.IPv4Protocol, v1.IPv6Protocol},
|
||||
|
||||
svc: v1.Service{
|
||||
Spec: v1.ServiceSpec{
|
||||
Type: v1.ServiceTypeClusterIP,
|
||||
IPFamilyPolicy: &requireDualStack,
|
||||
IPFamilies: []v1.IPFamily{v1.IPv4Protocol},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range testCases {
|
||||
// run with gate
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.IPv6DualStack, true)()
|
||||
obj2 := roundTrip(t, runtime.Object(&test.svc))
|
||||
svc2 := obj2.(*v1.Service)
|
||||
|
||||
if len(test.expectedIPFamiliesWithGate) != len(svc2.Spec.IPFamilies) {
|
||||
t.Fatalf("expected .spec.IPFamilies len:%v got %v", len(test.expectedIPFamiliesWithGate), len(svc2.Spec.IPFamilies))
|
||||
}
|
||||
|
||||
for i, family := range test.expectedIPFamiliesWithGate {
|
||||
if svc2.Spec.IPFamilies[i] != family {
|
||||
t.Fatalf("failed. expected family %v at %v got %v", family, i, svc2.Spec.IPFamilies[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// run without gate (families should not change)
|
||||
t.Run(fmt.Sprintf("without-gate:%s", test.name), func(t *testing.T) {
|
||||
obj2 := roundTrip(t, runtime.Object(&test.svc))
|
||||
svc2 := obj2.(*v1.Service)
|
||||
|
||||
if len(test.svc.Spec.IPFamilies) != len(svc2.Spec.IPFamilies) {
|
||||
t.Fatalf("expected .spec.IPFamilies len:%v got %v", len(test.expectedIPFamiliesWithGate), len(svc2.Spec.IPFamilies))
|
||||
}
|
||||
|
||||
for i, family := range test.svc.Spec.IPFamilies {
|
||||
if svc2.Spec.IPFamilies[i] != family {
|
||||
t.Fatalf("failed. expected family %v at %v got %v", family, i, svc2.Spec.IPFamilies[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetDefaultServiceIPFamilyPolicy(t *testing.T) {
|
||||
singleStack := v1.IPFamilyPolicySingleStack
|
||||
preferDualStack := v1.IPFamilyPolicyPreferDualStack
|
||||
requireDualStack := v1.IPFamilyPolicyRequireDualStack
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
expectedIPfamilyPolicy *v1.IPFamilyPolicyType
|
||||
svc v1.Service
|
||||
}{
|
||||
{
|
||||
name: "must not set for ExternalName",
|
||||
expectedIPfamilyPolicy: nil,
|
||||
|
||||
svc: v1.Service{
|
||||
Spec: v1.ServiceSpec{
|
||||
Type: v1.ServiceTypeExternalName,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "must not set for ExternalName even with semantically valid data",
|
||||
expectedIPfamilyPolicy: nil,
|
||||
|
||||
svc: v1.Service{
|
||||
Spec: v1.ServiceSpec{
|
||||
Type: v1.ServiceTypeExternalName,
|
||||
ClusterIPs: []string{"1.2.3.4", "2001::1"},
|
||||
IPFamilies: []v1.IPFamily{v1.IPv4Protocol, v1.IPv6Protocol},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "must set if there are more than one ip",
|
||||
expectedIPfamilyPolicy: &requireDualStack,
|
||||
|
||||
svc: v1.Service{
|
||||
Spec: v1.ServiceSpec{
|
||||
ClusterIPs: []string{"1.2.3.4", "2001::1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "must set if there are more than one ip family",
|
||||
expectedIPfamilyPolicy: &requireDualStack,
|
||||
|
||||
svc: v1.Service{
|
||||
Spec: v1.ServiceSpec{
|
||||
IPFamilies: []v1.IPFamily{v1.IPv4Protocol, v1.IPv6Protocol},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "must not set if there is one ip",
|
||||
expectedIPfamilyPolicy: nil,
|
||||
|
||||
svc: v1.Service{
|
||||
Spec: v1.ServiceSpec{
|
||||
ClusterIPs: []string{"1.2.3.4"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "must not set if there is one ip family",
|
||||
expectedIPfamilyPolicy: nil,
|
||||
|
||||
svc: v1.Service{
|
||||
Spec: v1.ServiceSpec{
|
||||
IPFamilies: []v1.IPFamily{v1.IPv4Protocol},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "must not override user input",
|
||||
expectedIPfamilyPolicy: &singleStack,
|
||||
|
||||
svc: v1.Service{
|
||||
Spec: v1.ServiceSpec{
|
||||
IPFamilyPolicy: &singleStack,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "must not override user input/ preferDualStack",
|
||||
expectedIPfamilyPolicy: &preferDualStack,
|
||||
|
||||
svc: v1.Service{
|
||||
Spec: v1.ServiceSpec{
|
||||
IPFamilyPolicy: &preferDualStack,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "must not override user input even when it is invalid",
|
||||
expectedIPfamilyPolicy: &preferDualStack,
|
||||
|
||||
svc: v1.Service{
|
||||
Spec: v1.ServiceSpec{
|
||||
IPFamilies: []v1.IPFamily{v1.IPv4Protocol, v1.IPv6Protocol},
|
||||
IPFamilyPolicy: &preferDualStack,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
// with gate
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.IPv6DualStack, true)()
|
||||
obj2 := roundTrip(t, runtime.Object(&test.svc))
|
||||
svc2 := obj2.(*v1.Service)
|
||||
|
||||
if test.expectedIPfamilyPolicy == nil && svc2.Spec.IPFamilyPolicy != nil {
|
||||
t.Fatalf("expected .spec.PreferDualStack to be unset (nil)")
|
||||
}
|
||||
if test.expectedIPfamilyPolicy != nil && (svc2.Spec.IPFamilyPolicy == nil || *(svc2.Spec.IPFamilyPolicy) != *(test.expectedIPfamilyPolicy)) {
|
||||
t.Fatalf("expected .spec.PreferDualStack to be set to %v got %v", *(test.expectedIPfamilyPolicy), svc2.Spec.IPFamilyPolicy)
|
||||
}
|
||||
})
|
||||
|
||||
// without gate. IPFamilyPolicy should never change
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
obj2 := roundTrip(t, runtime.Object(&test.svc))
|
||||
svc2 := obj2.(*v1.Service)
|
||||
|
||||
if test.svc.Spec.IPFamilyPolicy == nil && svc2.Spec.IPFamilyPolicy != nil {
|
||||
t.Fatalf("expected .spec.PreferDualStack to be unset (nil)")
|
||||
}
|
||||
if test.svc.Spec.IPFamilyPolicy != nil && (svc2.Spec.IPFamilyPolicy == nil || *(svc2.Spec.IPFamilyPolicy) != *(test.expectedIPfamilyPolicy)) {
|
||||
t.Fatalf("expected .spec.PreferDualStack to be set to %v got %v", *(test.expectedIPfamilyPolicy), svc2.Spec.IPFamilyPolicy)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user