mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 03:41:45 +00:00
Merge pull request #91400 from danwinship/ipfamily-validation
service: fix IPFamily validation and defaulting problems
This commit is contained in:
commit
d01cc01ab4
@ -17,14 +17,21 @@ limitations under the License.
|
||||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
netutils "k8s.io/utils/net"
|
||||
)
|
||||
|
||||
// ValidateConditionalService validates conditionally valid fields.
|
||||
func ValidateConditionalService(service, oldService *api.Service) field.ErrorList {
|
||||
// ValidateConditionalService validates conditionally valid fields. allowedIPFamilies is an ordered
|
||||
// list of the valid IP families (IPv4 or IPv6) that are supported. The first family in the slice
|
||||
// is the cluster default, although the clusterIP here dictates the family defaulting.
|
||||
func ValidateConditionalService(service, oldService *api.Service, allowedIPFamilies []api.IPFamily) field.ErrorList {
|
||||
var errs field.ErrorList
|
||||
// If the SCTPSupport feature is disabled, and the old object isn't using the SCTP feature, prevent the new object from using it
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(features.SCTPSupport) && len(serviceSCTPFields(oldService)) == 0 {
|
||||
@ -32,9 +39,82 @@ func ValidateConditionalService(service, oldService *api.Service) field.ErrorLis
|
||||
errs = append(errs, field.NotSupported(f, api.ProtocolSCTP, []string{string(api.ProtocolTCP), string(api.ProtocolUDP)}))
|
||||
}
|
||||
}
|
||||
|
||||
errs = append(errs, validateIPFamily(service, oldService, allowedIPFamilies)...)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
// validateIPFamily checks the IPFamily field.
|
||||
func validateIPFamily(service, oldService *api.Service, allowedIPFamilies []api.IPFamily) field.ErrorList {
|
||||
var errs field.ErrorList
|
||||
|
||||
// specifically allow an invalid value to remain in storage as long as the user isn't changing it, regardless of gate
|
||||
if oldService != nil && oldService.Spec.IPFamily != nil && service.Spec.IPFamily != nil && *oldService.Spec.IPFamily == *service.Spec.IPFamily {
|
||||
return errs
|
||||
}
|
||||
|
||||
// If the gate is off, setting or changing IPFamily is not allowed, but clearing it is
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(features.IPv6DualStack) {
|
||||
if service.Spec.IPFamily != nil {
|
||||
if oldService != nil {
|
||||
errs = append(errs, ValidateImmutableField(service.Spec.IPFamily, oldService.Spec.IPFamily, field.NewPath("spec", "ipFamily"))...)
|
||||
} else {
|
||||
errs = append(errs, field.Forbidden(field.NewPath("spec", "ipFamily"), "programmer error, must be cleared when the dual-stack feature gate is off"))
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// PrepareCreate, PrepareUpdate, and test cases must all set IPFamily when the gate is on
|
||||
if service.Spec.IPFamily == nil {
|
||||
errs = append(errs, field.Required(field.NewPath("spec", "ipFamily"), "programmer error, must be set or defaulted by other fields"))
|
||||
return errs
|
||||
}
|
||||
|
||||
// A user is not allowed to change the IPFamily field, except for ExternalName services
|
||||
if oldService != nil && oldService.Spec.IPFamily != nil && service.Spec.Type != api.ServiceTypeExternalName {
|
||||
errs = append(errs, ValidateImmutableField(service.Spec.IPFamily, oldService.Spec.IPFamily, field.NewPath("spec", "ipFamily"))...)
|
||||
}
|
||||
|
||||
// Verify the IPFamily is one of the allowed families
|
||||
desiredFamily := *service.Spec.IPFamily
|
||||
if hasIPFamily(allowedIPFamilies, desiredFamily) {
|
||||
// the IP family is one of the allowed families, verify that it matches cluster IP
|
||||
switch ip := net.ParseIP(service.Spec.ClusterIP); {
|
||||
case ip == nil:
|
||||
// do not need to check anything
|
||||
case netutils.IsIPv6(ip) && desiredFamily != api.IPv6Protocol:
|
||||
errs = append(errs, field.Invalid(field.NewPath("spec", "ipFamily"), *service.Spec.IPFamily, "does not match IPv6 cluster IP"))
|
||||
case !netutils.IsIPv6(ip) && desiredFamily != api.IPv4Protocol:
|
||||
errs = append(errs, field.Invalid(field.NewPath("spec", "ipFamily"), *service.Spec.IPFamily, "does not match IPv4 cluster IP"))
|
||||
}
|
||||
} else {
|
||||
errs = append(errs, field.Invalid(field.NewPath("spec", "ipFamily"), desiredFamily, fmt.Sprintf("only the following families are allowed: %s", joinIPFamilies(allowedIPFamilies, ", "))))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
func hasIPFamily(families []api.IPFamily, family api.IPFamily) bool {
|
||||
for _, allow := range families {
|
||||
if allow == family {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func joinIPFamilies(families []api.IPFamily, separator string) string {
|
||||
var b strings.Builder
|
||||
for i, family := range families {
|
||||
if i != 0 {
|
||||
b.WriteString(separator)
|
||||
}
|
||||
b.WriteString(string(family))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func serviceSCTPFields(service *api.Service) []*field.Path {
|
||||
if service == nil {
|
||||
return nil
|
||||
|
@ -19,6 +19,7 @@ package validation
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
@ -153,7 +154,7 @@ func TestValidateServiceSCTP(t *testing.T) {
|
||||
|
||||
t.Run(fmt.Sprintf("feature enabled=%v, old object %v, new object %v", enabled, oldServiceInfo.description, newServiceInfo.description), func(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SCTPSupport, enabled)()
|
||||
errs := ValidateConditionalService(newService, oldService)
|
||||
errs := ValidateConditionalService(newService, oldService, []api.IPFamily{api.IPv4Protocol})
|
||||
// objects should never be changed
|
||||
if !reflect.DeepEqual(oldService, oldServiceInfo.object()) {
|
||||
t.Errorf("old object changed: %v", diff.ObjectReflectDiff(oldService, oldServiceInfo.object()))
|
||||
@ -251,3 +252,240 @@ func TestValidateEndpointsSCTP(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateServiceIPFamily(t *testing.T) {
|
||||
ipv4 := api.IPv4Protocol
|
||||
ipv6 := api.IPv6Protocol
|
||||
var unknown api.IPFamily = "Unknown"
|
||||
testCases := []struct {
|
||||
name string
|
||||
dualStackEnabled bool
|
||||
ipFamilies []api.IPFamily
|
||||
svc *api.Service
|
||||
oldSvc *api.Service
|
||||
expectErr []string
|
||||
}{
|
||||
{
|
||||
name: "allowed ipv4",
|
||||
dualStackEnabled: true,
|
||||
ipFamilies: []api.IPFamily{api.IPv4Protocol},
|
||||
svc: &api.Service{
|
||||
Spec: api.ServiceSpec{
|
||||
IPFamily: &ipv4,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "allowed ipv6",
|
||||
dualStackEnabled: true,
|
||||
ipFamilies: []api.IPFamily{api.IPv6Protocol},
|
||||
svc: &api.Service{
|
||||
Spec: api.ServiceSpec{
|
||||
IPFamily: &ipv6,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "allowed ipv4 dual stack default IPv6",
|
||||
dualStackEnabled: true,
|
||||
ipFamilies: []api.IPFamily{api.IPv6Protocol, api.IPv4Protocol},
|
||||
svc: &api.Service{
|
||||
Spec: api.ServiceSpec{
|
||||
IPFamily: &ipv4,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "allowed ipv4 dual stack default IPv4",
|
||||
dualStackEnabled: true,
|
||||
ipFamilies: []api.IPFamily{api.IPv4Protocol, api.IPv6Protocol},
|
||||
svc: &api.Service{
|
||||
Spec: api.ServiceSpec{
|
||||
IPFamily: &ipv4,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "allowed ipv6 dual stack default IPv6",
|
||||
dualStackEnabled: true,
|
||||
ipFamilies: []api.IPFamily{api.IPv6Protocol, api.IPv4Protocol},
|
||||
svc: &api.Service{
|
||||
Spec: api.ServiceSpec{
|
||||
IPFamily: &ipv6,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "allowed ipv6 dual stack default IPv4",
|
||||
dualStackEnabled: true,
|
||||
ipFamilies: []api.IPFamily{api.IPv4Protocol, api.IPv6Protocol},
|
||||
svc: &api.Service{
|
||||
Spec: api.ServiceSpec{
|
||||
IPFamily: &ipv6,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "allow ipfamily to remain invalid if update doesn't change it",
|
||||
dualStackEnabled: true,
|
||||
ipFamilies: []api.IPFamily{api.IPv4Protocol, api.IPv6Protocol},
|
||||
svc: &api.Service{
|
||||
Spec: api.ServiceSpec{
|
||||
IPFamily: &unknown,
|
||||
},
|
||||
},
|
||||
oldSvc: &api.Service{
|
||||
Spec: api.ServiceSpec{
|
||||
IPFamily: &unknown,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not allowed ipfamily/clusterip mismatch",
|
||||
dualStackEnabled: true,
|
||||
ipFamilies: []api.IPFamily{api.IPv4Protocol, api.IPv6Protocol},
|
||||
svc: &api.Service{
|
||||
Spec: api.ServiceSpec{
|
||||
IPFamily: &ipv4,
|
||||
ClusterIP: "ffd0::1",
|
||||
},
|
||||
},
|
||||
expectErr: []string{"spec.ipFamily: Invalid value: \"IPv4\": does not match IPv6 cluster IP"},
|
||||
},
|
||||
{
|
||||
name: "not allowed unknown family",
|
||||
dualStackEnabled: true,
|
||||
ipFamilies: []api.IPFamily{api.IPv4Protocol},
|
||||
svc: &api.Service{
|
||||
Spec: api.ServiceSpec{
|
||||
IPFamily: &unknown,
|
||||
},
|
||||
},
|
||||
expectErr: []string{"spec.ipFamily: Invalid value: \"Unknown\": only the following families are allowed: IPv4"},
|
||||
},
|
||||
{
|
||||
name: "not allowed ipv4 cluster ip without family",
|
||||
dualStackEnabled: true,
|
||||
ipFamilies: []api.IPFamily{api.IPv6Protocol},
|
||||
svc: &api.Service{
|
||||
Spec: api.ServiceSpec{
|
||||
ClusterIP: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
expectErr: []string{"spec.ipFamily: Required value: programmer error, must be set or defaulted by other fields"},
|
||||
},
|
||||
{
|
||||
name: "not allowed ipv6 cluster ip without family",
|
||||
dualStackEnabled: true,
|
||||
ipFamilies: []api.IPFamily{api.IPv4Protocol},
|
||||
svc: &api.Service{
|
||||
Spec: api.ServiceSpec{
|
||||
ClusterIP: "ffd0::1",
|
||||
},
|
||||
},
|
||||
expectErr: []string{"spec.ipFamily: Required value: programmer error, must be set or defaulted by other fields"},
|
||||
},
|
||||
|
||||
{
|
||||
name: "not allowed to change ipfamily for default type",
|
||||
dualStackEnabled: true,
|
||||
ipFamilies: []api.IPFamily{api.IPv4Protocol},
|
||||
svc: &api.Service{
|
||||
Spec: api.ServiceSpec{
|
||||
IPFamily: &ipv4,
|
||||
},
|
||||
},
|
||||
oldSvc: &api.Service{
|
||||
Spec: api.ServiceSpec{
|
||||
IPFamily: &ipv6,
|
||||
},
|
||||
},
|
||||
expectErr: []string{"spec.ipFamily: Invalid value: \"IPv4\": field is immutable"},
|
||||
},
|
||||
{
|
||||
name: "allowed to change ipfamily for external name",
|
||||
dualStackEnabled: true,
|
||||
ipFamilies: []api.IPFamily{api.IPv4Protocol},
|
||||
svc: &api.Service{
|
||||
Spec: api.ServiceSpec{
|
||||
Type: api.ServiceTypeExternalName,
|
||||
IPFamily: &ipv4,
|
||||
},
|
||||
},
|
||||
oldSvc: &api.Service{
|
||||
Spec: api.ServiceSpec{
|
||||
Type: api.ServiceTypeExternalName,
|
||||
IPFamily: &ipv6,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "ipfamily allowed to be empty when dual stack is off",
|
||||
dualStackEnabled: false,
|
||||
ipFamilies: []api.IPFamily{api.IPv4Protocol},
|
||||
svc: &api.Service{
|
||||
Spec: api.ServiceSpec{
|
||||
ClusterIP: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ipfamily must be empty when dual stack is off",
|
||||
dualStackEnabled: false,
|
||||
ipFamilies: []api.IPFamily{api.IPv4Protocol},
|
||||
svc: &api.Service{
|
||||
Spec: api.ServiceSpec{
|
||||
IPFamily: &ipv4,
|
||||
ClusterIP: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
expectErr: []string{"spec.ipFamily: Forbidden: programmer error, must be cleared when the dual-stack feature gate is off"},
|
||||
},
|
||||
{
|
||||
name: "ipfamily allowed to be cleared when dual stack is off",
|
||||
dualStackEnabled: false,
|
||||
ipFamilies: []api.IPFamily{api.IPv4Protocol},
|
||||
svc: &api.Service{
|
||||
Spec: api.ServiceSpec{
|
||||
Type: api.ServiceTypeClusterIP,
|
||||
ClusterIP: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
oldSvc: &api.Service{
|
||||
Spec: api.ServiceSpec{
|
||||
Type: api.ServiceTypeClusterIP,
|
||||
ClusterIP: "127.0.0.1",
|
||||
IPFamily: &ipv4,
|
||||
},
|
||||
},
|
||||
expectErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.IPv6DualStack, tc.dualStackEnabled)()
|
||||
oldSvc := tc.oldSvc.DeepCopy()
|
||||
newSvc := tc.svc.DeepCopy()
|
||||
originalNewSvc := newSvc.DeepCopy()
|
||||
errs := ValidateConditionalService(newSvc, oldSvc, tc.ipFamilies)
|
||||
// objects should never be changed
|
||||
if !reflect.DeepEqual(oldSvc, tc.oldSvc) {
|
||||
t.Errorf("old object changed: %v", diff.ObjectReflectDiff(oldSvc, tc.svc))
|
||||
}
|
||||
if !reflect.DeepEqual(newSvc, originalNewSvc) {
|
||||
t.Errorf("new object changed: %v", diff.ObjectReflectDiff(newSvc, originalNewSvc))
|
||||
}
|
||||
|
||||
if len(errs) != len(tc.expectErr) {
|
||||
t.Fatalf("unexpected number of errors: %v", errs)
|
||||
}
|
||||
for i := range errs {
|
||||
if !strings.Contains(errs[i].Error(), tc.expectErr[i]) {
|
||||
t.Errorf("unexpected error %d: %v", i, errs[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -3951,7 +3951,6 @@ 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, allowAppProtocol bool) field.ErrorList {
|
||||
@ -4152,21 +4151,6 @@ func ValidateService(service *core.Service, allowAppProtocol bool) field.ErrorLi
|
||||
}
|
||||
}
|
||||
|
||||
//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
|
||||
}
|
||||
@ -4275,19 +4259,12 @@ func ValidateServiceCreate(service *core.Service) field.ErrorList {
|
||||
func ValidateServiceUpdate(service, oldService *core.Service) field.ErrorList {
|
||||
allErrs := ValidateObjectMetaUpdate(&service.ObjectMeta, &oldService.ObjectMeta, field.NewPath("metadata"))
|
||||
|
||||
// ClusterIP and IPFamily should be immutable for services using it (every type other than ExternalName)
|
||||
// ClusterIP 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"))...)
|
||||
}
|
||||
}
|
||||
|
||||
// allow AppProtocol value if the feature gate is set or the field is
|
||||
|
@ -10193,12 +10193,12 @@ func TestValidateServiceCreate(t *testing.T) {
|
||||
numErrs: 0,
|
||||
},
|
||||
{
|
||||
name: "invalid, service with invalid IPFamily",
|
||||
name: "allowed valid, service with invalid IPFamily is ignored (tested in conditional validation)",
|
||||
tweakSvc: func(s *core.Service) {
|
||||
invalidServiceIPFamily := core.IPFamily("not-a-valid-ip-family")
|
||||
s.Spec.IPFamily = &invalidServiceIPFamily
|
||||
},
|
||||
numErrs: 1,
|
||||
numErrs: 0,
|
||||
},
|
||||
{
|
||||
name: "valid topology keys",
|
||||
@ -12204,18 +12204,18 @@ func TestValidateServiceUpdate(t *testing.T) {
|
||||
numErrs: 0,
|
||||
},
|
||||
{
|
||||
name: "remove ipfamily",
|
||||
name: "remove ipfamily (covered by conditional validation)",
|
||||
tweakSvc: func(oldSvc, newSvc *core.Service) {
|
||||
ipv6Service := core.IPv6Protocol
|
||||
oldSvc.Spec.IPFamily = &ipv6Service
|
||||
|
||||
newSvc.Spec.IPFamily = nil
|
||||
},
|
||||
numErrs: 1,
|
||||
numErrs: 0,
|
||||
},
|
||||
|
||||
{
|
||||
name: "change ServiceIPFamily",
|
||||
name: "change ServiceIPFamily (covered by conditional validation)",
|
||||
tweakSvc: func(oldSvc, newSvc *core.Service) {
|
||||
ipv4Service := core.IPv4Protocol
|
||||
oldSvc.Spec.Type = core.ServiceTypeClusterIP
|
||||
@ -12225,7 +12225,7 @@ func TestValidateServiceUpdate(t *testing.T) {
|
||||
newSvc.Spec.Type = core.ServiceTypeClusterIP
|
||||
newSvc.Spec.IPFamily = &ipv6Service
|
||||
},
|
||||
numErrs: 1,
|
||||
numErrs: 0,
|
||||
},
|
||||
{
|
||||
name: "update with valid app protocol, field unset, gate disabled",
|
||||
|
@ -190,11 +190,6 @@ func (c LegacyRESTStorageProvider) NewLegacyRESTStorage(restOptionsGetter generi
|
||||
return LegacyRESTStorage{}, genericapiserver.APIGroupInfo{}, err
|
||||
}
|
||||
|
||||
serviceRESTStorage, serviceStatusStorage, err := servicestore.NewGenericREST(restOptionsGetter)
|
||||
if err != nil {
|
||||
return LegacyRESTStorage{}, genericapiserver.APIGroupInfo{}, err
|
||||
}
|
||||
|
||||
var serviceClusterIPRegistry rangeallocation.RangeRegistry
|
||||
serviceClusterIPRange := c.ServiceIPRange
|
||||
if serviceClusterIPRange.IP == nil {
|
||||
@ -262,6 +257,11 @@ func (c LegacyRESTStorageProvider) NewLegacyRESTStorage(restOptionsGetter generi
|
||||
return LegacyRESTStorage{}, genericapiserver.APIGroupInfo{}, err
|
||||
}
|
||||
|
||||
serviceRESTStorage, serviceStatusStorage, err := servicestore.NewGenericREST(restOptionsGetter, serviceClusterIPRange, secondaryServiceClusterIPAllocator != nil)
|
||||
if err != nil {
|
||||
return LegacyRESTStorage{}, genericapiserver.APIGroupInfo{}, err
|
||||
}
|
||||
|
||||
serviceRest, serviceRestProxy := servicestore.NewREST(serviceRESTStorage,
|
||||
endpointsStorage,
|
||||
podStorage.Pod,
|
||||
|
@ -27,6 +27,7 @@ go_library(
|
||||
"//staging/src/k8s.io/apiserver/pkg/registry/rest:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/storage/names:go_default_library",
|
||||
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||
"//vendor/k8s.io/utils/net:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
|
@ -29,6 +29,7 @@ go_test(
|
||||
"//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/intstr:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/net:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/rand:go_default_library",
|
||||
|
@ -53,14 +53,14 @@ import (
|
||||
|
||||
// REST adapts a service registry into apiserver's RESTStorage model.
|
||||
type REST struct {
|
||||
services ServiceStorage
|
||||
endpoints EndpointsStorage
|
||||
serviceIPs ipallocator.Interface
|
||||
secondaryServiceIPs ipallocator.Interface
|
||||
defaultServiceIPFamily api.IPFamily
|
||||
serviceNodePorts portallocator.Interface
|
||||
proxyTransport http.RoundTripper
|
||||
pods rest.Getter
|
||||
strategy rest.RESTCreateUpdateStrategy
|
||||
services ServiceStorage
|
||||
endpoints EndpointsStorage
|
||||
serviceIPs ipallocator.Interface
|
||||
secondaryServiceIPs ipallocator.Interface
|
||||
serviceNodePorts portallocator.Interface
|
||||
proxyTransport http.RoundTripper
|
||||
pods rest.Getter
|
||||
}
|
||||
|
||||
// ServiceNodePort includes protocol and port number of a service NodePort.
|
||||
@ -102,26 +102,20 @@ func NewREST(
|
||||
serviceNodePorts portallocator.Interface,
|
||||
proxyTransport http.RoundTripper,
|
||||
) (*REST, *registry.ProxyREST) {
|
||||
// detect this cluster default Service IPFamily (ipfamily of --service-cluster-ip-range)
|
||||
// we do it once here, to avoid having to do it over and over during ipfamily assignment
|
||||
serviceIPFamily := api.IPv4Protocol
|
||||
cidr := serviceIPs.CIDR()
|
||||
if netutil.IsIPv6CIDR(&cidr) {
|
||||
serviceIPFamily = api.IPv6Protocol
|
||||
}
|
||||
|
||||
klog.V(0).Infof("the default service ipfamily for this cluster is: %s", string(serviceIPFamily))
|
||||
strategy, _ := registry.StrategyForServiceCIDRs(serviceIPs.CIDR(), secondaryServiceIPs != nil)
|
||||
|
||||
rest := &REST{
|
||||
services: services,
|
||||
endpoints: endpoints,
|
||||
serviceIPs: serviceIPs,
|
||||
secondaryServiceIPs: secondaryServiceIPs,
|
||||
serviceNodePorts: serviceNodePorts,
|
||||
defaultServiceIPFamily: serviceIPFamily,
|
||||
proxyTransport: proxyTransport,
|
||||
pods: pods,
|
||||
strategy: strategy,
|
||||
services: services,
|
||||
endpoints: endpoints,
|
||||
serviceIPs: serviceIPs,
|
||||
secondaryServiceIPs: secondaryServiceIPs,
|
||||
serviceNodePorts: serviceNodePorts,
|
||||
proxyTransport: proxyTransport,
|
||||
pods: pods,
|
||||
}
|
||||
|
||||
return rest, ®istry.ProxyREST{Redirector: rest, ProxyTransport: proxyTransport}
|
||||
}
|
||||
|
||||
@ -177,12 +171,7 @@ func (rs *REST) Export(ctx context.Context, name string, opts metav1.ExportOptio
|
||||
func (rs *REST) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
|
||||
service := obj.(*api.Service)
|
||||
|
||||
// set the service ip family, if it was not already set
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.IPv6DualStack) && service.Spec.IPFamily == nil {
|
||||
service.Spec.IPFamily = &rs.defaultServiceIPFamily
|
||||
}
|
||||
|
||||
if err := rest.BeforeCreate(registry.Strategy, ctx, obj); err != nil {
|
||||
if err := rest.BeforeCreate(rs.strategy, ctx, obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -228,7 +217,7 @@ func (rs *REST) Create(ctx context.Context, obj runtime.Object, createValidation
|
||||
|
||||
out, err := rs.services.Create(ctx, service, createValidation, options)
|
||||
if err != nil {
|
||||
err = rest.CheckGeneratedNameError(registry.Strategy, err, service)
|
||||
err = rest.CheckGeneratedNameError(rs.strategy, err, service)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
@ -396,7 +385,7 @@ func (rs *REST) Update(ctx context.Context, name string, objInfo rest.UpdatedObj
|
||||
}
|
||||
|
||||
// Copy over non-user fields
|
||||
if err := rest.BeforeUpdate(registry.Strategy, ctx, service, oldService); err != nil {
|
||||
if err := rest.BeforeUpdate(rs.strategy, ctx, service, oldService); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
@ -666,6 +655,8 @@ func allocateHealthCheckNodePort(service *api.Service, nodePortOp *portallocator
|
||||
|
||||
// The return bool value indicates if a cluster IP is allocated successfully.
|
||||
func initClusterIP(service *api.Service, allocator ipallocator.Interface) (bool, error) {
|
||||
var allocatedIP net.IP
|
||||
|
||||
switch {
|
||||
case service.Spec.ClusterIP == "":
|
||||
// Allocate next available.
|
||||
@ -676,19 +667,32 @@ func initClusterIP(service *api.Service, allocator ipallocator.Interface) (bool,
|
||||
// not really an internal error.
|
||||
return false, errors.NewInternalError(fmt.Errorf("failed to allocate a serviceIP: %v", err))
|
||||
}
|
||||
allocatedIP = ip
|
||||
service.Spec.ClusterIP = ip.String()
|
||||
return true, nil
|
||||
case service.Spec.ClusterIP != api.ClusterIPNone && service.Spec.ClusterIP != "":
|
||||
// Try to respect the requested IP.
|
||||
if err := allocator.Allocate(net.ParseIP(service.Spec.ClusterIP)); err != nil {
|
||||
ip := net.ParseIP(service.Spec.ClusterIP)
|
||||
if err := allocator.Allocate(ip); err != nil {
|
||||
// TODO: when validation becomes versioned, this gets more complicated.
|
||||
el := field.ErrorList{field.Invalid(field.NewPath("spec", "clusterIP"), service.Spec.ClusterIP, err.Error())}
|
||||
return false, errors.NewInvalid(api.Kind("Service"), service.Name, el)
|
||||
}
|
||||
return true, nil
|
||||
allocatedIP = ip
|
||||
}
|
||||
|
||||
return false, nil
|
||||
// assuming the object was valid prior to setting, always force the IPFamily
|
||||
// to match the allocated IP at this point
|
||||
if allocatedIP != nil {
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.IPv6DualStack) {
|
||||
ipFamily := api.IPv4Protocol
|
||||
if netutil.IsIPv6(allocatedIP) {
|
||||
ipFamily = api.IPv6Protocol
|
||||
}
|
||||
service.Spec.IPFamily = &ipFamily
|
||||
}
|
||||
}
|
||||
|
||||
return allocatedIP != nil, nil
|
||||
}
|
||||
|
||||
func initNodePorts(service *api.Service, nodePortOp *portallocator.PortAllocationOperation) error {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -18,6 +18,7 @@ package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
@ -29,6 +30,7 @@ import (
|
||||
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
|
||||
printerstorage "k8s.io/kubernetes/pkg/printers/storage"
|
||||
"k8s.io/kubernetes/pkg/registry/core/service"
|
||||
registry "k8s.io/kubernetes/pkg/registry/core/service"
|
||||
)
|
||||
|
||||
type GenericREST struct {
|
||||
@ -36,17 +38,19 @@ type GenericREST struct {
|
||||
}
|
||||
|
||||
// NewREST returns a RESTStorage object that will work against services.
|
||||
func NewGenericREST(optsGetter generic.RESTOptionsGetter) (*GenericREST, *StatusREST, error) {
|
||||
func NewGenericREST(optsGetter generic.RESTOptionsGetter, serviceCIDR net.IPNet, hasSecondary bool) (*GenericREST, *StatusREST, error) {
|
||||
strategy, _ := registry.StrategyForServiceCIDRs(serviceCIDR, hasSecondary)
|
||||
|
||||
store := &genericregistry.Store{
|
||||
NewFunc: func() runtime.Object { return &api.Service{} },
|
||||
NewListFunc: func() runtime.Object { return &api.ServiceList{} },
|
||||
DefaultQualifiedResource: api.Resource("services"),
|
||||
ReturnDeletedObject: true,
|
||||
|
||||
CreateStrategy: service.Strategy,
|
||||
UpdateStrategy: service.Strategy,
|
||||
DeleteStrategy: service.Strategy,
|
||||
ExportStrategy: service.Strategy,
|
||||
CreateStrategy: strategy,
|
||||
UpdateStrategy: strategy,
|
||||
DeleteStrategy: strategy,
|
||||
ExportStrategy: strategy,
|
||||
|
||||
TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},
|
||||
}
|
||||
@ -56,7 +60,7 @@ func NewGenericREST(optsGetter generic.RESTOptionsGetter) (*GenericREST, *Status
|
||||
}
|
||||
|
||||
statusStore := *store
|
||||
statusStore.UpdateStrategy = service.StatusStrategy
|
||||
statusStore.UpdateStrategy = service.NewServiceStatusStrategy(strategy)
|
||||
return &GenericREST{store}, &StatusREST{store: &statusStore}, nil
|
||||
}
|
||||
|
||||
|
@ -39,7 +39,7 @@ func newStorage(t *testing.T) (*GenericREST, *StatusREST, *etcd3testing.EtcdTest
|
||||
DeleteCollectionWorkers: 1,
|
||||
ResourcePrefix: "services",
|
||||
}
|
||||
serviceStorage, statusStorage, err := NewGenericREST(restOptions)
|
||||
serviceStorage, statusStorage, err := NewGenericREST(restOptions, *makeIPNet(t), false)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error from REST storage: %v", err)
|
||||
}
|
||||
|
@ -19,54 +19,113 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
"k8s.io/apiserver/pkg/storage/names"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/apis/core/validation"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
netutil "k8s.io/utils/net"
|
||||
)
|
||||
|
||||
type Strategy interface {
|
||||
rest.RESTCreateUpdateStrategy
|
||||
rest.RESTExportStrategy
|
||||
}
|
||||
|
||||
// svcStrategy implements behavior for Services
|
||||
type svcStrategy struct {
|
||||
runtime.ObjectTyper
|
||||
names.NameGenerator
|
||||
|
||||
ipFamilies []api.IPFamily
|
||||
}
|
||||
|
||||
// Services is the default logic that applies when creating and updating Service
|
||||
// objects.
|
||||
var Strategy = svcStrategy{legacyscheme.Scheme, names.SimpleNameGenerator}
|
||||
// StrategyForServiceCIDRs returns the appropriate service strategy for the given configuration.
|
||||
func StrategyForServiceCIDRs(primaryCIDR net.IPNet, hasSecondary bool) (Strategy, api.IPFamily) {
|
||||
// detect this cluster default Service IPFamily (ipfamily of --service-cluster-ip-range)
|
||||
// we do it once here, to avoid having to do it over and over during ipfamily assignment
|
||||
serviceIPFamily := api.IPv4Protocol
|
||||
if netutil.IsIPv6CIDR(&primaryCIDR) {
|
||||
serviceIPFamily = api.IPv6Protocol
|
||||
}
|
||||
|
||||
var strategy Strategy
|
||||
switch {
|
||||
case hasSecondary && serviceIPFamily == api.IPv4Protocol:
|
||||
strategy = svcStrategy{
|
||||
ObjectTyper: legacyscheme.Scheme,
|
||||
NameGenerator: names.SimpleNameGenerator,
|
||||
ipFamilies: []api.IPFamily{api.IPv4Protocol, api.IPv6Protocol},
|
||||
}
|
||||
case hasSecondary && serviceIPFamily == api.IPv6Protocol:
|
||||
strategy = svcStrategy{
|
||||
ObjectTyper: legacyscheme.Scheme,
|
||||
NameGenerator: names.SimpleNameGenerator,
|
||||
ipFamilies: []api.IPFamily{api.IPv6Protocol, api.IPv4Protocol},
|
||||
}
|
||||
case serviceIPFamily == api.IPv6Protocol:
|
||||
strategy = svcStrategy{
|
||||
ObjectTyper: legacyscheme.Scheme,
|
||||
NameGenerator: names.SimpleNameGenerator,
|
||||
ipFamilies: []api.IPFamily{api.IPv6Protocol},
|
||||
}
|
||||
default:
|
||||
strategy = svcStrategy{
|
||||
ObjectTyper: legacyscheme.Scheme,
|
||||
NameGenerator: names.SimpleNameGenerator,
|
||||
ipFamilies: []api.IPFamily{api.IPv4Protocol},
|
||||
}
|
||||
}
|
||||
return strategy, serviceIPFamily
|
||||
}
|
||||
|
||||
// NamespaceScoped is true for services.
|
||||
func (svcStrategy) NamespaceScoped() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// PrepareForCreate clears fields that are not allowed to be set by end users on creation.
|
||||
func (svcStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
|
||||
// PrepareForCreate sets contextual defaults and clears fields that are not allowed to be set by end users on creation.
|
||||
func (strategy svcStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
|
||||
service := obj.(*api.Service)
|
||||
service.Status = api.ServiceStatus{}
|
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.IPv6DualStack) && service.Spec.IPFamily == nil {
|
||||
family := strategy.ipFamilies[0]
|
||||
service.Spec.IPFamily = &family
|
||||
}
|
||||
|
||||
dropServiceDisabledFields(service, nil)
|
||||
}
|
||||
|
||||
// PrepareForUpdate clears fields that are not allowed to be set by end users on update.
|
||||
func (svcStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
||||
// PrepareForUpdate sets contextual defaults and clears fields that are not allowed to be set by end users on update.
|
||||
func (strategy svcStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
||||
newService := obj.(*api.Service)
|
||||
oldService := old.(*api.Service)
|
||||
newService.Status = oldService.Status
|
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.IPv6DualStack) && newService.Spec.IPFamily == nil {
|
||||
if oldService.Spec.IPFamily != nil {
|
||||
newService.Spec.IPFamily = oldService.Spec.IPFamily
|
||||
} else {
|
||||
family := strategy.ipFamilies[0]
|
||||
newService.Spec.IPFamily = &family
|
||||
}
|
||||
}
|
||||
|
||||
dropServiceDisabledFields(newService, oldService)
|
||||
}
|
||||
|
||||
// Validate validates a new service.
|
||||
func (svcStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
|
||||
func (strategy svcStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
|
||||
service := obj.(*api.Service)
|
||||
allErrs := validation.ValidateServiceCreate(service)
|
||||
allErrs = append(allErrs, validation.ValidateConditionalService(service, nil)...)
|
||||
allErrs = append(allErrs, validation.ValidateConditionalService(service, nil, strategy.ipFamilies)...)
|
||||
return allErrs
|
||||
}
|
||||
|
||||
@ -78,9 +137,9 @@ func (svcStrategy) AllowCreateOnUpdate() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (svcStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
||||
func (strategy svcStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
||||
allErrs := validation.ValidateServiceUpdate(obj.(*api.Service), old.(*api.Service))
|
||||
allErrs = append(allErrs, validation.ValidateConditionalService(obj.(*api.Service), old.(*api.Service))...)
|
||||
allErrs = append(allErrs, validation.ValidateConditionalService(obj.(*api.Service), old.(*api.Service), strategy.ipFamilies)...)
|
||||
return allErrs
|
||||
}
|
||||
|
||||
@ -120,6 +179,7 @@ func dropServiceDisabledFields(newSvc *api.Service, oldSvc *api.Service) {
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(features.IPv6DualStack) && !serviceIPFamilyInUse(oldSvc) {
|
||||
newSvc.Spec.IPFamily = nil
|
||||
}
|
||||
|
||||
// Drop TopologyKeys if ServiceTopology is not enabled
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(features.ServiceTopology) && !topologyKeysInUse(oldSvc) {
|
||||
newSvc.Spec.TopologyKeys = nil
|
||||
@ -146,11 +206,13 @@ func topologyKeysInUse(svc *api.Service) bool {
|
||||
}
|
||||
|
||||
type serviceStatusStrategy struct {
|
||||
svcStrategy
|
||||
Strategy
|
||||
}
|
||||
|
||||
// StatusStrategy is the default logic invoked when updating service status.
|
||||
var StatusStrategy = serviceStatusStrategy{Strategy}
|
||||
// NewServiceStatusStrategy creates a status strategy for the provided base strategy.
|
||||
func NewServiceStatusStrategy(strategy Strategy) Strategy {
|
||||
return serviceStatusStrategy{strategy}
|
||||
}
|
||||
|
||||
// PrepareForUpdate clears fields that are not allowed to be set by end users on update of status
|
||||
func (serviceStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||
package service
|
||||
|
||||
import (
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
@ -35,7 +36,18 @@ import (
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
)
|
||||
|
||||
func newStrategy(cidr string, hasSecondary bool) (testStrategy Strategy, testStatusStrategy Strategy) {
|
||||
_, testCIDR, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
panic("invalid CIDR")
|
||||
}
|
||||
testStrategy, _ = StrategyForServiceCIDRs(*testCIDR, hasSecondary)
|
||||
testStatusStrategy = NewServiceStatusStrategy(testStrategy)
|
||||
return
|
||||
}
|
||||
|
||||
func TestExportService(t *testing.T) {
|
||||
testStrategy, _ := newStrategy("10.0.0.0/16", false)
|
||||
tests := []struct {
|
||||
objIn runtime.Object
|
||||
objOut runtime.Object
|
||||
@ -98,7 +110,7 @@ func TestExportService(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
err := Strategy.Export(genericapirequest.NewContext(), test.objIn, test.exact)
|
||||
err := testStrategy.Export(genericapirequest.NewContext(), test.objIn, test.exact)
|
||||
if err != nil {
|
||||
if !test.expectErr {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
@ -116,18 +128,19 @@ func TestExportService(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCheckGeneratedNameError(t *testing.T) {
|
||||
testStrategy, _ := newStrategy("10.0.0.0/16", false)
|
||||
expect := errors.NewNotFound(api.Resource("foos"), "bar")
|
||||
if err := rest.CheckGeneratedNameError(Strategy, expect, &api.Service{}); err != expect {
|
||||
if err := rest.CheckGeneratedNameError(testStrategy, expect, &api.Service{}); err != expect {
|
||||
t.Errorf("NotFoundError should be ignored: %v", err)
|
||||
}
|
||||
|
||||
expect = errors.NewAlreadyExists(api.Resource("foos"), "bar")
|
||||
if err := rest.CheckGeneratedNameError(Strategy, expect, &api.Service{}); err != expect {
|
||||
if err := rest.CheckGeneratedNameError(testStrategy, expect, &api.Service{}); err != expect {
|
||||
t.Errorf("AlreadyExists should be returned when no GenerateName field: %v", err)
|
||||
}
|
||||
|
||||
expect = errors.NewAlreadyExists(api.Resource("foos"), "bar")
|
||||
if err := rest.CheckGeneratedNameError(Strategy, expect, &api.Service{ObjectMeta: metav1.ObjectMeta{GenerateName: "foo"}}); err == nil || !errors.IsServerTimeout(err) {
|
||||
if err := rest.CheckGeneratedNameError(testStrategy, expect, &api.Service{ObjectMeta: metav1.ObjectMeta{GenerateName: "foo"}}); err == nil || !errors.IsServerTimeout(err) {
|
||||
t.Errorf("expected try again later error: %v", err)
|
||||
}
|
||||
}
|
||||
@ -153,11 +166,131 @@ func makeValidService() api.Service {
|
||||
}
|
||||
|
||||
// TODO: This should be done on types that are not part of our API
|
||||
func TestBeforeCreate(t *testing.T) {
|
||||
withIP := func(family *api.IPFamily, ip string) *api.Service {
|
||||
svc := makeValidService()
|
||||
svc.Spec.IPFamily = family
|
||||
svc.Spec.ClusterIP = ip
|
||||
return &svc
|
||||
}
|
||||
|
||||
ipv4 := api.IPv4Protocol
|
||||
ipv6 := api.IPv6Protocol
|
||||
testCases := []struct {
|
||||
name string
|
||||
cidr string
|
||||
configureDualStack bool
|
||||
enableDualStack bool
|
||||
in *api.Service
|
||||
expect *api.Service
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "does not set ipfamily when dual stack gate is disabled",
|
||||
cidr: "10.0.0.0/16",
|
||||
in: withIP(nil, ""),
|
||||
expect: withIP(nil, ""),
|
||||
},
|
||||
|
||||
{
|
||||
name: "clears ipfamily when dual stack gate is disabled",
|
||||
cidr: "10.0.0.0/16",
|
||||
in: withIP(&ipv4, ""),
|
||||
expect: withIP(nil, ""),
|
||||
},
|
||||
|
||||
{
|
||||
name: "allows ipfamily to configured ipv4 value",
|
||||
cidr: "10.0.0.0/16",
|
||||
enableDualStack: true,
|
||||
in: withIP(nil, ""),
|
||||
expect: withIP(&ipv4, ""),
|
||||
},
|
||||
{
|
||||
name: "allows ipfamily to configured ipv4 value when dual stack is in use",
|
||||
cidr: "10.0.0.0/16",
|
||||
enableDualStack: true,
|
||||
configureDualStack: true,
|
||||
in: withIP(nil, ""),
|
||||
expect: withIP(&ipv4, ""),
|
||||
},
|
||||
{
|
||||
name: "allows ipfamily to configured ipv6 value",
|
||||
cidr: "fd00::/64",
|
||||
enableDualStack: true,
|
||||
in: withIP(nil, ""),
|
||||
expect: withIP(&ipv6, ""),
|
||||
},
|
||||
{
|
||||
name: "allows ipfamily to configured ipv6 value when dual stack is in use",
|
||||
cidr: "fd00::/64",
|
||||
enableDualStack: true,
|
||||
configureDualStack: true,
|
||||
in: withIP(nil, ""),
|
||||
expect: withIP(&ipv6, ""),
|
||||
},
|
||||
|
||||
{
|
||||
name: "rejects ipv6 ipfamily when single-stack ipv4",
|
||||
enableDualStack: true,
|
||||
cidr: "10.0.0.0/16",
|
||||
in: withIP(&ipv6, ""),
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "rejects ipv4 ipfamily when single-stack ipv6",
|
||||
enableDualStack: true,
|
||||
cidr: "fd00::/64",
|
||||
in: withIP(&ipv4, ""),
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "rejects implicit ipv4 ipfamily when single-stack ipv6",
|
||||
enableDualStack: true,
|
||||
cidr: "fd00::/64",
|
||||
in: withIP(nil, "10.0.1.0"),
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "rejects implicit ipv6 ipfamily when single-stack ipv4",
|
||||
enableDualStack: true,
|
||||
cidr: "10.0.0.0/16",
|
||||
in: withIP(nil, "fd00::1"),
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.IPv6DualStack, tc.enableDualStack)()
|
||||
testStrategy, _ := newStrategy(tc.cidr, tc.configureDualStack)
|
||||
ctx := genericapirequest.NewDefaultContext()
|
||||
err := rest.BeforeCreate(testStrategy, ctx, runtime.Object(tc.in))
|
||||
if tc.expectErr != (err != nil) {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if tc.expect != nil && tc.in != nil {
|
||||
tc.expect.ObjectMeta = tc.in.ObjectMeta
|
||||
}
|
||||
if !reflect.DeepEqual(tc.expect, tc.in) {
|
||||
t.Fatalf("unexpected change: %s", diff.ObjectReflectDiff(tc.expect, tc.in))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBeforeUpdate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
tweakSvc func(oldSvc, newSvc *api.Service) // given basic valid services, each test case can customize them
|
||||
expectErr bool
|
||||
name string
|
||||
enableDualStack bool
|
||||
defaultIPv6 bool
|
||||
allowSecondary bool
|
||||
tweakSvc func(oldSvc, newSvc *api.Service) // given basic valid services, each test case can customize them
|
||||
expectErr bool
|
||||
expectObj func(t *testing.T, svc *api.Service)
|
||||
}{
|
||||
{
|
||||
name: "no change",
|
||||
@ -195,6 +328,19 @@ func TestBeforeUpdate(t *testing.T) {
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "clear IP family is allowed (defaulted back by before update)",
|
||||
enableDualStack: true,
|
||||
tweakSvc: func(oldSvc, newSvc *api.Service) {
|
||||
oldSvc.Spec.IPFamily = nil
|
||||
},
|
||||
expectErr: false,
|
||||
expectObj: func(t *testing.T, svc *api.Service) {
|
||||
if svc.Spec.IPFamily == nil {
|
||||
t.Errorf("ipfamily was not defaulted")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "change selector",
|
||||
tweakSvc: func(oldSvc, newSvc *api.Service) {
|
||||
@ -205,23 +351,36 @@ func TestBeforeUpdate(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
oldSvc := makeValidService()
|
||||
newSvc := makeValidService()
|
||||
tc.tweakSvc(&oldSvc, &newSvc)
|
||||
ctx := genericapirequest.NewDefaultContext()
|
||||
err := rest.BeforeUpdate(Strategy, ctx, runtime.Object(&oldSvc), runtime.Object(&newSvc))
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("unexpected non-error for %q", tc.name)
|
||||
}
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("unexpected error for %q: %v", tc.name, err)
|
||||
}
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.IPv6DualStack, tc.enableDualStack)()
|
||||
var cidr string
|
||||
if tc.defaultIPv6 {
|
||||
cidr = "ffd0::/64"
|
||||
} else {
|
||||
cidr = "172.30.0.0/16"
|
||||
}
|
||||
strategy, _ := newStrategy(cidr, tc.allowSecondary)
|
||||
oldSvc := makeValidService()
|
||||
newSvc := makeValidService()
|
||||
tc.tweakSvc(&oldSvc, &newSvc)
|
||||
ctx := genericapirequest.NewDefaultContext()
|
||||
err := rest.BeforeUpdate(strategy, ctx, runtime.Object(&newSvc), runtime.Object(&oldSvc))
|
||||
if tc.expectObj != nil {
|
||||
tc.expectObj(t, &newSvc)
|
||||
}
|
||||
if tc.expectErr && err == nil {
|
||||
t.Fatalf("unexpected non-error: %v", err)
|
||||
}
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceStatusStrategy(t *testing.T) {
|
||||
_, testStatusStrategy := newStrategy("10.0.0.0/16", false)
|
||||
ctx := genericapirequest.NewDefaultContext()
|
||||
if !StatusStrategy.NamespaceScoped() {
|
||||
if !testStatusStrategy.NamespaceScoped() {
|
||||
t.Errorf("Service must be namespace scoped")
|
||||
}
|
||||
oldService := makeValidService()
|
||||
@ -236,23 +395,23 @@ func TestServiceStatusStrategy(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
StatusStrategy.PrepareForUpdate(ctx, &newService, &oldService)
|
||||
testStatusStrategy.PrepareForUpdate(ctx, &newService, &oldService)
|
||||
if newService.Status.LoadBalancer.Ingress[0].IP != "127.0.0.2" {
|
||||
t.Errorf("Service status updates should allow change of status fields")
|
||||
}
|
||||
if newService.Spec.SessionAffinity != "None" {
|
||||
t.Errorf("PrepareForUpdate should have preserved old spec")
|
||||
}
|
||||
errs := StatusStrategy.ValidateUpdate(ctx, &newService, &oldService)
|
||||
errs := testStatusStrategy.ValidateUpdate(ctx, &newService, &oldService)
|
||||
if len(errs) != 0 {
|
||||
t.Errorf("Unexpected error %v", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func makeServiceWithIPFamily(IPFamily *api.IPFamily) *api.Service {
|
||||
func makeServiceWithIPFamily(ipFamily *api.IPFamily) *api.Service {
|
||||
return &api.Service{
|
||||
Spec: api.ServiceSpec{
|
||||
IPFamily: IPFamily,
|
||||
IPFamily: ipFamily,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -294,6 +453,20 @@ func TestDropDisabledField(t *testing.T) {
|
||||
oldSvc: nil,
|
||||
compareSvc: makeServiceWithIPFamily(&ipv6Service),
|
||||
},
|
||||
{
|
||||
name: "dualstack, field used, changed",
|
||||
enableDualStack: true,
|
||||
svc: makeServiceWithIPFamily(&ipv6Service),
|
||||
oldSvc: makeServiceWithIPFamily(&ipv4Service),
|
||||
compareSvc: makeServiceWithIPFamily(&ipv6Service),
|
||||
},
|
||||
{
|
||||
name: "dualstack, field used, not changed",
|
||||
enableDualStack: true,
|
||||
svc: makeServiceWithIPFamily(&ipv6Service),
|
||||
oldSvc: makeServiceWithIPFamily(&ipv6Service),
|
||||
compareSvc: makeServiceWithIPFamily(&ipv6Service),
|
||||
},
|
||||
|
||||
/* add more tests for other dropped fields as needed */
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user