mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-04 18:00:08 +00:00
Svc REST: De-layer Create
Gut the "outer" Create() and move it to the inner BeginCreate(). This uses a "transaction" type to make cleanup functions easy to read. Background: Service has an "outer" and "inner" REST handler. This is because of how we do IP and port allocations synchronously, but since we don't have API transactions, we need to roll those back in case of a failure. Both layers use the same `Strategy`, but the outer calls into the inner, which causes a lot of complexity in the code (including an open-coded partial reimplementation of a date-unknown snapshot of the generic REST code) and results in `Prepare` and `Validate` hooks being called twice. The "normal" REST flow seems to be: ``` mutating webhooks generic REST store Create { cleanup = BeginCreate BeforeCreate { strategy.PrepareForCreate { dropDisabledFields } strategy.Validate strategy.Canonicalize } createValidation (validating webhooks) storage Create cleanup AfterCreate Decorator } ``` Service (before this commit) does: ``` mutating webhooks svc custom Create { BeforeCreate { strategy.PrepareForCreate { dropDisabledFields } strategy.Validate strategy.Canonicalize } Allocations inner (generic) Create { cleanup = BeginCreate BeforeCreate { strategy.PrepareForCreate { dropDisabledFields } strategy.Validate strategy.Canonicalize } createValidation (validating webhooks) storage Create cleanup AfterCreate Decorator } } ``` After this commit: ``` mutating webhooks generic REST store Create { cleanup = BeginCreate Allocations BeforeCreate { strategy.PrepareForCreate { dropDisabledFields } strategy.Validate strategy.Canonicalize } createValidation (validating webhooks) storage Create cleanup AfterCreate Rollback allocations on error Decorator } ``` This same fix pattern will be applied to Delete and Update in subsequent commits.
This commit is contained in:
parent
5e7e35ca45
commit
634055bded
@ -261,7 +261,7 @@ func (c LegacyRESTStorageProvider) NewLegacyRESTStorage(restOptionsGetter generi
|
|||||||
serviceIPAllocators[secondaryServiceClusterIPAllocator.IPFamily()] = secondaryServiceClusterIPAllocator
|
serviceIPAllocators[secondaryServiceClusterIPAllocator.IPFamily()] = secondaryServiceClusterIPAllocator
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceRESTStorage, serviceStatusStorage, err := servicestore.NewGenericREST(restOptionsGetter, serviceClusterIPRange, secondaryServiceClusterIPAllocator != nil)
|
serviceRESTStorage, serviceStatusStorage, err := servicestore.NewGenericREST(restOptionsGetter, serviceClusterIPAllocator.IPFamily(), serviceIPAllocators, serviceNodePortAllocator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return LegacyRESTStorage{}, genericapiserver.APIGroupInfo{}, err
|
return LegacyRESTStorage{}, genericapiserver.APIGroupInfo{}, err
|
||||||
}
|
}
|
||||||
|
@ -177,41 +177,7 @@ func (rs *REST) Watch(ctx context.Context, options *metainternalversion.ListOpti
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rs *REST) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
|
func (rs *REST) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
|
||||||
// DeepCopy to prevent writes here propagating back to tests.
|
return rs.services.Create(ctx, obj, createValidation, options)
|
||||||
obj = obj.DeepCopyObject()
|
|
||||||
|
|
||||||
service := obj.(*api.Service)
|
|
||||||
|
|
||||||
if err := rest.BeforeCreate(rs.strategy, ctx, obj); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allocate IPs and ports. If we had a transactional store, this would just
|
|
||||||
// be part of the larger transaction. We don't have that, so we have to do
|
|
||||||
// it manually. This has to happen here and not in any earlier hooks (e.g.
|
|
||||||
// defaulting) because it needs to be aware of flags and be able to access
|
|
||||||
// API storage.
|
|
||||||
txn, err := rs.alloc.allocateCreate(service, dryrun.IsDryRun(options.DryRun))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if txn != nil {
|
|
||||||
txn.Revert()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
out, err := rs.services.Create(ctx, service, createValidation, options)
|
|
||||||
if err != nil {
|
|
||||||
err = rest.CheckGeneratedNameError(ctx, rs.strategy, err, service)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
txn.Commit()
|
|
||||||
txn = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return out, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (al *RESTAllocStuff) allocateCreate(service *api.Service, dryRun bool) (transaction, error) {
|
func (al *RESTAllocStuff) allocateCreate(service *api.Service, dryRun bool) (transaction, error) {
|
||||||
|
@ -37,6 +37,7 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/registry/generic"
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
|
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
|
||||||
|
"k8s.io/apiserver/pkg/storage/storagebackend"
|
||||||
"k8s.io/apiserver/pkg/util/dryrun"
|
"k8s.io/apiserver/pkg/util/dryrun"
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||||
@ -60,6 +61,7 @@ import (
|
|||||||
// in a completely different way. We should unify it.
|
// in a completely different way. We should unify it.
|
||||||
|
|
||||||
type serviceStorage struct {
|
type serviceStorage struct {
|
||||||
|
inner *GenericREST
|
||||||
Services map[string]*api.Service
|
Services map[string]*api.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,12 +120,16 @@ func (s *serviceStorage) New() runtime.Object {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *serviceStorage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
|
func (s *serviceStorage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
|
||||||
if dryrun.IsDryRun(options.DryRun) {
|
ret, err := s.inner.Create(ctx, obj, createValidation, options)
|
||||||
return obj, nil
|
if err != nil {
|
||||||
|
return ret, err
|
||||||
}
|
}
|
||||||
svc := obj.(*api.Service)
|
|
||||||
|
if dryrun.IsDryRun(options.DryRun) {
|
||||||
|
return ret.DeepCopyObject(), nil
|
||||||
|
}
|
||||||
|
svc := ret.(*api.Service)
|
||||||
s.saveService(svc)
|
s.saveService(svc)
|
||||||
s.Services[svc.Name].ResourceVersion = "1"
|
|
||||||
|
|
||||||
return s.Services[svc.Name].DeepCopy(), nil
|
return s.Services[svc.Name].DeepCopy(), nil
|
||||||
}
|
}
|
||||||
@ -173,8 +179,6 @@ func NewTestREST(t *testing.T, ipFamilies []api.IPFamily) (*REST, *etcd3testing.
|
|||||||
func NewTestRESTWithPods(t *testing.T, endpoints []*api.Endpoints, pods []api.Pod, ipFamilies []api.IPFamily) (*REST, *etcd3testing.EtcdTestServer) {
|
func NewTestRESTWithPods(t *testing.T, endpoints []*api.Endpoints, pods []api.Pod, ipFamilies []api.IPFamily) (*REST, *etcd3testing.EtcdTestServer) {
|
||||||
etcdStorage, server := registrytest.NewEtcdStorage(t, "")
|
etcdStorage, server := registrytest.NewEtcdStorage(t, "")
|
||||||
|
|
||||||
serviceStorage := &serviceStorage{}
|
|
||||||
|
|
||||||
podStorage, err := podstore.NewStorage(generic.RESTOptions{
|
podStorage, err := podstore.NewStorage(generic.RESTOptions{
|
||||||
StorageConfig: etcdStorage.ForResource(schema.GroupResource{Resource: "pods"}),
|
StorageConfig: etcdStorage.ForResource(schema.GroupResource{Resource: "pods"}),
|
||||||
Decorator: generic.UndecoratedStorage,
|
Decorator: generic.UndecoratedStorage,
|
||||||
@ -246,11 +250,29 @@ func NewTestRESTWithPods(t *testing.T, endpoints []*api.Endpoints, pods []api.Po
|
|||||||
if rSecondary != nil {
|
if rSecondary != nil {
|
||||||
ipAllocators[rSecondary.IPFamily()] = rSecondary
|
ipAllocators[rSecondary.IPFamily()] = rSecondary
|
||||||
}
|
}
|
||||||
rest, _ := NewREST(serviceStorage, endpointStorage, podStorage.Pod, rPrimary.IPFamily(), ipAllocators, portAllocator, nil)
|
|
||||||
|
inner := newInnerREST(t, etcdStorage, ipAllocators, portAllocator)
|
||||||
|
rest, _ := NewREST(inner, endpointStorage, podStorage.Pod, rPrimary.IPFamily(), ipAllocators, portAllocator, nil)
|
||||||
|
|
||||||
return rest, server
|
return rest, server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This bridges to the "inner" REST implementation so tests continue to run
|
||||||
|
// during the delayering of service REST code.
|
||||||
|
func newInnerREST(t *testing.T, etcdStorage *storagebackend.ConfigForResource, ipAllocs map[api.IPFamily]ipallocator.Interface, portAlloc portallocator.Interface) *serviceStorage {
|
||||||
|
restOptions := generic.RESTOptions{
|
||||||
|
StorageConfig: etcdStorage.ForResource(schema.GroupResource{Resource: "services"}),
|
||||||
|
Decorator: generic.UndecoratedStorage,
|
||||||
|
DeleteCollectionWorkers: 1,
|
||||||
|
ResourcePrefix: "services",
|
||||||
|
}
|
||||||
|
inner, _, err := NewGenericREST(restOptions, api.IPv4Protocol, ipAllocs, portAlloc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error from REST storage: %v", err)
|
||||||
|
}
|
||||||
|
return &serviceStorage{inner: inner}
|
||||||
|
}
|
||||||
|
|
||||||
func makeIPNet(t *testing.T) *net.IPNet {
|
func makeIPNet(t *testing.T) *net.IPNet {
|
||||||
_, net, err := netutils.ParseCIDRSloppy("1.2.3.0/24")
|
_, net, err := netutils.ParseCIDRSloppy("1.2.3.0/24")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -18,13 +18,13 @@ package storage
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apiserver/pkg/registry/generic"
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
|
"k8s.io/apiserver/pkg/util/dryrun"
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
api "k8s.io/kubernetes/pkg/apis/core"
|
api "k8s.io/kubernetes/pkg/apis/core"
|
||||||
"k8s.io/kubernetes/pkg/features"
|
"k8s.io/kubernetes/pkg/features"
|
||||||
@ -32,8 +32,9 @@ import (
|
|||||||
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
|
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
|
||||||
printerstorage "k8s.io/kubernetes/pkg/printers/storage"
|
printerstorage "k8s.io/kubernetes/pkg/printers/storage"
|
||||||
"k8s.io/kubernetes/pkg/registry/core/service"
|
"k8s.io/kubernetes/pkg/registry/core/service"
|
||||||
registry "k8s.io/kubernetes/pkg/registry/core/service"
|
|
||||||
svcreg "k8s.io/kubernetes/pkg/registry/core/service"
|
svcreg "k8s.io/kubernetes/pkg/registry/core/service"
|
||||||
|
"k8s.io/kubernetes/pkg/registry/core/service/ipallocator"
|
||||||
|
"k8s.io/kubernetes/pkg/registry/core/service/portallocator"
|
||||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||||
|
|
||||||
netutil "k8s.io/utils/net"
|
netutil "k8s.io/utils/net"
|
||||||
@ -41,13 +42,19 @@ import (
|
|||||||
|
|
||||||
type GenericREST struct {
|
type GenericREST struct {
|
||||||
*genericregistry.Store
|
*genericregistry.Store
|
||||||
primaryIPFamily *api.IPFamily
|
primaryIPFamily api.IPFamily
|
||||||
secondaryFamily *api.IPFamily
|
secondaryIPFamily api.IPFamily
|
||||||
|
alloc RESTAllocStuff
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGenericREST returns a RESTStorage object that will work against services.
|
// NewGenericREST returns a RESTStorage object that will work against services.
|
||||||
func NewGenericREST(optsGetter generic.RESTOptionsGetter, serviceCIDR net.IPNet, hasSecondary bool) (*GenericREST, *StatusREST, error) {
|
func NewGenericREST(
|
||||||
strategy, _ := registry.StrategyForServiceCIDRs(serviceCIDR, hasSecondary)
|
optsGetter generic.RESTOptionsGetter,
|
||||||
|
serviceIPFamily api.IPFamily,
|
||||||
|
ipAllocs map[api.IPFamily]ipallocator.Interface,
|
||||||
|
portAlloc portallocator.Interface) (*GenericREST, *StatusREST, error) {
|
||||||
|
|
||||||
|
strategy, _ := svcreg.StrategyForServiceCIDRs(ipAllocs[serviceIPFamily].CIDR(), len(ipAllocs) > 1)
|
||||||
|
|
||||||
store := &genericregistry.Store{
|
store := &genericregistry.Store{
|
||||||
NewFunc: func() runtime.Object { return &api.Service{} },
|
NewFunc: func() runtime.Object { return &api.Service{} },
|
||||||
@ -72,22 +79,12 @@ func NewGenericREST(optsGetter generic.RESTOptionsGetter, serviceCIDR net.IPNet,
|
|||||||
statusStore.UpdateStrategy = statusStrategy
|
statusStore.UpdateStrategy = statusStrategy
|
||||||
statusStore.ResetFieldsStrategy = statusStrategy
|
statusStore.ResetFieldsStrategy = statusStrategy
|
||||||
|
|
||||||
ipv4 := api.IPv4Protocol
|
var primaryIPFamily api.IPFamily = serviceIPFamily
|
||||||
ipv6 := api.IPv6Protocol
|
var secondaryIPFamily api.IPFamily = "" // sentinel value
|
||||||
var primaryIPFamily *api.IPFamily
|
if len(ipAllocs) > 1 {
|
||||||
var secondaryFamily *api.IPFamily
|
secondaryIPFamily = otherFamily(serviceIPFamily)
|
||||||
if netutil.IsIPv6CIDR(&serviceCIDR) {
|
|
||||||
primaryIPFamily = &ipv6
|
|
||||||
if hasSecondary {
|
|
||||||
secondaryFamily = &ipv4
|
|
||||||
}
|
}
|
||||||
} else {
|
genericStore := &GenericREST{store, primaryIPFamily, secondaryIPFamily, makeAlloc(serviceIPFamily, ipAllocs, portAlloc)}
|
||||||
primaryIPFamily = &ipv4
|
|
||||||
if hasSecondary {
|
|
||||||
secondaryFamily = &ipv6
|
|
||||||
}
|
|
||||||
}
|
|
||||||
genericStore := &GenericREST{store, primaryIPFamily, secondaryFamily}
|
|
||||||
store.Decorator = genericStore.defaultOnRead
|
store.Decorator = genericStore.defaultOnRead
|
||||||
store.BeginCreate = genericStore.beginCreate
|
store.BeginCreate = genericStore.beginCreate
|
||||||
store.BeginUpdate = genericStore.beginUpdate
|
store.BeginUpdate = genericStore.beginUpdate
|
||||||
@ -95,6 +92,15 @@ func NewGenericREST(optsGetter generic.RESTOptionsGetter, serviceCIDR net.IPNet,
|
|||||||
return genericStore, &StatusREST{store: &statusStore}, nil
|
return genericStore, &StatusREST{store: &statusStore}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// otherFamily returns the non-selected IPFamily. This assumes the input is
|
||||||
|
// valid.
|
||||||
|
func otherFamily(fam api.IPFamily) api.IPFamily {
|
||||||
|
if fam == api.IPv4Protocol {
|
||||||
|
return api.IPv6Protocol
|
||||||
|
}
|
||||||
|
return api.IPv4Protocol
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ rest.ShortNamesProvider = &GenericREST{}
|
_ rest.ShortNamesProvider = &GenericREST{}
|
||||||
_ rest.CategoriesProvider = &GenericREST{}
|
_ rest.CategoriesProvider = &GenericREST{}
|
||||||
@ -196,7 +202,7 @@ func (r *GenericREST) defaultOnReadService(service *api.Service) {
|
|||||||
preferDualStack := api.IPFamilyPolicyPreferDualStack
|
preferDualStack := api.IPFamilyPolicyPreferDualStack
|
||||||
// headless services
|
// headless services
|
||||||
if len(service.Spec.ClusterIPs) == 1 && service.Spec.ClusterIPs[0] == api.ClusterIPNone {
|
if len(service.Spec.ClusterIPs) == 1 && service.Spec.ClusterIPs[0] == api.ClusterIPNone {
|
||||||
service.Spec.IPFamilies = []api.IPFamily{*r.primaryIPFamily}
|
service.Spec.IPFamilies = []api.IPFamily{r.primaryIPFamily}
|
||||||
|
|
||||||
// headless+selectorless
|
// headless+selectorless
|
||||||
// headless+selectorless takes both families. Why?
|
// headless+selectorless takes both families. Why?
|
||||||
@ -205,7 +211,7 @@ func (r *GenericREST) defaultOnReadService(service *api.Service) {
|
|||||||
// it to PreferDualStack on any cluster (single or dualstack configured).
|
// it to PreferDualStack on any cluster (single or dualstack configured).
|
||||||
if len(service.Spec.Selector) == 0 {
|
if len(service.Spec.Selector) == 0 {
|
||||||
service.Spec.IPFamilyPolicy = &preferDualStack
|
service.Spec.IPFamilyPolicy = &preferDualStack
|
||||||
if *r.primaryIPFamily == api.IPv4Protocol {
|
if r.primaryIPFamily == api.IPv4Protocol {
|
||||||
service.Spec.IPFamilies = append(service.Spec.IPFamilies, api.IPv6Protocol)
|
service.Spec.IPFamilies = append(service.Spec.IPFamilies, api.IPv6Protocol)
|
||||||
} else {
|
} else {
|
||||||
service.Spec.IPFamilies = append(service.Spec.IPFamilies, api.IPv4Protocol)
|
service.Spec.IPFamilies = append(service.Spec.IPFamilies, api.IPv4Protocol)
|
||||||
@ -216,8 +222,8 @@ func (r *GenericREST) defaultOnReadService(service *api.Service) {
|
|||||||
// selector and will have to follow how the cluster is configured. If the cluster is
|
// selector and will have to follow how the cluster is configured. If the cluster is
|
||||||
// configured to dual stack then the service defaults to PreferDualStack. Otherwise we
|
// configured to dual stack then the service defaults to PreferDualStack. Otherwise we
|
||||||
// default it to SingleStack.
|
// default it to SingleStack.
|
||||||
if r.secondaryFamily != nil {
|
if r.secondaryIPFamily != "" {
|
||||||
service.Spec.IPFamilies = append(service.Spec.IPFamilies, *r.secondaryFamily)
|
service.Spec.IPFamilies = append(service.Spec.IPFamilies, r.secondaryIPFamily)
|
||||||
service.Spec.IPFamilyPolicy = &preferDualStack
|
service.Spec.IPFamilyPolicy = &preferDualStack
|
||||||
} else {
|
} else {
|
||||||
service.Spec.IPFamilyPolicy = &singleStack
|
service.Spec.IPFamilyPolicy = &singleStack
|
||||||
@ -246,13 +252,27 @@ func (r *GenericREST) defaultOnReadService(service *api.Service) {
|
|||||||
func (r *GenericREST) beginCreate(ctx context.Context, obj runtime.Object, options *metav1.CreateOptions) (genericregistry.FinishFunc, error) {
|
func (r *GenericREST) beginCreate(ctx context.Context, obj runtime.Object, options *metav1.CreateOptions) (genericregistry.FinishFunc, error) {
|
||||||
svc := obj.(*api.Service)
|
svc := obj.(*api.Service)
|
||||||
|
|
||||||
// FIXME: remove this when implementing
|
// Make sure ClusterIP and ClusterIPs are in sync. This has to happen
|
||||||
_ = svc
|
// early, before anyone looks at them.
|
||||||
|
// NOTE: the args are (old, new)
|
||||||
|
svcreg.NormalizeClusterIPs(nil, svc)
|
||||||
|
|
||||||
|
// Allocate IPs and ports. If we had a transactional store, this would just
|
||||||
|
// be part of the larger transaction. We don't have that, so we have to do
|
||||||
|
// it manually. This has to happen here and not in any earlier hooks (e.g.
|
||||||
|
// defaulting) because it needs to be aware of flags and be able to access
|
||||||
|
// API storage.
|
||||||
|
txn, err := r.alloc.allocateCreate(svc, dryrun.IsDryRun(options.DryRun))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Our cleanup callback
|
// Our cleanup callback
|
||||||
finish := func(_ context.Context, success bool) {
|
finish := func(_ context.Context, success bool) {
|
||||||
if success {
|
if success {
|
||||||
|
txn.Commit()
|
||||||
} else {
|
} else {
|
||||||
|
txn.Revert()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,9 +283,10 @@ func (r *GenericREST) beginUpdate(ctx context.Context, obj, oldObj runtime.Objec
|
|||||||
newSvc := obj.(*api.Service)
|
newSvc := obj.(*api.Service)
|
||||||
oldSvc := oldObj.(*api.Service)
|
oldSvc := oldObj.(*api.Service)
|
||||||
|
|
||||||
// FIXME: remove these when implementing
|
// Make sure ClusterIP and ClusterIPs are in sync. This has to happen
|
||||||
_ = oldSvc
|
// early, before anyone looks at them.
|
||||||
_ = newSvc
|
// NOTE: the args are (old, new)
|
||||||
|
svcreg.NormalizeClusterIPs(oldSvc, newSvc)
|
||||||
|
|
||||||
// Our cleanup callback
|
// Our cleanup callback
|
||||||
finish := func(_ context.Context, success bool) {
|
finish := func(_ context.Context, success bool) {
|
||||||
|
@ -17,6 +17,8 @@ limitations under the License.
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -30,6 +32,7 @@ import (
|
|||||||
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
||||||
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
|
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
|
||||||
api "k8s.io/kubernetes/pkg/apis/core"
|
api "k8s.io/kubernetes/pkg/apis/core"
|
||||||
|
"k8s.io/kubernetes/pkg/registry/core/service/ipallocator"
|
||||||
"k8s.io/kubernetes/pkg/registry/registrytest"
|
"k8s.io/kubernetes/pkg/registry/registrytest"
|
||||||
netutils "k8s.io/utils/net"
|
netutils "k8s.io/utils/net"
|
||||||
|
|
||||||
@ -38,6 +41,14 @@ import (
|
|||||||
"k8s.io/kubernetes/pkg/features"
|
"k8s.io/kubernetes/pkg/features"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func makeIPAllocator(cidr *net.IPNet) ipallocator.Interface {
|
||||||
|
al, err := ipallocator.NewInMemory(cidr)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("error creating IP allocator: %v", err))
|
||||||
|
}
|
||||||
|
return al
|
||||||
|
}
|
||||||
|
|
||||||
func newStorage(t *testing.T) (*GenericREST, *StatusREST, *etcd3testing.EtcdTestServer) {
|
func newStorage(t *testing.T) (*GenericREST, *StatusREST, *etcd3testing.EtcdTestServer) {
|
||||||
etcdStorage, server := registrytest.NewEtcdStorage(t, "")
|
etcdStorage, server := registrytest.NewEtcdStorage(t, "")
|
||||||
restOptions := generic.RESTOptions{
|
restOptions := generic.RESTOptions{
|
||||||
@ -46,7 +57,10 @@ func newStorage(t *testing.T) (*GenericREST, *StatusREST, *etcd3testing.EtcdTest
|
|||||||
DeleteCollectionWorkers: 1,
|
DeleteCollectionWorkers: 1,
|
||||||
ResourcePrefix: "services",
|
ResourcePrefix: "services",
|
||||||
}
|
}
|
||||||
serviceStorage, statusStorage, err := NewGenericREST(restOptions, *makeIPNet(t), false)
|
ipAllocs := map[api.IPFamily]ipallocator.Interface{
|
||||||
|
api.IPv4Protocol: makeIPAllocator(makeIPNet(t)),
|
||||||
|
}
|
||||||
|
serviceStorage, statusStorage, err := NewGenericREST(restOptions, api.IPv4Protocol, ipAllocs, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error from REST storage: %v", err)
|
t.Fatalf("unexpected error from REST storage: %v", err)
|
||||||
}
|
}
|
||||||
@ -427,7 +441,10 @@ func TestServiceDefaultOnRead(t *testing.T) {
|
|||||||
t.Fatalf("failed to parse CIDR")
|
t.Fatalf("failed to parse CIDR")
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceStorage, _, err := NewGenericREST(restOptions, *cidr, false)
|
ipAllocs := map[api.IPFamily]ipallocator.Interface{
|
||||||
|
api.IPv4Protocol: makeIPAllocator(cidr),
|
||||||
|
}
|
||||||
|
serviceStorage, _, err := NewGenericREST(restOptions, api.IPv4Protocol, ipAllocs, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error from REST storage: %v", err)
|
t.Fatalf("unexpected error from REST storage: %v", err)
|
||||||
}
|
}
|
||||||
@ -471,7 +488,7 @@ func TestServiceDefaultOnRead(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestServiceDefaulting(t *testing.T) {
|
func TestServiceDefaulting(t *testing.T) {
|
||||||
makeStorage := func(t *testing.T, primaryCIDR string, isDualStack bool) (*GenericREST, *StatusREST, *etcd3testing.EtcdTestServer) {
|
makeStorage := func(t *testing.T, ipFamilies []api.IPFamily) (*GenericREST, *StatusREST, *etcd3testing.EtcdTestServer) {
|
||||||
etcdStorage, server := registrytest.NewEtcdStorage(t, "")
|
etcdStorage, server := registrytest.NewEtcdStorage(t, "")
|
||||||
restOptions := generic.RESTOptions{
|
restOptions := generic.RESTOptions{
|
||||||
StorageConfig: etcdStorage.ForResource(schema.GroupResource{Resource: "services"}),
|
StorageConfig: etcdStorage.ForResource(schema.GroupResource{Resource: "services"}),
|
||||||
@ -480,12 +497,19 @@ func TestServiceDefaulting(t *testing.T) {
|
|||||||
ResourcePrefix: "services",
|
ResourcePrefix: "services",
|
||||||
}
|
}
|
||||||
|
|
||||||
_, cidr, err := netutils.ParseCIDRSloppy(primaryCIDR)
|
ipAllocs := map[api.IPFamily]ipallocator.Interface{}
|
||||||
if err != nil {
|
for _, fam := range ipFamilies {
|
||||||
t.Fatalf("failed to parse CIDR %s", primaryCIDR)
|
switch fam {
|
||||||
|
case api.IPv4Protocol:
|
||||||
|
_, cidr, _ := netutils.ParseCIDRSloppy("10.0.0.0/16")
|
||||||
|
ipAllocs[fam] = makeIPAllocator(cidr)
|
||||||
|
case api.IPv6Protocol:
|
||||||
|
_, cidr, _ := netutils.ParseCIDRSloppy("2000::/108")
|
||||||
|
ipAllocs[fam] = makeIPAllocator(cidr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceStorage, statusStorage, err := NewGenericREST(restOptions, *(cidr), isDualStack)
|
serviceStorage, statusStorage, err := NewGenericREST(restOptions, ipFamilies[0], ipAllocs, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error from REST storage: %v", err)
|
t.Fatalf("unexpected error from REST storage: %v", err)
|
||||||
}
|
}
|
||||||
@ -494,34 +518,24 @@ func TestServiceDefaulting(t *testing.T) {
|
|||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
primaryCIDR string
|
ipFamilies []api.IPFamily
|
||||||
PrimaryIPv6 bool
|
|
||||||
isDualStack bool
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "IPv4 single stack cluster",
|
name: "IPv4 single stack cluster",
|
||||||
primaryCIDR: "10.0.0.0/16",
|
ipFamilies: []api.IPFamily{api.IPv4Protocol},
|
||||||
PrimaryIPv6: false,
|
|
||||||
isDualStack: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "IPv6 single stack cluster",
|
name: "IPv6 single stack cluster",
|
||||||
primaryCIDR: "2000::/108",
|
ipFamilies: []api.IPFamily{api.IPv6Protocol},
|
||||||
PrimaryIPv6: true,
|
|
||||||
isDualStack: false,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "IPv4, IPv6 dual stack cluster",
|
name: "IPv4, IPv6 dual stack cluster",
|
||||||
primaryCIDR: "10.0.0.0/16",
|
ipFamilies: []api.IPFamily{api.IPv4Protocol, api.IPv6Protocol},
|
||||||
PrimaryIPv6: false,
|
|
||||||
isDualStack: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "IPv6, IPv4 dual stack cluster",
|
name: "IPv6, IPv4 dual stack cluster",
|
||||||
primaryCIDR: "2000::/108",
|
ipFamilies: []api.IPFamily{api.IPv6Protocol, api.IPv4Protocol},
|
||||||
PrimaryIPv6: true,
|
|
||||||
isDualStack: true,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -533,7 +547,7 @@ func TestServiceDefaulting(t *testing.T) {
|
|||||||
// this func only works with dual stack feature gate on.
|
// this func only works with dual stack feature gate on.
|
||||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.IPv6DualStack, true)()
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.IPv6DualStack, true)()
|
||||||
|
|
||||||
storage, _, server := makeStorage(t, testCase.primaryCIDR, testCase.isDualStack)
|
storage, _, server := makeStorage(t, testCase.ipFamilies)
|
||||||
defer server.Terminate(t)
|
defer server.Terminate(t)
|
||||||
defer storage.Store.DestroyFunc()
|
defer storage.Store.DestroyFunc()
|
||||||
|
|
||||||
@ -551,7 +565,7 @@ func TestServiceDefaulting(t *testing.T) {
|
|||||||
defaultedServiceList.Items[0].Spec.IPFamilyPolicy = &singleStack
|
defaultedServiceList.Items[0].Spec.IPFamilyPolicy = &singleStack
|
||||||
|
|
||||||
// primary family
|
// primary family
|
||||||
if testCase.PrimaryIPv6 {
|
if testCase.ipFamilies[0] == api.IPv6Protocol {
|
||||||
// no selector, gets both families
|
// no selector, gets both families
|
||||||
defaultedServiceList.Items[1].Spec.IPFamilyPolicy = &preferDualStack
|
defaultedServiceList.Items[1].Spec.IPFamilyPolicy = &preferDualStack
|
||||||
defaultedServiceList.Items[1].Spec.IPFamilies = []api.IPFamily{api.IPv6Protocol, api.IPv4Protocol}
|
defaultedServiceList.Items[1].Spec.IPFamilies = []api.IPFamily{api.IPv6Protocol, api.IPv4Protocol}
|
||||||
@ -559,7 +573,7 @@ func TestServiceDefaulting(t *testing.T) {
|
|||||||
//assume single stack for w/selector
|
//assume single stack for w/selector
|
||||||
defaultedServiceList.Items[0].Spec.IPFamilies = []api.IPFamily{api.IPv6Protocol}
|
defaultedServiceList.Items[0].Spec.IPFamilies = []api.IPFamily{api.IPv6Protocol}
|
||||||
// make dualstacked. if needed
|
// make dualstacked. if needed
|
||||||
if testCase.isDualStack {
|
if len(testCase.ipFamilies) > 1 {
|
||||||
defaultedServiceList.Items[0].Spec.IPFamilyPolicy = &preferDualStack
|
defaultedServiceList.Items[0].Spec.IPFamilyPolicy = &preferDualStack
|
||||||
defaultedServiceList.Items[0].Spec.IPFamilies = append(defaultedServiceList.Items[0].Spec.IPFamilies, api.IPv4Protocol)
|
defaultedServiceList.Items[0].Spec.IPFamilies = append(defaultedServiceList.Items[0].Spec.IPFamilies, api.IPv4Protocol)
|
||||||
}
|
}
|
||||||
@ -571,7 +585,7 @@ func TestServiceDefaulting(t *testing.T) {
|
|||||||
// assume single stack for w/selector
|
// assume single stack for w/selector
|
||||||
defaultedServiceList.Items[0].Spec.IPFamilies = []api.IPFamily{api.IPv4Protocol}
|
defaultedServiceList.Items[0].Spec.IPFamilies = []api.IPFamily{api.IPv4Protocol}
|
||||||
// make dualstacked. if needed
|
// make dualstacked. if needed
|
||||||
if testCase.isDualStack {
|
if len(testCase.ipFamilies) > 1 {
|
||||||
defaultedServiceList.Items[0].Spec.IPFamilyPolicy = &preferDualStack
|
defaultedServiceList.Items[0].Spec.IPFamilyPolicy = &preferDualStack
|
||||||
defaultedServiceList.Items[0].Spec.IPFamilies = append(defaultedServiceList.Items[0].Spec.IPFamilies, api.IPv6Protocol)
|
defaultedServiceList.Items[0].Spec.IPFamilies = append(defaultedServiceList.Items[0].Spec.IPFamilies, api.IPv6Protocol)
|
||||||
}
|
}
|
||||||
|
@ -109,6 +109,7 @@ func (strategy svcStrategy) PrepareForCreate(ctx context.Context, obj runtime.Ob
|
|||||||
service := obj.(*api.Service)
|
service := obj.(*api.Service)
|
||||||
service.Status = api.ServiceStatus{}
|
service.Status = api.ServiceStatus{}
|
||||||
|
|
||||||
|
//FIXME: Normalize is now called from BeginCreate in pkg/registry/core/service/storage
|
||||||
NormalizeClusterIPs(nil, service)
|
NormalizeClusterIPs(nil, service)
|
||||||
dropServiceDisabledFields(service, nil)
|
dropServiceDisabledFields(service, nil)
|
||||||
}
|
}
|
||||||
@ -120,6 +121,7 @@ func (strategy svcStrategy) PrepareForUpdate(ctx context.Context, obj, old runti
|
|||||||
newService.Status = oldService.Status
|
newService.Status = oldService.Status
|
||||||
|
|
||||||
patchAllocatedValues(newService, oldService)
|
patchAllocatedValues(newService, oldService)
|
||||||
|
//FIXME: Normalize is now called from BeginUpdate in pkg/registry/core/service/storage
|
||||||
NormalizeClusterIPs(oldService, newService)
|
NormalizeClusterIPs(oldService, newService)
|
||||||
dropServiceDisabledFields(newService, oldService)
|
dropServiceDisabledFields(newService, oldService)
|
||||||
dropTypeDependentFields(newService, oldService)
|
dropTypeDependentFields(newService, oldService)
|
||||||
@ -362,6 +364,7 @@ func patchAllocatedValues(newSvc, oldSvc *api.Service) {
|
|||||||
|
|
||||||
// NormalizeClusterIPs adjust clusterIPs based on ClusterIP. This must not
|
// NormalizeClusterIPs adjust clusterIPs based on ClusterIP. This must not
|
||||||
// consider any other fields.
|
// consider any other fields.
|
||||||
|
//FIXME: move this to pkg/registry/core/service/storage
|
||||||
func NormalizeClusterIPs(oldSvc, newSvc *api.Service) {
|
func NormalizeClusterIPs(oldSvc, newSvc *api.Service) {
|
||||||
// In all cases here, we don't need to over-think the inputs. Validation
|
// In all cases here, we don't need to over-think the inputs. Validation
|
||||||
// will be called on the new object soon enough. All this needs to do is
|
// will be called on the new object soon enough. All this needs to do is
|
||||||
|
Loading…
Reference in New Issue
Block a user